【笔记】图论

基本

存图

我们一般用 vector 来存。有特殊需求的时候也可能用邻接表,比如写网络流时为了快速找反边用。

邻接表:

int h[N], e[M], w[M], ne[M], idx;
void init() {
    memset(h, -1, sizeof h); // 一定记得设成 -1 !
    return;
}
void add(int u, int v, int wt) {
    e[idx] = v, w[idx] = wt, ne[idx] = h[u];
    h[u] = idx++;
    return;
}

vector:

using pii = pair<int, int>;
vector<pii> g[N];
void add(int u, int v, int w) {
    g[u].push_back({v, w}); // 如果是无向图加两边就可以
    return;
}

最短路

主要有全源最短路,单源最短路这两种。

Floyd

用来求全源最短路,本质上是 DP,核心思想是枚举每一个中间点,判断以这个点为中转站是否能让某两点 \(u, v\) 间的距离比原来更小。转移方程如下:

\[d_{(u, v)} = \min({d_{(u, v)}, d_{(u, k)} + d_{(k, v)}}) \]

将 \(k\) 迭代取遍 \(1\sim n\) 即可。此处有一点很重要,当钦定中转点 \(k\) 时, \(d_{(u, v)}\) 表示的是以某个小于 \(k\) 的点为中转点时,\(u, v\) 间的最小距离(因为此时大于等于 \(k\) 的最短距离还没有更新)。因此注意可能会利用这个 “单调性” 来做题。

还有一点,如果题目对于最短路的要求不是仅与一个变量有关,那么 Floyd 可能是错的。举例,若定义两点间距离是两点间经过的边权和加上点权和,那么分别求出边权和与点权和最小值相加就是错的,因为它们不一定同时取等。

时间复杂度 \(O(n^3)\)。

#include <bits/stdc++.h>
using namespace std;
const int N = 505;
int n, g[N][N];
int main() {
    scanf("%d", &n);
    memset(g, 0x3f, sizeof g);
    for (int i = 1; i <= n; i++)
        for(int j = 1; j <= n; j++)
            scanf("%d", &g[i][j]);
    for (int k = 1; k <= n; k++)
        for (int i = 1; i <= n; i++)
            for (int j = 1; j <= n; j++)
                g[i][j] = min(g[i][j], g[i][k] + g[k][j]);
    for (int i = 1; i <= n; i++) {
        for (int j = 1; j <= n; j++)
            printf("%d ", g[i][j]);
        printf("\n");
    }
    return 0;
}

Dijkstra

单源最短路。

实质是贪心算法,在一个边权均为正的图中,记录每个点目前距离源点的距离为 \(d_u\),向这个集合中加入源点,之后每次都从这个集合中选取最小值,并删除,用它去松弛更新其他点的 \(d\) 值,如果发生更新就将新的点放入集合。

显然如果发生更新,一定是由这些点引起(但这些点并不一定引起更新),所以只要一个点被另一个点加入集合,它的最短距离就已经被确定,因此等到集合为空,\(d\) 中对应的就是所有点最短距离。用堆维护这个集合。

注意,Dijkstra 不能求含负权边的最短路或者最长路,因为这会使得它的贪心出错。

时间复杂度 \(O((n + m) \log n)\)。

#include <bits/stdc++.h>
using namespace std;
using pii = pair<int, int>;
const int N = 1e5 + 5, nex = 0x3f3f3f3f;
int n, m, s, q, dis[N];
bool st[N];
vector<pii> g[N];
void init(){
    memset(dis, 0x3f, sizeof dis), memset(st, false, sizeof st);
    return;
}
inline void add(int u, int v, int w){
    g[u].push_back({v, w});
    return;
}
void dijkstra(int s){
    priority_queue<pii, vector<pii>, greater<pii> > q;
    q.push({dis[s] = 0, s});
    while(q.size()){
        auto [d, u] = q.top();
        q.pop();
        if(st[u])
            continue;
        st[u] = true;
        // 它在队里,现在被弹出用来更新别人,因此已经是最短,不再需要入队
        for(auto [v, w] : g[u]){
            int nd = dis[u] + w;
            if(nd < dis[v]){
                dis[v] = nd;
                q.push({nd, v});
            }
        }
    }
    return;
}
int main(){
    init();
    scanf("%d%d%d%d", &n, &m, &s, &q);
    for(int i = 1; i <= m; i++){
        int u, v, w;
        scanf("%d%d%d", &u, &v, &w);
        add(u, v, w);
    }
    dijkstra(s);
    while(q--){
        int u;
        scanf("%d", &u);
        if(dis[u] == nex)
            printf("IMPOSSIBLE\n");
        else    
            printf("%d\n", dis[u]);        
    }
    return 0;
}

SPFA

本质是 BFS。用一个队列存储点及其目前的距离,不断用队头节点松弛更新其它点,直到队空(不再更新,即代表所有节点已经是最短)。

它还可以判断负环:当一个点的最短路中含超过 \(n\) 条边,说明存在。注意判断负环要放在最前面,不然可能会被 in[v] == true 跳过而漏判。

还可以做到求最长路:把松弛那里符号反过来就行。不过 dijkstra 就不行,因为不满足贪心的条件。

时间复杂度:平均 \(O(km)\) (\(k\) 为常数),最坏 \(O(nm)\)(网格图可卡)。

#include <bits/stdc++.h>
using namespace std;
using pii = pair<int, int>;
const int N = 1e4 + 5;
const int nex = 0x3f3f3f3f, inf = INT_MAX;
int n, m, s, d[N], in[N], cnt[N];
vector<pii> g[N];
void init() {
    memset(d, 0x3f, sizeof d);
    memset(in, false, sizeof in);
    memset(cnt, 0, sizeof cnt);
    return;
}
void add(int u, int v, int w) {
    g[u].push_back({v, w});
    return;
}
bool spfa(int s) {
    queue<int> q;
    q.push(s);
    d[s] = 0, in[s] = true;
    while (q.size()) {
        int u = q.front();
        q.pop();
        in[u] = false;
        for (auto [v, w] : g[u]) {
            int nd = d[u] + w;
            if (nd < d[v]) {
                d[v] = nd;
                if (in[v])
                    continue;
                q.push(v);
                in[v] = true, cnt[v]++;
                if (cnt[v] >= n)
                    return false;
            }
        }
    }
    return true;
}
int main() {
    init();
    scanf("%d%d%d", &n, &m, &s);
    for (int i = 1; i <= m; i++) {
        int u, v, w;
        scanf("%d%d%d", &u, &v, &w);
        add(u, v, w);
    }
    bool res = spfa(s);
    if(!res) {
        printf("Negative Circle\n");
        return 0;
    }
    for (int i = 1; i <= n; i++)
        printf("%d ", d[i] == nex ? inf : d[i]);
    return 0;
}

最小生成树

Prim

朴素版 \(O(n^2)\),堆优化 \(O(m\log n)\) 的一个算法,理论上稠密图比 Kruskal 表现要好,不过我觉得没用。

Kruskal

依旧是贪心思想,将边从小到大排序,依次加入,同时用并查集维护点之间的连通性,如果这条边两端点已经在一个连通块中,说明这条边的加入会生成一个环,不满足最小生成树要求,是没有必要的,就跳过。加入满 \(n - 1\) 条边就可以退出了,如果遍历了 \(m\) 条边仍没有加入足够的边,说明图不连通。

注意以下两点,第一:并查集要初始化 \(f_u = u\)。而且这个初始化要放在输入 \(n\) 之后。第二:一定要在 Kruskal 之前先对边排序!

时间复杂度 \(O(m\log m)\),稀疏图表现很不错。

#include <bits/stdc++.h>
using namespace std;
const int N = 5e3 + 5, M = 2e5 + 5;
int n, m, fa[N];
struct edge {
    int u, v, w;
}e[M];
bool cmp(edge a, edge b) {
    return a.w < b.w;
}
void init() {
    for (int i = 1; i <= n; i++)
        fa[i] = i;
    return;
}
int find(int u) {
    if(fa[u] == u)
        return u;
    return fa[u] = find(fa[u]);
}
int kruskal() {
    int sum = 0, cnt = 0;
    for (int i = 1; i <= m; i++) {
        int u = e[i].u, v = e[i].v, w = e[i].w;
        int fu = find(u), fv = find(v);
        if(fu == fv)
            continue;
        cnt++;
        sum += w;
        fa[fu] = fv;
        if(cnt == n - 1)
            return sum;
    }
    return -1;
}
int main() {
    scanf("%d%d", &n, &m);
    init();
    for (int i = 1; i <= m; i++) {
        int u, v, w;
        scanf("%d%d%d", &u, &v, &w);
        e[i] = {u, v, w};
    }
    sort(e + 1, e + 1 + m, cmp);
    int res = kruskal();
    res == -1 ? printf("orz\n") : printf("%d\n", res);
    return 0;
}

Borůvka

听有的人说如果边的特性比较多,可以用这个。待补。

欧拉路

欧拉路/回路的判断

假设我们已经找到了一条欧拉路,观察路径上的每一个点,不难发现除了起点和终点以外,从别的点“进入”它,以及从它“出去”的次数是相同的。也即除起点和终点以外,所有点的入度等于出度,起点的出度比入度多 \(1\),终点的出度比入度少 \(1\)。这就是判断欧拉路存在的充分必要条件(同时图也必须是连通的)。

特别地,如果所有点入度与出度都相同,那么图存在欧拉回路。

Hierholzer

这个算法的核心逻辑是,从起点开始进行 dfs,直到没有边可以走为止;这时如果还有边没走,那么一定存在某个点在这条路径上,且它在一个环里。(欧拉路相当于一条主链加上若干个环)每次把这个环合并到欧拉路里即可。用 dfs 可能会爆栈,迭代版可以解决这个问题。

看到“每条边恰好经过一次”“每个物品恰好都用一次”“首尾相接”类似的条件,可以试试往这边想。

注意对于无向图有向图欧拉路存在性的判断:

  • 有向图:忽略边的方向,图弱联通,入度不等于出度,且出入度差 \(1\) 的点为 \(0\) 或 \(2\) 个。
  • 无向图:图联通,且度数为奇数的点仅有 \(0\) 或 \(2\) 个。

时间复杂度 \(O(n+m)\)。

有向图:

#include <bits/stdc++.h>
using namespace std;
const int N = 1e5 + 5;
int n, m, s, in[N], ot[N], cur[N];
vector<int> g[N];
vector<int> stk, path;
int main() {
    scanf("%d%d", &n, &m);
    for (int i = 1; i <= m; i++) {
        int u, v;
        scanf("%d%d", &u, &v);
        g[u].push_back(v);
        in[v]++, ot[u]++;
    }
    for (int i = 1; i <= n; i++)
        sort(g[i].begin(), g[i].end());          

    int c1 = 0, c2 = 0;
    s = -1;
    for (int i = 1; i <= n; i++) {
        if (ot[i] == in[i])
            continue;
        if (ot[i] - in[i] == 1)
            c1++, s = i;                         
        else if (in[i] - ot[i] == 1)
            c2++;                                 
        else {
            printf("No\n");
            return 0;
        }
    }
    if (c1 == 0 && c2 == 0) {                 
        for (int i = 1; i <= n; i++)
            if (ot[i] > 0) {
                s = i;
                break;
            }
    } else if (!(c1 == 1 && c2 == 1)) {
        printf("No\n");
        return 0;
    }
    if (s == -1) {
        printf("No\n");
        return 0;
    }

    stk.push_back(s);
    while (!stk.empty()) {
        int u = stk.back();
        if (cur[u] < (int)g[u].size()) {
            stk.push_back(g[u][cur[u]++]);        
        } else {
            path.push_back(u);                    
            stk.pop_back();
        }
    }

    if ((int)path.size() != m + 1) { 
        printf("No\n");
        return 0;
    }
    for (int i = (int)path.size() - 1; i >= 0; i--) {
        printf("%d", path[i]);
        if (i) printf(" ");
    }
    printf("\n");
    return 0;
}

无向图:

#include <bits/stdc++.h>
using namespace std;
using pii = pair<int, int>;
const int N = 1e5 + 5;
int n, src, c, d[60], fa[60], cur[60];
bool used[N], app[60];
vector<pii> g[60];
vector<int> p;
int id(char ch) {
    if ('A' <= ch && ch <= 'Z')
        return ch - 'A';
    return ch - 'a' + 26;
}
char ch(int x) {
    if (x < 26)
        return 'A' + x;
    return 'a' + x - 26;
}
void init() {
    memset(app, false, sizeof app);
    memset(used, false, sizeof used);
    for (int i = 0; i <= 52; i++)
        fa[i] = i;
    return;
}
int find(int u) {
    if(fa[u] == u)
        return u;
    return fa[u] = find(fa[u]);
}
void dfs(int u) {
    while (cur[u] < (int)g[u].size()) {
        int v = g[u][cur[u]].first, idx = g[u][cur[u]].second;
        cur[u]++;
        if (used[idx])
            continue;
        used[idx] = true;
        dfs(v);
    }
    p.push_back(u);
    return;
}
int main() {
    scanf("%d", &n);
    init();
    for (int i = 1; i <= n; i++) {
        char buf[5];
        scanf("%s", buf + 1);
        int u = id(buf[1]), v = id(buf[2]);
        d[u]++, d[v]++;
        g[u].push_back({v, i}), g[v].push_back({u, i});
        app[u] = true, app[v] = true;
        int fu = find(u), fv = find(v);
        if (fu != fv)
            fa[fu] = fv;
    }
    for (int i = 0; i < 52; i++)
        sort(g[i].begin(), g[i].end());

    int b = -1, odd = 0, fstapp = -1, fstodd = -1;
    for (int i = 0; i < 52; i++) {
        if (!app[i])
            continue;
        if (b == -1)
            b = find(i);
        if (find(i) != b) {
            printf("No Solution\n");
            return 0;
        }
        if (fstapp == -1)
            fstapp = i;
        if (d[i] & 1) {
            odd++;
            if (fstodd == -1)
                fstodd = i;
        }
    }
    if (odd == 0)
        src = fstapp;
    else if (odd == 2)
        src = fstodd;
    else {
        printf("No Solution\n");
        return 0;
    }

    dfs(src);

    if ((int)p.size() != n + 1) {
        printf("No Solution\n");
        return 0;
    }
    for (int i = (int)p.size() - 1; i >= 0; i--)
        putchar(ch(p[i]));
    putchar('\n');
    return 0;
}

连通性问题 (Tarjan)

以下定义:

  • \(\text{dfn}_u\) 表示对图 \(G\) dfs 后 \(u\) 点被访问到的次序,即时间戳。
  • T 边:指向一个未被访问到的新节点的边。
  • B 边:指向一个已经被访问过的且在栈中的点的边。
  • F 边:指向这条边始点的后代的边。
  • C 边:指向其它子树节点的边。

强连通分量 (SCC)

若某图的一个子图中所有点都可以互相到达,则称它是这个图的一个强连通子图。如果向这个子图中再加入任何边和点都会使它不再强连通,则称这个子图是原图的一个强连通分量。

也就是说,强连通分量 = 有向图中的极大强连通子图。

我们定义 \(\text{low}_{u}\) 为节点 \(u\) 走过若干 T 边和至多一条 B 边可以到达的 \(\text{dfn}\) 值最小的节点的 \(\text{dfn}\) 值。

任选一个节点开始进行 dfs,若从 \(u\) 走到 \(\text{dfn}\) 值为空的节点(即未被搜索到过的节点),就更新其 \(\text{dfn}\) 值,将 \(\text{low}\) 值初始化为其 \(\text{dfn}\) 值,并把它入栈,继续搜索。若走到了 \(\text{dfn}\) 值不为空的节点,且它在栈中,说明 \(u\) 通过仅一条 B 边走到了它的祖先,这时候应维护 \(\text{low}_u = \min(\text{low}_u, \text{dfn}_v)\),注意这里应该用 \(\text{dfn}_v\) 来更新而不是 \(\text{low}_v\)。这是因为因为我们定义 \(\text{low}\) 值意为仅经过最多一条 B 边能到达的最高的节点,而 \((u, v)\) 本身就是一条 B 边,若用 \(\text{low}_v\) 来更新可能存在走两条及以上 B 边的可能,与定义不符。

对于 \(u\),处理完所有能走到的点之后,如果它的 \(\text{low}_u\) 与 \(\text{dfn}_u\) 仍然相等,说明它就是它所在的强连通分量中最高的那个点。由于栈中的节点 \(\text{dfn}\) 值单调递增,因此所有比 \(u\) 晚入栈的节点都是它的后代,也在这个强连通分量中。

所以我们弹栈,直到将 \(u\) 弹出,这些点全部归入一个强连通分量中。这样就求完了。

还有一个技巧:缩点。将一个强连通分量中的所有点看作一个点,忽略分量内部的连边,原图就会变成一个 DAG,可以进行拓扑 dp 等一系列操作。

时间复杂度 \(O(n + m)\)。

以下代码完成了求 SCC 及其元素。

#include <bits/stdc++.h>
using namespace std;
const int N = 1e4 + 5;
int n, m, num, cnt, top;
bool in[N];
int dfn[N], low[N], stk[N];
vector<int> g[N], p[N];
void scc(int u) {
    dfn[u] = low[u] = ++num;
    stk[++top] = u, in[u] = true;
    for (int v : g[u]) {
        if (!dfn[v]) {
            scc(v);
            low[u] = min(low[u], low[v]);
        } else if (in[v])
            low[u] = min(low[u], dfn[v]);
    }
    if (dfn[u] == low[u]){
        cnt++;
        int t;
        do {
            t = stk[top--];
            in[t] = false;
            p[cnt].push_back(t);
        } while(t != u);
    }
}
int main() {
    scanf("%d%d", &n, &m);
    while (m--) {
        int u, v;
        scanf("%d%d", &u, &v);
        if (u != v)
            g[u].push_back(v);
    }
    for (int i = 1; i <= n; i++)
        if (!dfn[i])
            scc(i);
    for (int i = 1; i <= cnt; i++) {
        printf("SCC %d : %d elements : \n", i, p[i].size());
        sort(p[i].begin(), p[i].end());
        for (int u : p[i])
            printf("%d ", u);
        printf("\n");
    }
    return 0;
}

割点 (CV)

posted @ 2026-09-21 22:31  Hyazinth  阅读(7)  评论(0)    收藏  举报