PAT – ADVANCED – 图算法精讲

本文主要讲解图的表示与经典算法在PAT解题中的算法选型、应用思路

联通分量问题

A1013 连通分量计数

数据结构:由于仅涉及节点编号,无其他信息,无向图的邻接表示用vector数组。
题意解析:敌人占领一个城市后,该城市与其他城市的道路均封闭,此时至少建几条路才能保证除该城市的所有城市保持联通。图中删去节点及邻路后,最少的建路方案是在剩余的n个连通分量之间建n-1条路,可以用DFS或并查集统计连通分量个数,不过并查集代码量略大一些


乀(ˉεˉ乀) Q:城市道路封闭是怎么考虑的呢?

(̿▀̿ ̿Ĺ̯̿̿▀̿ ̿) A:按理来说应该在图中删除该节点及所有邻边再去统计连通分量,但是修改了图结构。不删除的方法是统计连通分量时不统计这个节点,且DFS过程中无论哪个节点到达这个节点都返回,即没有这条邻边,相当于视其不存在

*=͟͟͞͞(꒪⌓꒪)** Q:我想的是新修路数=被占节点的相邻节点数-相邻节点之间的连通个数。而且觉得只看这K个节点就OK,没必要管全图。晕晕的~

(ง ˙o˙)ว A:你这个仅适用于这K个节点就是全图的情况,理由如下:如果去掉K图中的节点得到图K’仍然联通,这样看来就不需要建路了,但是你有没有想过,总体N除去K的子图本身也可能是不连通的呢?子图的不连通分量也要建的呀亲~所以每检查一个节点都要重新计算全图的连通分量个数。其次检测连接性用并查集吧,两两检测M个相邻节点之间的连接性要 $\scriptsize O(M^2)$,如果用DFS太慢了。哦对了,注意一下这道题目的节点默认标号是1-N,不要搞错

(⑉・̆-・̆⑉) Q:怎么求连通分量个数?

✧*。٩(ˊωˋ)و✧\ A:每次DFS都访问一整个联通分量,这样遍历图中每个节点,依次检查图的每一个连通分量计数即可。访问过的节点都是已知连通分量中的,未访问的节点进行DFS标记就是将要标记的一个新的连通分量


#include <iostream>
#include <vector>
#include <bits/stdc++.h>
using namespace std;

const int maxn = 1001;
vector<int> g[maxn];
bool isVisited[maxn];
int cur;

void DFS(int s) {
    if (s == cur) return;
    isVisited[s] = true;
    for (int &x : g[s])
        if (!isVisited[x])
            DFS(x);
}

int main() {
    int N, M, K;
    int s, t, cnt;
    scanf("%d %d %d", &N, &M, &K);
    for (int i = 0; i < M; i++) {
        scanf("%d %d", &s, &t);
        g[s].push_back(t);
        g[t].push_back(s);
    }

    for (int i = 0; i < K; i++) {
        cnt = 0;
        scanf("%d", &cur);
        memset(isVisited, 0, sizeof(isVisited));
        for (int i = 1; i <= N; i++) {
            if (!isVisited[i] && i != cur) {
                DFS(i);
                cnt++;
            }
        }
        printf("%d\n", cnt - 1);
    }

    return 0;
}

最短路径问题

最短路径问题的理论讲解详见 ✈最短路径经典算法

A1003 最短路径计数、双尺度点权最优

堆优化的Dijkstra

算法选型:节点个数多于200,排除 $\scriptsize O(n^3)$ 的Floyd。路径长度无负权值,优选队列优化的Dijkstra,采用链式前向星实现

思路解析:题意为三个目标:
①寻找最短路径:Dijkstra策略从源节点s逐步松弛各边即可
②记录最短路径条数:rNum记录源节点到当前节点的最短路径条数。从源节点出发记条数为1开始扩散。
每次执行松弛代表找到从s→from→to的唯一一条最短路径,而s→from可能有多条相同长度的最短路径,所以s→from→to的最短路径数就是s→from的最短路径数。
dis[from]+length=dis[to]表示找到s→from→to的另一条不同的最短路径,此时在原有基础上又多了rNum[from]条最短路径
③记录最短路径中的最大点权和:每找到一条最短路径,都更新为该条最短路径的点权和,最短路径长度相同时,更新为更小的点权和,更新公式为 $\scriptsize \sum \limits_{i=s}^{to}Wi = \sum \limits{i=s}^{from}Wi + W{to}$


(╯>д<)╯ Q:链式前向星是什么神秘的数据结构?

(:3▓▒ A:链式前向星由一个数组(表示节点)和一个结构体数组(表示边)和一个边地址累计索引(表示新边插入结构体数组的索引)组成。节点数组:对应下标表示节点编号,值指向最后一次插入的边地址。结构体数组:保存边权、该条有向边指向的节点、同顶点其他出边的索引。添加的新边依次保存在边数组[索引]、边数组[索引+1]…边数组[索引+i]中。每插入一条新边使其指向上一次插入的边old,再让该出边的节点指向新边new,这样就实现了vertex→new→old,串起了节点出发的所有邻边,所以本质上是图的邻接表表示。值得注意的是本题是无向图,建图的时候一定要分别添加两个方向的边。

(˘•ω•˘)ง Q:为什么我的Dijkstra算法没写错,但是运行一次就退出了,有时候还运行不对?

⁽⁽ଘ( ˊᵕˋ )ଓ⁾⁾ A:在建图后、松弛操作时、第二尺度判断时埋打印点,拿输出结果和实例比对一下,方便调试找到问题。很可能的原因是运行算法前初始化出错了。一定不要忘记初始条件下:源节点到自身距离0、源节点出发的最短路径条数1、源节点本身即是最大点权和


#include <iostream>
#include <queue>
#include <vector>
#include <bits/stdc++.h>
using namespace std;

const int maxV = 500;
const int maxE = 1e5;
const int inf = 1e6;
int vertex[maxV], dis[maxV], done[maxV], inq[maxV];
int tSum[maxV], rNum[maxV], tNum[maxV];
int p = 0;

struct e {
    int next;
    int to;
    int w;
} edge[maxE];

struct Cmp {
    bool operator()(int a, int b) {
        return dis[a] > dis[b];
    }
};

priority_queue<int, vector<int>, Cmp> q;

void addEdge(int u, int v, int w) {
    edge[p].to = v;
    edge[p].w = w;
    edge[p].next = vertex[u];
    vertex[u] = p++;
}

void dijkstra(int s) {
    q.push(s);
    dis[s] = 0;
    rNum[s] = 1;
    tSum[s] = tNum[s];
    while (!q.empty()) {
        int v = q.top();
        q.pop();
        done[v] = 1;
        inq[v] = 0;

        for (int i = vertex[v]; ~i; i = edge[i].next) {
            int w = edge[i].w;
            int to = edge[i].to;
            if (done[to]) continue;
            if (dis[v] + w < dis[to]) {
                dis[to] = dis[v] + w;
                tSum[to] = tSum[v] + tNum[to];
                rNum[to] = rNum[v];
                if (!inq[to]) {
                    q.push(to);
                    inq[to] = 1;
                }
            }
            else if (dis[v] + w == dis[to]) {
                if (tSum[v] + tNum[to] > tSum[to])
                    tSum[to] = tSum[v] + tNum[to];
                rNum[to] += rNum[v];
            }
        }
    }
}

int main() {
    int N, M, C1, C2;
    int u, v, w;
    scanf("%d %d %d %d", &N, &M, &C1, &C2);
    for (int i = 0; i < N; i++)
        scanf("%d", &tNum[i]);

    fill(dis, dis + N, inf);
    memset(vertex, -1, sizeof(vertex));
    memset(done, 0, sizeof(done));
    memset(inq, 0, sizeof(inq));
    memset(tSum, 0, sizeof(tSum));
    memset(rNum, 0, sizeof(rNum));

    for (int i = 0; i < M; i++) {
        scanf("%d %d %d", &u, &v, &w);
        addEdge(u, v, w);
        addEdge(v, u, w);
    }

    dijkstra(C1);
    printf("%d %d\n", rNum[C2], tSum[C2]);

    return 0;
}

Dijkstra+DFS

来自晴神《算法笔记》的解法

A1030 保存最短路径+双尺度边权

算法选型:节点个数多于200,边长为城市距离无负权值,优选队列优化的Dijkstra,采用链式前向星实现

思路解析:题意为两个目标:
①找到最短路径中代价最小的那条。双尺度问题在A1003中详细解析过,按第一尺度找最短路径,按第二尺度记录更新最短路径的代价最小值。本题的第二尺度仅仅是将点权修改为边权。
②记录最短路径并打印。如果u节点的出边e使u→v松弛达到最短路径,就在v处保存前向的该节点,如果最短路径相同,就更新使第二尺度更小的前向节点u,如此从目标节点回溯到源节点即可找到该最短路径,倒序打印即可。


٩(*´◒`*)۶ Q:path保存的是倒序结果,那怎么正向打印节点呢?

<-biubiu-⊂(`ω´∩) A:栈或者递归咯!递归写起来简单,我们将递归置于操作之前,就能实现倒序操作的效果。递归边界是到达源节点,此时可以用path[s]=-1来判断。注意我们判断的是前序节点值path[i],输出的是本层节点i


#include <iostream>
#include <queue>
#include <vector>
#include <bits/stdc++.h>
using namespace std;

const int maxV = 500;
const int maxE = 1e5;
const int inf = 1e6;
int dis[maxV], done[maxV], path[maxV], inq[maxV], cost[maxV];
int p = 0;

int Vertex[maxV];
struct Edge {
    int next;
    int to;
    int w;
    int c;
} e[maxE];

void addEdge(int from, int to, int w, int c) {
    e[p].to = to;
    e[p].w = w;
    e[p].c = c;
    e[p].next = Vertex[from];
    Vertex[from] = p++;
}

struct Cmp {
    bool operator()(int a, int b) {
        return dis[a] > dis[b];
    }
};

priority_queue<int, vector<int>, Cmp> q;

void dijkstra(int s) {
    dis[s] = 0;
    cost[s] = 0;
    q.push(s);

    while (!q.empty()) {
        int from = q.top();
        q.pop();
        done[from] = 1;
        inq[from] = 0;
        for (int i = Vertex[from]; ~i; i = e[i].next) {
            int to = e[i].to;
            int w = e[i].w;
            int c = e[i].c;
            if (done[to]) continue;
            if (dis[from] + w < dis[to]) {
                dis[to] = dis[from] + w;
                cost[to] = cost[from] + c;
                path[to] = from;
                if (!inq[to]) {
                    q.push(to);
                    inq[to] = 1;
                }
            }
            else if (dis[from] + w == dis[to]) {
                if (cost[from] + c < cost[to]) {
                    cost[to] = cost[from] + c;
                    path[to] = from;
                }
            }
        }
    }
}

void printPath(int d) {
    int u = path[d];
    if (~u) {
        printPath(u);
        printf(" %d", d);
    }
    else {
        printf("%d", d);
        return;
    }
}

int main() {
    int N, M, S, D;
    int C1, C2, w, c;
    scanf("%d %d %d %d", &N, &M, &S, &D);

    fill(dis, dis + N, inf);
    memset(Vertex, -1, sizeof(Vertex));
    memset(path, -1, sizeof(path));
    memset(done, 0, sizeof(done));
    memset(cost, 0, sizeof(cost));
    memset(inq, 0, sizeof(inq));

    while (M--) {
        scanf("%d %d %d %d", &C1, &C2, &w, &c);
        addEdge(C1, C2, w, c);
        addEdge(C2, C1, w, c);
    }

    dijkstra(S);
    printPath(D);
    printf(" %d %d", dis[D], cost[D]);
    return 0;
}

发表回复

您的电子邮箱地址不会被公开。 必填项已用*标注

©2018-2024 Howell版权所有 备案号:冀ICP备19000576号