图论学习笔记


更新中

一些前言

打算从现在开始写一点有关图论的知识,用来给自己没事复习看看,所写内容也没什么顺序,感觉自己容易忘就会贴上去;
我自己也是学算法没有多久的初学ACMer,如果有错误欢迎指正。

负环

判断一个图是否存在负环一般用spfa()算法,只需要判断任意一个点是否被遍历超过N次,如果超过N次,则存在负环,在数据量过大时,还可以运用一些剪枝方法,比如在循环次数大于10000次时,就可以认定存在负环。

s p f a ( ) 算 法 : spfa()算法: spfa():

bool spfa()
{
    memset(st, false, sizeof st);
    memset(cnt, 0, sizeof cnt);
    //memset(dist, 0, sizeof dist);
    int tt = 0, hh = 0; 

    for(int i = 1; i <= n; i ++)
    {
        q[tt ++] = i;
        st[i] = true;
    }

    while(hh != tt)
    {
        int t = q[hh ++];
        if(hh == N) hh = 0;
        st[t] = false;

        for(int i = h[t]; ~i; i = ne[i])
        {
            int j = e[i];
            if(dist[j] > dist[t] + w[i])
            {
                dist[j] = dist[t] + w[i];
                cnt[j] = cnt[t] + 1;
                if(cnt[t] >= n) return true;
                if(!st[j])
                {
                    q[tt ++] = j;
                    if(tt == N) tt = 0;
                    st[j] = true;
                }
            }
        }
    }
    return false;
}

作者:Rhymes
链接:https://www.acwing.com/activity/content/code/content/1743650/
来源:AcWing
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

spfa()的优化

1. 在 s p f a ( ) 的 初 始 化 中 , 可 以 省 略 对 d i s t [ ] 数 组 的 初 始 化 。 1.在spfa()的初始化中,可以省略对dist[ ]数组的初始化。 1.spfa()dist[]
原 因 是 当 有 负 环 存 在 时 候 , 无 论 更 新 多 少 次 , d i s t [ ] 总 是 会 向 着 更 小 的 数 更 新 一 直 到 负 无 穷 。 原因是当有负环存在时候,无论更新多少次,dist[ ]总是会向着更小的数更新一直到负无穷。 ,dist[]
2. 当 循 环 次 数 大 于 1 e 4 的 时 候 , 即 可 以 判 断 已 经 存 在 了 负 环 2.当循环次数大于1e4的时候,即可以判断已经存在了负环 2.1e4
3. 可 以 将 队 列 换 成 栈 , 可 以 在 一 定 程 度 上 极 大 的 提 升 寻 找 负 环 的 速 度 3.可以将队列换成栈,可以在一定程度上极大的提升寻找负环的速度 3.
理 由 是 在 判 断 负 环 时 候 , 总 是 在 用 刚 遍 历 过 的 点 继 续 遍 历 下 一 个 点 , 所 以 在 运 用 栈 可 以 有 利 于 更 快 的 找 到 一 个 环 理由是在判断负环时候,总是在用刚遍历过的点继续遍历下一个点,所以在运用栈可以有利于更快的找到一个环

spfa()的应用

题 目 中 一 般 不 会 让 直 接 判 断 负 环 , 而 是 通 过 对 题 目 等 价 变 形 得 到 一 个 新 的 边 权 至 来 判 断 。 题目中一般不会让直接判断负环,而是通过对题目等价变形得到一个新的边权至来判断。
比 如 在 判 断 一 个 图 中 的 边 权 值 之 ( f [ i ] ) 和 比 上 各 点 权 值 ( t [ i ] ) 的 最 大 值 时 。 比如在判断一个图中的边权值之(f[i])和比上各点权值(t[i])的最大值时。 (f[i])(t[i])
可 以 通 过 二 分 求 这 个 比 值 的 最 大 值 可以通过二分求这个比值的最大值

∑ 1 n f ( i ) t ( i ) > = m i d ⇒ ∑ 1 n f ( i ) − ∑ 1 n t ( i ) ∗ m i d > = 0 \sum_1^n\frac{f(i)}{t(i)} >= mid \Rightarrow \sum_1^nf(i) - \sum_1^nt(i) * mid>= 0 1nt(i)f(i)>=mid1nf(i)1nt(i)mid>=0

即 只 需 要 判 断 边 权 为 f ( i ) − t ( i ) ∗ m 所 构 成 的 图 是 否 存 在 负 环 。 即只需要判断边权为f(i) - t(i) * m所构成的图是否存在负环。 f(i)t(i)m
如 果 存 在 s p f a ( ) 返 回 t r u e , m i d 可 以 为 更 大 的 值 , 否 则 返 回 f a l s e 。 如果存在 spfa()返回true, mid可以为更大的值,否则返回false。 spfa()true,mid,false

矩阵乘法

一次矩阵乘法相当于是使用了一次 F l o y d Floyd Floyd ,加上快速幂即可求出 ∣ A ∣ k |A|^k Ak, 表示在图上走 k k k 部的所有可行方案。

#include <cstdlib>
#include <iostream>
#include <algorithm>
#include <cstring>
using namespace std;
typedef long long ll;
typedef pair<int, int> PII;
int n, m, t, ans;

typedef struct Mat {
    int g[35][35];

    Mat() {
        memset(g, 0, sizeof g);
    }

    Mat operator * (const Mat b) const { //矩阵乘法
        Mat c;
        for(int i = 0; i <= n; i ++)
            for(int j = 0; j <= n; j ++)
                for(int k = 0; k <= n; k ++)
                    c.g[i][j] = (c.g[i][j] + g[i][k] * b.g[k][j]) % 2017;
        return c;
    }
}M;
M I, E, ansm;

M pow(M a,int p) { //快速幂
    if(p == 0) return I;
    
    M temp = pow(a, p / 2);
    if(p % 2) return temp * temp * a;
    else return temp * temp;
}

int main() {
    ios::sync_with_stdio; cin.tie(0); cout.tie(0);
#ifndef ONLINE_JUDGE
    freopen("D:/Cpp/program/Test.in", "r", stdin);
    freopen("D:/Cpp/program/Test.out", "w", stdout);
#endif
    cin >> n >> m;
    for(int i = 1; i <= m; i ++) {
        int a, b;
        cin >> a >> b;
        E.g[a][b] = E.g[b][a] = 1;
    }

    for(int i = 0; i <= n; i ++) I.g[i][i] = E.g[i][i] = 1, E.g[i][0] = 1; //连接停下来的边和自爆的边

    cin >> t;
    ansm = pow(E, t);
    for(int i = 0; i <= n; i ++) ans += ansm.g[1][i];
    cout << ans % 2017 << '\n';
    return 0;
}

求最小环

D i j k s t r a Dijkstra Dijkstra 求最小环

d [ i ] [ j ] d[i][j] d[i][j] 表示点 i i i 到点 j j j 的最短距离,则 d [ i ] [ i ] d[i][i] d[i][i] 就表示自到自己的最短距离,在预处理时不初始化 d i s t dist dist 数组中的距离为0, 而是直接将 0 0 0 放入优先队列中来更新, 最好如果再次走到自己就更新了自己到自己的距离为最小环权值,否则为 I N F INF INF

#include <cstdlib>
#include <iostream>
#include <algorithm>
#include <cstring>
#include <queue>
using namespace std;
typedef long long ll;
typedef pair<int, int> PII;
const int N = 1e4 + 10, M = N * 2;
int n, m, sum;
int e[M], ne[M], h[N], w[M], idx;
int dist[N][N];
bool st[N];

void add(int a, int b, int c) {
    e[idx] = b, ne[idx] = h[a], w[idx] = c, h[a] = idx ++;
}

void dijkstra(int s) {
    priority_queue<PII, vector<PII>, greater<PII>> hea;
    memset(st, 0, sizeof st);
    hea.push({0, s});

    while(hea.size()) {
        auto t = hea.top();
        hea.pop();
        int ver = t.second, dis = t.first;

        if(st[ver]) continue;
        st[ver] = true;

        for(int i = h[ver]; ~i; i = ne[i]) {
            int j = e[i];
            if(dist[s][j] > dis + w[i]) {
                dist[s][j] = dis + w[i];
                hea.push({dist[s][j], j});
            }
        }
    }
}

int main() {
    ios::sync_with_stdio(false); 
    cin.tie(0); cout.tie(0);
#ifndef ONLINE_JUDGE
    freopen("D:/Cpp/program/Test.in", "r", stdin);
    freopen("D:/Cpp/program/Test.out", "w", stdout);
#endif
    cin >> n >> m;
    memset(h, -1, sizeof h);
    while(m --) {
        int a, b, k;
        cin >> a >> b >> k;
        add(a, b, k);
        }
    

    memset(dist, 0x3f, sizeof dist);
    for(int i = 1; i <= n; i ++) {
        dijkstra(i);
        //cout << dist[i][i] << '\n';
    }
	int ans = 0x3f3f3f3f;
    for(int i = 1; i <= n; i ++) {
        //cout << dist[i][i] << '\n';
        if(dist[i][i] != 0x3f3f3f3f) {
       		ans = min(ans, dist[i][i]);
        }
    }
	cout << ans << '\n';
    return 0;
}

搜索

求子树大小及到根节点距离

不记录本身大小, 根节点到自己距离为0

void dfs(int u, int fa) {
    for(int i = h[u]; ~i; i = ne[i]) {
        int j = e[i];
        if(j == fa) continue;
        a[j].dist = a[u].dist + 1;
        dfs(j, u);
        a[u].sz += (a[j].sz + 1);
    }
}

求叶子节点个数

记录一个每一层的一个 s u m sum sum, 如果 s u m sum sum 0 0 0 表示为叶子节点 返回 1 1 1, 否则返回 0 0 0 即可。

int dfs(int u, int fa) {
    int sum = 0;
    for(int i = h[u]; ~i; i = ne[i]) {
        int j = e[i];
        if(j == fa) continue;
        sum += dfs(j, u);
    }
    if(!sum) return 1;
    else {
        ans += sum; //叶子数量
        return 0;
    }
}

差分约束

在求最大值时,应该求最短路,判断有无负环。
如果存在负环,则无解,说明边可以无限小
a ≤ b + c a \le b + c ab+c, 建立一条从b到a长度为c的边,求所有约束条件中的最小值。
比如:有一下几个条件
a ≤ 5 a \le 5 a5 a ≤ 7 a \le 7 a7 a ≤ 3 a \le 3 a3 此时a的值就是3, 求的是所有条件中值最小的那个,因此为最短路

在求最小值时,应该求最长路,判断有无正环。
如果存在正环,则无解,说明边可以无限大
a ≥ b + c a \ge b + c ab+c 建立一条从b到a长度为c的边,求所有约束条件中的最大值。
同上

在表示 a = = b a == b a==b 的时候,只需要添加 a ≤ b a \le b ab a ≥ b a \ge b ab两个条件即可;
a d d ( a , b , 0 ) , a d d ( b , a , 0 ) ; add(a, b, 0), add(b, a, 0); add(a,b,0),add(b,a,0);

例 题 : 例题: :
A c W i n g 1169. 糖 果 AcWing1169.糖果 AcWing1169.

幼儿园里有 N N N 个小朋友,老师现在想要给这些小朋友们分配糖果,要求每个小朋友都要分到糖果。

但是小朋友们也有嫉妒心,总是会提出一些要求,比如小明不希望小红分到的糖果比他的多,于是在分配糖果的时候, 老师需要满足小朋友们的 K K K 个要求。

幼儿园的糖果总是有限的,老师想知道他至少需要准备多少个糖果,才能使得每个小朋友都能够分到糖果,并且满足小朋友们所有的要求。

输入格式
输入的第一行是两个整数 N , K N,K N,K

接下来 K 行,表示分配糖果时需要满足的关系,每行 3 个数字 X , A , B X,A,B X,A,B

如果 X = 1 X=1 X=1.表示第 A A A 个小朋友分到的糖果必须和第 B B B 个小朋友分到的糖果一样多。
如果 X = 2 X=2 X=2,表示第 A A A 个小朋友分到的糖果必须少于第 B B B 个小朋友分到的糖果。
如果 X = 3 X=3 X=3,表示第 A A A 个小朋友分到的糖果必须不少于第 B B B 个小朋友分到的糖果。
如果 X = 4 X=4 X=4,表示第 A A A 个小朋友分到的糖果必须多于第 B B B 个小朋友分到的糖果。
如果 X = 5 X=5 X=5,表示第 A A A 个小朋友分到的糖果必须不多于第 B B B 个小朋友分到的糖果。
小朋友编号从 1 1 1 N N N

输出格式
输出一行,表示老师至少需要准备的糖果数,如果不能满足小朋友们的所有要求,就输出 −1

数 据 范 围 数据范围
1 ≤ N < 105 , 1≤N<105, 1N<105,
1 ≤ K ≤ 105 , 1≤K≤105, 1K105,
1 ≤ X ≤ 5 , 1≤X≤5, 1X5,
1 ≤ A , B ≤ N 1≤A,B≤N 1A,BN
输 入 样 例 : 输入样例:

5 7
1 1 2
2 3 2
4 4 1
3 4 5
5 4 5
2 3 5
4 5 1

输 出 样 例 : 输出样例:

11

思 路 : 思路: : 因为是求最小值,所以我们应该求所有条件中的最长路。

  1. x = = 1 x == 1 x==1时: a = b a = b a=b < = = > <==> <==> a ≥ b a \ge b ab b ≥ a b \ge a ba
  2. x = = 2 x == 2 x==2时: a < b a \lt b a<b < = = > <==> <==> b ≥ a − 1 b \ge a - 1 ba1 表示从 a 到 a 长度为 -1 的边
  3. x = = 3 x == 3 x==3时: a ≥ b a \ge b ab < = = > <==> <==> a ≥ b a \ge b ab 表示从 b 到 a 长度为 0 的边
  4. x = = 4 x == 4 x==4时: a > b a \gt b a>b < = = > <==> <==> a ≥ b + 1 a \ge b + 1 ab+1 表示从 b到 a长度为 1的边
  5. x = = 5 x == 5 x==5时: a ≤ b a \le b ab < = = > <==> <==> b ≥ a b \ge a ba 表示从 a 到 b 长度为 0的边

代码:

#include <iostream>
#include <algorithm>
#include <cstring>

using namespace std;
const int N = 1e5 + 10, M = 3e5 + 10;

int n, m;
int e[M], ne[M], h[N], w[M], idx;
int q[N], cnt[N];
long long dist[N];
bool st[N];

void add(int a, int b, int c)
{
    e[idx] = b, ne[idx] = h[a], w[idx] = c, h[a] = idx ++;
}

bool spfa()
{
    memset(dist, -0x3f, sizeof dist);
    int tt = 1;
    q[0] = 0;
    dist[0] = 0;
    st[0] = true;
    
    while(tt != 0)
    {
        int t = q[-- tt];
        st[t] = false;
        
        for(int i = h[t]; ~i; i = ne[i])
        {
            int j = e[i];
            //cout << t<< " " << j << endl;
            if(dist[j] < dist[t] + w[i])
            {
                dist[j] = dist[t] + w[i];
                cnt[j] = cnt[t] + 1;
                if(cnt[j] > n) return false;
                if(!st[j])
                {
                    q[tt ++] = j;
                    st[j] = true;
                }
            }
        }
    }
    
    return true;
}

int main()
{
    cin >> n >> m;
    memset(h, -1, sizeof h);
    while(m --)
    {
        int x, a, b;
        cin >> x >> a >> b;
        if(x == 1) add(a, b, 0), add(b, a, 0);
        else if(x == 2) add(a, b, 1);
        else if(x == 3) add(b, a, 0);
        else if(x == 4) add(b, a, 1);
        else add(a, b, 0);
    }
    
    for(int i = 1; i <= n; i ++) add(0, i, 1);
    
    if(!spfa()) puts("-1");
    else
    {
        long long res = 0;
        for(int i = 1; i <= n; i ++) res += dist[i];
        cout << res << endl;
    }
    
    return 0;
}

Tarjan 算法

Tarjan 发明了很多算法结构。不少他发明的算法都以他的名字命名,以至于有时会让人混淆几种不同的算法。比如求各种连通分量的 Tarjan 算法,求 LCA(Lowest Common Ancestor,最近公共祖先)的 Tarjan 算法。并查集、Splay、Toptree 也是 Tarjan 发明的。

Tarjan 算法求强连通分量


在任意一个连通图中,一定存在一个点 d f n [ u ] = = l o w [ u ] dfn[u] == low[u] dfn[u]==low[u],并且该点一定是在访问这个强连通图时,第一个被访问到的点。
当有以上条件成立时,既可以开始从 u u u点生成一个强连通分量

代码

void tarjan(int u)
{
    dfn[u] = low[u] = ++ timestamp;
    stk[ ++ top] = u, in_stk[u] = true;
    for (int i = h[u]; i != -1; i = ne[i])
    {
        int j = e[i];
        if (!dfn[j])
        {
            tarjan(j);
            low[u] = min(low[u], low[j]);
        }
        else if (in_stk[j]) low[u] = min(low[u], dfn[j]);
    }

    if (dfn[u] == low[u])
    {
        ++ scc_cnt;
        int y;
        do {
            y = stk[top -- ];
            in_stk[y] = false;
            id[y] = scc_cnt;
            Size[scc_cnt] ++ ;
        } while (y != u);
    }
}

作者:Rhymes

例题
A c W i n g 367 学 校 网 络 : AcWing 367 学校网络: AcWing367:
一些学校连接在一个计算机网络上,学校之间存在软件支援协议,每个学校都有它应支援的学校名单(学校 A 支援学校 B,并不表示学校 B 一定要支援学校 A)。

当某校获得一个新软件时,无论是直接获得还是通过网络获得,该校都应立即将这个软件通过网络传送给它应支援的学校。

因此,一个新软件若想让所有学校都能使用,只需将其提供给一些学校即可。

现在请问最少需要将一个新软件直接提供给多少个学校,才能使软件能够通过网络被传送到所有学校?

最少需要添加几条新的支援关系,使得将一个新软件提供给任何一个学校,其他所有学校就都可以通过网络获得该软件?


输 入 格 式 输入格式
第 1 行包含整数 N N N,表示学校数量。

第 2… N + 1 N+1 N+1行,每行包含一个或多个整数,第 i + 1 i+1 i+1 行表示学校 i i i 应该支援的学校名单,每行最后都有一个 0 0 0 表示名单结束(只有一个 0 0 0 即表示该学校没有需要支援的学校)。


输 出 格 式 输出格式
输出两个问题的结果,每个结果占一行。$

数 据 范 围 数据范围

2≤N≤100

输 入 样 例 : 输入样例:

5
2 4 3 0
4 5 0
0
0
1 0

输 出 样 例 : 输出样例:

1
2

思 路 思路
通过 T a r j a n Tarjan Tarjan算法把每一个强连通图缩成一个点,然后建立新图,统计新图中每一个点的入度和出度。
对于第一问,我们只需要在每一个入度为0的点上加上一个软件即可。
对于第二问,我们需要统计入度为0的点的数目a和出度为0的点的数目b,添加边的最小数目就是 m a x ( a , b ) max(a, b) max(a,b)该结论可以通过画图证明
代码:

#include <iostream>
#include <algorithm>
#include <cstring>

using namespace std;

const int N = 110, M = 10010;

int n, m;
int e[M], ne[M], h[N], idx;
int low[N], dfn[N], timestamp;
int stk[N], top;
bool in_stk[N];
int id[N], scc_cnt;
int din[N], dout[N];

void add(int a, int b)
{
    e[idx] = b, ne[idx] = h[a], h[a] = idx ++;
}

void tarjan(int u)
{
    dfn[u] = low[u] = ++ timestamp;
    stk[++ top] = u, in_stk[u] = true;

    for(int i = h[u]; ~i; i = ne[i])
    {
        int j = e[i];
        if(!dfn[j])
        {
            tarjan(j);
            low[u] = min(low[u], low[j]);
        }
        else if(in_stk[j])
            low[u] = min(low[u], dfn[j]);
    }

    if(dfn[u] == low[u])
    {
        ++scc_cnt;
        int y; 
        do {
            y = stk[top --];
            in_stk[y] = false;
            id[y] = scc_cnt;
        } while(y != u);
    }
}

int main()
{
    cin >> n;

    memset(h, -1, sizeof h);
    for(int i = 1; i <= n; i ++)
    {
        int t;
        while(cin >> t, t) add(i, t);
    }

    for(int i = 1; i <= n; i ++)
        if(!dfn[i])
            tarjan(i);

//不需要实际的重新建图,只需要统计相连的两个点是否在同一个强连通分量中
//如果不在则使得a的出度++, b的入度++;
    for(int i = 1; i <= n; i ++)
        for(int j = h[i]; ~j; j = ne[j])
        {
            int k = e[j];
            int a = id[i], b = id[k];
            if(a != b)
            {
                din[b] ++;
                dout[a] ++;
            }

        }

    int a = 0, b = 0;
    for(int i = 1; i <= scc_cnt; i ++)
    {
        if(!din[i]) a ++;
        if(!dout[i]) b ++;
    }

    cout << a << endl;
    if(scc_cnt == 1) puts("0");
    else
        cout << max(a, b) << endl ;

    return 0;
}

Tarjan算法在无向图中的应用

Tarjan算法求桥

假设 u u u j j j的父节点
d f n [ u ] < l o w [ j ] dfn[u] < low[j] dfn[u]<low[j]的时候说明 j j j永远无法到达 u u u u u u以上的位置,说明 u — j u — j uj这条边是图中的一个桥.
代码:

void tarjan(int u, int from)
{
    dfn[u] = low[u] = ++ timestamp;
    stk[ ++ top] = u;

    for (int i = h[u]; ~i; i = ne[i])
    {
        int j = e[i];
        if (!dfn[j])
        {
            tarjan(j, i);
            low[u] = min(low[u], low[j]);
            if (dfn[u] < low[j])
                is_bridge[i] = is_bridge[i ^ 1] = true;
        }
        else if (i != (from ^ 1))
            low[u] = min(low[u], dfn[j]);
    }

    if (dfn[u] == low[u])
    {
        ++ dcc_cnt;
        int y;
        do {
            y = stk[top -- ];
            id[y] = dcc_cnt;
        } while (y != u);
    }
}

注意:在我们更新 l o w [ u ] low[u] low[u]的时候,不能使用 反向边更新 l o w [ u ] low[u] low[u]的值否则会使父节点及其上面的点被更新。

Tarjan 算法求割点

假设 u u u j j j的父节点,根节点为 r o o t root root
d f n [ u ] ≤ l o w [ j ] dfn[u]\le low[j] dfn[u]low[j]同时如果 u u u不为根节点或者是根节点但是度 > 1 > 1 >1 则是割点。
代码:

#include <cstdlib>
#include <iostream>
#include <algorithm>
#include <cstring>
using namespace std;
typedef long long ll;
typedef pair<int, int> PII;
const int N = 1e5 + 10, M = N * 2;
int n, m;
int e[M], ne[M], h[N], idx;
int dfn[N], low[N], ans;
bool cut[N];

void add(int a, int b) {
	e[idx] = b, ne[idx] = h[a], h[a] = idx ++;
}

void tarjan (int u, int f) {
    dfn[u] = low[u] = ++ idx;
    int child = 0;
    for(int i = h[u]; ~i; i = ne[i]) {
        int v = e[i];
        if (!dfn[v]) {
            tarjan (v, f);
            low[u] = min(low[u], low[v]);
            if (low[v] >= dfn[u] && u != f && !cut[u]) {
                cut[u] = true;
				ans ++;
			}
            if(u == f)
                child ++;
        }
        low[u] = min(low[u], dfn[v]);
    }
    if (child >= 2 && u == f && !cut[u]) {
        cut[u] = true;
		ans ++;
	}
}

int main() {
    ios::sync_with_stdio(false); 
    cin.tie(0); cout.tie(0);
#ifndef ONLINE_JUDGE
    freopen("D:/Cpp/program/Test.in", "r", stdin);
    freopen("D:/Cpp/program/Test.out", "w", stdout);
#endif
    cin >> n >> m;
	memset(h, -1, sizeof h);
	for(int i = 1; i <= m; i ++) {
		int a, b;
		cin >> a >> b;
		add(a, b), add(b, a);
	}
    for(int i = 1; i <= n; i ++) {
        if (!dfn[i]) tarjan(i, i);
    }

	cout << ans << '\n';
    for(int i = 1; i <= n; i ++) {
		if(cut[i]) cout << i << ' ';
	}
    return 0;
}

例 题 : 例题:
A c W i n g 1183 : AcWing 1183: AcWing1183:
给定一个由 n 个点 m 条边构成的无向图,请你求出该图删除一个点之后,连通块最多有多少。
输 入 格 式 输入格式
输入包含多组数据。
每组数据第一行包含两个整数 n,m。
接下来 m m m行,每行包含两个整数 a , b a,b a,b,表示 a , b a,b a,b 两点之间有边连接。
数据保证无重边。
点的编号从 0 0 0 n − 1 n−1 n1
读入以一行 0 , 0 0, 0 0,0 结束。

输 出 格 式 输出格式
每组数据输出一个结果,占一行,表示连通块的最大数量。

数 据 范 围 数据范围
1 ≤ n ≤ 10000 , 1≤n≤10000, 1n10000,
0 ≤ m ≤ 15000 , 0≤m≤15000, 0m15000,
0 ≤ a , b < n 0≤a,b<n 0a,b<n
输 入 样 例 : 输入样例:

3 3
0 1
0 2
2 1
4 2
0 1
2 3
3 1
1 0
0 0

输 出 样 例 : 输出样例:

1
2
2

思 路 : 思路: 枚举一遍删除每一个连通块的割点时可以增加的连通块数目然后取最大值。
最终的答案就是:原本所存在的连通块数目 + + + 割点之后该割点所在的连通块的连通块数目 − - 1 1 1
代码:

#include <iostream>
#include <cstring>
#include <algorithm>

using namespace std;

const int N = 1e4 + 10, M = 3e4 + 10;

int n, m;
int e[M], ne[M], h[N], idx;
int low[N], dfn[N], timestamp;
int stk[N], top;
bool in_stk[N];
int ans, root; /ans记录割点后最多的连通块数目

void add(int a, int b)
{
    e[idx] = b, ne[idx] = h[a], h[a] = idx ++;
}

void tarjan(int u)
{
    low[u] = dfn[u] = ++ timestamp;

    int cnt = 0;
    for(int i = h[u]; ~i; i = ne[i])
    {
        int j = e[i];
        if(!dfn[j])
        {
            tarjan(j);
            low[u] = min(low[u], low[j]);
            if(low[j] >= dfn[u]) cnt ++;
        }
        else low[u] = min(low[u], dfn[j]);
    }

    if(u != root) cnt ++;

    ans = max(ans, cnt);
}

int main()
{
    while(cin >> n >> m, m || n)
    {
        memset(dfn, 0, sizeof dfn);
        memset(low, 0, sizeof low);
        memset(h, -1, sizeof h);
        idx = timestamp = 0;
        while(m --)
        {
            int a, b;
            cin >> a >> b;
            add(a, b), add(b, a);
        }

        ans = 0;
        int cnt = 0;
        for(root = 0; root < n; root ++)
            if(!dfn[root])
            {
                cnt ++;
                tarjan(root);
            }

        cout << ans + cnt - 1 << endl;;
    }

    return 0;
}

例 题 : 例题:
A c W i n g 396 : 矿 场 搭 建 AcWing396:矿场搭建 AcWing396
煤矿工地可以看成是由隧道连接挖煤点组成的无向图。

为安全起见,希望在工地发生事故时所有挖煤点的工人都能有一条出路逃到救援出口处。

于是矿主决定在某些挖煤点设立救援出口,使得无论哪一个挖煤点坍塌之后,其他挖煤点的工人都有一条道路通向救援出口。

请写一个程序,用来计算至少需要设置几个救援出口,以及不同最少救援出口的设置方案总数。

输 入 格 式 输入格式
输入文件有若干组数据,每组数据的第一行是一个正整数 N,表示工地的隧道数。

接下来的 N 行每行是用空格隔开的两个整数 S 和 T,表示挖煤点 S 与挖煤点 T 由隧道直接连接。

注意,每组数据的挖煤点的编号为 1∼Max,其中 Max 表示由隧道连接的挖煤点中,编号最大的挖煤点的编号,可能存在没有被隧道连接的挖煤点。

输入数据以 0 结尾。

输 出 格 式 输出格式
每组数据输出结果占一行。

其中第 i 行以 Case i: 开始(注意大小写,Case 与 i 之间有空格,i 与 : 之间无空格,: 之后有空格)。

其后是用空格隔开的两个正整数,第一个正整数表示对于第 i 组输入数据至少需要设置几个救援出口,第二个正整数表示对于第 i 组输入数据不同最少救援出口的设置方案总数。

输入数据保证答案小于 264,输出格式参照以下输入输出样例。

数 据 范 围 数据范围
1 ≤ N ≤ 500 , 1≤N≤500, 1N500
1 ≤ M a x ≤ 1000 1≤Max≤1000 1Max1000
输 入 样 例 : 输入样例:

9
1 3
4 1
3 5
1 2
2 6
1 5
6 3
1 6
3 2
6
1 2
1 3
2 4
2 5
3 6
3 7
0

输 出 样 例 : 输出样例:

Case 1: 2 4
Case 2: 4 1

思 路 : 思路: 对于每一个连通块中的割点数目,我们进行分类讨论

  1. 当连通块中不存在割点的时候:
    1.1 连通块中只有一个点 只能在该点上放置一个出口,方案数++
    1.2 连通块中点的数目 ≥ 2 \ge2 2的时候,需要放置两个出口,方案数为 c n 2 c_n^2 cn2
  2. 当连通块中割点数目为 1 1 1 的时候,如果割点坍塌,该连通块无法到达其他连通块,所以至少需要在该连通块中非割点位置放置一个出口;方案数为 c n t − 1 cnt - 1 cnt1;
  3. 当连通块中的 割 点 数 目 > 1 割点数目 \gt1 >1 的时候,当一个割点坍塌,剩下的连通块仍与其他块相连通,所以不需要添加出口。

代 码 : 代码: :

#include <iostream>
#include <cstring>
#include <algorithm>
#include <vector>

using namespace std;

typedef unsigned long long ULL;

const int N = 510, M = 1010;

int n, m;
int e[M], ne[M], h[N],idx;
int dfn[N], low[N], timestamp;
int stk[N], top;
int dcc_cnt;
vector<int> dcc[N];
bool cut[N];
int root;

void add(int a, int b)
{
    e[idx] = b, ne[idx] = h[a], h[a] = idx ++;
}

void tarjan(int u)
{
    low[u] = dfn[u] = ++ timestamp;
    stk[++ top] = u;
    
    if(root == u && h[u] == -1)
    {
        dcc_cnt ++;
        dcc[dcc_cnt].push_back(u);
        return;
    }
    
    int cnt = 0;
    for(int i = h[u]; ~i; i = ne[i])
    {
        int j = e[i];
        if(!dfn[j])
        {
            tarjan(j);
            low[u] = min(low[u], low[j]);
            if(dfn[u] <= low[j]) // 点j不可能到达比他更高的点
            {
                cnt ++; // 度++
                if(u != root || cnt > 1) cut[u] = true; //如果不为根节点或者是根节点但是度 > 1 则是割点
                ++ dcc_cnt; //连通块数
                int y;
                do {
                    y = stk[top --];
                    dcc[dcc_cnt].push_back(y);
                } while(y != j); //注意这里是j
                dcc[dcc_cnt].push_back(u); //最后再把父节点放入
            }
        }
        else low[u] = min(low[u], dfn[j]);
    }
}

int main()
{
    int T = 1;
    while(cin >> m, m)
    {
        for(int i = 1; i <= dcc_cnt; i++) dcc[i].clear();
        idx = timestamp = top = dcc_cnt = n = 0;
        memset(h, -1, sizeof h);
        memset(dfn, 0, sizeof dfn);
        memset(cut, 0, sizeof cut);
        
        while(m --)
        {
            int a, b;
            cin >> a >> b;
            n = max(a, n), n = max(b, n);
            add(a, b), add(b, a);
        }
        
        for(root = 1; root <= n; root ++)
            if(!dfn[root])
                tarjan(root);
            
        int res = 0;
        ULL num = 1; //方案数
        for(int i = 1; i <= dcc_cnt; i ++)
        {
            int cnt = 0;
            for(int j = 0; j < dcc[i].size(); j ++)
                if(cut[dcc[i][j]])
                    cnt ++;
                    
            if(cnt == 0) //割点数目为0
            {
                if(dcc[i].size() > 1) res += 2, num *= dcc[i].size() * (dcc[i].size() - 1) / 2;
                else res ++;
            }
            
            else if(cnt == 1) res ++, num *= dcc[i].size() - 1;
        }
        
        printf("Case %d: %d %llu\n", T ++, res, num);
    }
    return 0;
}

一些结论

  1. 增加一些边,使得有向图中称为强连通图
    需要增加的边的数目: m a x ( p , q ) max(p, q) max(p,q)
    (其中, p, q分别为出度为0的点和入度为0的点的数目)

  2. 增加一些边,使得在无向图中不存桥
    需要增加的边的数目: c n t + 1 2 \dfrac{cnt + 1}{2} 2cnt+1
    (其中 cnt为图中度为1的节点的个数)

最近公共祖先(LCA)

定义 :

最近公共祖先简称 LCA(Lowest Common Ancestor)。两个节点的最近公共祖先,就是这两个点的公共祖先里面,离根最远的那个。

在一般的题目中,会用到求两个节点的最近公共祖先。

求LCA方法

一般常用的求LCA的方法是倍增算法和Tarjan

1.倍增算法:

时 间 复 杂 度 o ( n l o g n ) 时间复杂度o(nlogn) o(nlogn)

这里推荐一个视频,文字叙述也不是很好懂,视频很短, 看了之后可以很好的理解倍增算法的精髓:

倍增算法求LCA

代码实现:

例题:
给定一棵包含 n 个节点的有根无向树,节点编号互不相同,但不一定是 1∼n。
有 m 个询问,每个询问给出了一对节点的编号 x 和 y,询问 x 与 y 的祖孙关系。
输入格式$
输入第一行包括一个整数 表示节点个数;
接下来 n 行每行一对整数 a 和 b,表示 a 和 b 之间有一条无向边。如果 b 是 −1,那么 a 就是树的根;
第 n+2 行是一个整数 m 表示询问个数;
接下来 m 行,每行两个不同的正整数 x 和 y,表示一个询问。
输 出 格 式 输出格式
对于每一个询问,若 x 是 y 的祖先则输出 1,若 y 是 x 的祖先则输出 2,否则输出 0。
数 据 范 围 数据范围
1 ≤ n , m ≤ 4 × 104 , 1≤n,m≤4×104, 1n,m4×104,
1 ≤ 每 个 节 点 的 编 号 ≤ 4 × 104 1≤每个节点的编号≤4×104 14×104

输 入 样 例 : 输入样例:

10
234 -1
12 234
13 234
14 234
15 234
16 234
17 234
18 234
19 234
233 19
5
234 233
233 12
233 13
233 15
233 19

输 出 样 例 : 输出样例:

1
0
0
0
2

第一步:建树,这里我用的是邻接表建树
第二步: 求fa[ ][ ]数组,DFS和BFS都可以
然后最后直接求LCA判断一下就行了

B F S 求 法 ( 不 容 易 产 生 段 错 误 ) BFS求法(不容易产生段错误) BFS)
点 u 表 示 根 节 点 模 拟 一 个 队 列 q , 不 断 扩 展 所 有 点 点u表示根节点 模拟一个队列q, 不断扩展所有点 uq,

#include <iostream>
#include <cstring>
#include <algorithm>

using namespace std;
const int N = 4e4 + 10, M = N * 2;
int e[M], ne[M], h[N], idx;
int depth[N], fa[N][16];
int n, m;

void add(int a, int b)
{
    e[idx] = b, ne[idx] = h[a], h[a] = idx ++;
}

void bfs(int u)
{
    memset(depth, 0x3f ,sizeof depth);
    depth[u] = 1, depth[0] = 0;
    int q[N];
    int tt = 0, hh = 0;
    q[0] = u;

    while(hh <= tt)
    {
        int t = q[hh ++];

        for(int i = h[t]; ~i; i = ne[i])
        {
            int j = e[i];
            if(depth[j] > depth[t] + 1)
            {
                depth[j] = depth[t] + 1;
                q[++ tt] = j;
                fa[j][0] = t;
                for(int k = 1; k <= 15; k ++)
                    fa[j][k] = fa[fa[j][k - 1]][k - 1];
            }
        }
    }
}

int lca(int a, int b)
{
    if(depth[a] < depth[b]) swap(a, b);
    for(int k = 15; k >= 0; k --)
        if(depth[fa[a][k]] >= depth[b])
            a = fa[a][k];

    if(a == b) return a;

    for(int k = 15; k >= 0; k --)
        if(fa[a][k] != fa[b][k])
        {
            a = fa[a][k];
            b = fa[b][k];
        }

    return fa[a][0];
}

int main()
{
    int n; //表示有n个点
    cin >> n;
    int root = 0;
    memset(h, -1 ,sizeof h);
    for(int i = 0; i < n; i ++)
    {
        int a, b;
        cin >> a >> b;
        if(b == -1) root = a;
        else add(a, b), add(b, a);
    }

    bfs(root);

    cin >> m;
    while (m --)
    {
        int a, b;
        cin >> a >> b;
        int p = lca(a ,b);
        if(p == a) puts("1");
        else if(p == b) puts("2");
        else puts("0");
    }

    return 0;
}

作者:Rhymes
链接:https://www.acwing.com/activity/content/code/content/1732177/
来源:AcWing
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

这里再贴一道次小生成树的题,之前刷到感觉有点难度。

严格次小生成树

给定一张 N 个点 M 条边的无向图,求无向图的严格次小生成树。

设最小生成树的边权之和为 sum,严格次小生成树就是指边权之和大于 sum 的生成树中最小的一个。

输 入 格 式 输入格式
第一行包含两个整数 N 和 M。

接下来 M 行,每行包含三个整数 x,y,z,表示点 x 和点 y 之前存在一条边,边的权值为 z。

输 出 格 式 输出格式
包含一行,仅一个数,表示严格次小生成树的边权和。(数据保证必定存在严格次小生成树)

数 据 范 围 数据范围
N ≤ 105 , M ≤ 3 × 105 N≤105,M≤3×105 N105,M3×105

输 入 样 例 : 输入样例:

5 6
1 2 1
1 3 2
2 4 3
3 5 4
3 4 3
4 5 6

输 出 样 例 : 输出样例:

11

简单说一下思路
首 先 用 k u r s k a l 算 法 求 一 下 最 小 生 成 树 首先用kurskal算法求一下最小生成树 kurskal

再通过dfs找到最小生成树中任意两点之间所经过的路径的最大值和次大值
(比如从a走到e可能经过b c d, 现在就是要找到从a 到 e上的最大值和次大值)

  • (建议自己画个图理解下)

( 这 里 我 们 定 义 不 在 树 中 的 边 L i n e 连 接 着 点 a b , 最 近 公 共 祖 先 节 点 是 L C A ) (这里我们定义不在树中的边Line连接着点a b, 最近公共祖先节点是LCA) (Lineab,LCA)

找 到 点 a 和 b 的 L C A ( 最 近 公 共 祖 先 ) 找到点a和b的LCA(最近公共祖先) abLCA)
在 a 和 b 分 别 走 到 L C A 上 时 候 会 产 生 两 对 最 大 值 和 次 大 值 在a 和 b分别走到LCA上时候会产生两对最大值和次大值 abLCA
(边从 a -> LCA 和 b -> LCA两条边, 每条边会分别产生一对最大值和次大值)

然 后 再 次 求 出 这 四 个 值 中 的 最 大 值 和 次 大 值 然后再次求出这四个值中的最大值和次大值

如 果 最 大 值 小 于 L i n e ( 遍 历 到 的 不 在 树 中 的 边 ) 的 权 值 , 就 替 换 成 最 大 值 , 否 则 如 果 大 于 次 大 值 则 替 换 次 大 值 ; 如果最大值小于Line(遍历到的不在树中的边)的权值,就替换成最大值,否则如果大于次大值则替换次大值; Line()

#include <iostream>
#include <cstdio>
#include <cstring>
#include <algorithm>
typedef long long ll;
using namespace std;

const int N = 1e5 + 10, M = 3e5 + 10;
const int INF = 0x3f3f3f3f;
int n, m;
int p[N];
int fa[N][32], deep[N];
int d1[N][32], d2[N][32]; //d1表示最大值 d2表示次大值
int e[N * 2], ne[N * 2], h[N], w[N * 2], idx;

struct Edge
{
    int a, b, w;
    bool f;

    bool operator < (const Edge W) const
    {
        return w < W.w;
    }
}edge[M];

void add(int a, int b, int c)
{
    e[idx] = b, ne[idx] = h[a], w[idx] = c, h[a] = idx++;
}

int find(int x)
{
    if (p[x] != x) p[x] = find(p[x]);

    return p[x];
}

//求最小生成树 以及判断边是否在生成树中 顺便建个图(邻接表)
ll kurskal()
{
    for (int i = 1; i <= n; i++) p[i] = i;

    memset(h, -1, sizeof h);

    ll sum = 0;
    for (int i = 0; i < m; i++)
    {
        int a = edge[i].a, b = edge[i].b, w = edge[i].w;
        int pa = find(a), pb = find(b);
        if (pa != pb)
        {
            p[pa] = pb;
            edge[i].f = true;
            add(a, b, w), add(b, a, w);
            sum += w;
        }
    }

    return sum;
}

//求最小生成树中的最大值和次大值
void dfs(int u, int father)
{
    fa[u][0] = father;
    deep[u] = deep[father] + 1;

    for (int i = 1; i <= 31; i++)
    {
        int anc = fa[u][i - 1];
        fa[u][i] = fa[anc][i - 1];
        int distance[4] = { d1[u][i - 1], d2[u][i - 1], d1[anc][i - 1], d2[anc][i - 1] };
        d1[u][i] = d2[u][i] = -INF;
        for (int k = 0; k < 4; k++)
        {
            int t = distance[k];
            if (t > d1[u][i]) d2[u][i] = d1[u][i], d1[u][i] = t;
            else if (t > d2[u][i] && t != d1[u][i]) d2[u][i] = t;
        }
    }

    for (int i = h[u]; ~i; i = ne[i])
    {
        int j = e[i];
        if (j == father) continue;
        d1[j][0] = w[i], d2[j][0] = -INF;
        dfs(j, u);
    }
}

//只求lca节点
int lca(int x, int y)
{
    if (deep[y] > deep[x]) swap(x, y);

    for (int i = 31; i >= 0; i--)
        if (deep[fa[x][i]] >= deep[y])
            x = fa[x][i];

    if (x == y) return x;

    for (int i = 31; i >= 0; i--)
        if (fa[x][i] != fa[y][i])
            x = fa[x][i], y = fa[y][i];

    return fa[x][0];
}

//每次只求树中一个方向的最大值和次大值 
//再带入另一个方向后就能求出两个方向上一起的最大值和次大值
void qmax(int x, int y, int& max1, int& max2)
{
    if (deep[y] > deep[x]) swap(x, y);

    for (int i = 31; i >= 0; i--)
        if (deep[fa[x][i]] >= deep[y])
        {
            int dis[2] = { d1[x][i], d2[x][i] };
            for (int k = 0; k < 2; k++)
            {
                int w = dis[k];
                if (w > max1) max2 = max1, max1 = w;
                else if (w > max2 && w != max1) max2 = w;
                x = fa[x][i];
            }
        }
    return;
}

int main()
{
    cin >> n >> m;

    for (int i = 0; i < m; i++)
    {
        int a, b, w;
        scanf("%d%d%d", &a, &b, &w);
        edge[i] = { a, b, w };
    }

    sort(edge, edge + m);

    ll sum = kurskal();

    //lca预处理
    deep[1] = 1;
    d1[1][0] = d2[1][0] = INF;
    dfs(1, -1);

    //lca处理出任意两点路径之间的最大值, 次大值
    ll ans = 1e18;
    for (int i = 0; i < m; i++) //这里是访问没在生成树中的边 
        if (!edge[i].f)
        {
            int a = edge[i].a, b = edge[i].b, w = edge[i].w;
            ll t;
            int max1 = -INF;
            int max2 = -INF;
            int LCA = lca(a, b);
            //分别从lca节点两个方向更新
            qmax(a, LCA, max1, max2); 
            qmax(b, LCA, max1, max2);
            if (w > max1)
                t = sum + w - max1;
            else if (w > max2)
                t = sum + w - max2;
            ans = min(ans, t);
        }

    cout << ans << endl;
    return 0;
}

作者:Rhymes
链接:https://www.acwing.com/solution/content/62841/
来源:AcWing
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
o(1) 求LCA
tttt  18:30:11
namespace LCA{
    int cnt,dep[maxn],fir[maxn],lg,lg2[maxn<<1],st[21][maxn<<1];
    void dfs(int p,int f){
        dep[p]=dep[f]+1;
        st[0][++cnt]=p;
        fir[p]=cnt;
        for(ri i=hd[p];i;i=e[i].nxt)
            if(e[i].to!=f)
                b[e[i].to]=b[p]+(a[p]-a[e[i].to]),dfs(e[i].to,p),st[0][++cnt]=p;
    }
    inline int ptmin(const int &x,const int &y){
        return dep[x]<dep[y]?x:y;
    }
    inline void build_st(){
        for(ri i=1;i<=cnt;++i)lg2[i]=lg2[i-1]+((2<<lg2[i-1])==i);
        lg=lg2[cnt];
        for(ri i=1;i<=lg;++i)
            for(ri j=1;j+(1<<i)-1<=cnt;++j)
                st[i][j]=ptmin(st[i-1][j],st[i-1][j+(1<<(i-1))]);
    }
    inline int lca(int x,int y){
        ri l=fir[x],r=fir[y];
        if(l>r)swap(l,r);
        ri k=lg2[r-l+1];
        return ptmin(st[k][l],st[k][r-(1<<k)+1]);
    }
    inline void init(int rt){
        dfs(rt,0);
        build_st();
    }
}
using LCA::lca;

tttt  18:30:37
使用:
LCA::init(1)
lca(a, b)


2.Tarjan算法

时 间 复 杂 度 o ( m + n ) T a r j a n 是 一 种 离 线 算 法 。 时间复杂度o(m + n) Tarjan是一种离线算法。 o(m+n)Tarjan线

这里引用一下OI wiki上的解释
在这里插入图片描述

例 题 例题
给 出 n 个 点 的 一 棵 树 , 多 次 询 问 两 点 之 间 的 最 短 距 离 。 给出 n 个点的一棵树,多次询问两点之间的最短距离。 n

注 意 : 注意:

  • 边是无向的。
  • 所有节点的编号是 1,2,…,n。

输 入 格 式 输入格式
第一行为两个整数 n 和 m。n 表示点数,m 表示询问次数;

下来 n−1 行,每行三个整数 x,y,k,表示点 x 和点 y 之间存在一条边长度为 k;

$再接下来 m 行,每行两个整数 x,y,表示询问点 x 到点 y 的最短距离。

树中结点编号从 1 到 n。

输 出 格 式 输出格式
共 m 行 , 对 于 每 次 询 问 , 输 出 一 行 询 问 结 果 。 共 m 行,对于每次询问,输出一行询问结果。 m

数 据 范 围 数据范围
2 ≤ n ≤ 104 , 2≤n≤104, 2n104,
1 ≤ m ≤ 2 × 104 , 1≤m≤2×104, 1m2×104,
0 < k ≤ 100 , 0<k≤100, 0<k100,
1 ≤ x , y ≤ n 1≤x,y≤n 1x,yn
输 入 样 例 1 : 输入样例1: 1

2 2
1 2 100
1 2
2 1

输 出 样 例 1 : 输出样例1: 1

100
100

C++代码:

#include <iostream>
#include <algorithm> 
#include <vector>
#include <cstring>

using namespace std;

const int N = 1e4 + 10, M = 4e4 + 10;

typedef pair<int, int> PII;
int n, m;
int e[M], ne[M], h[N], w[M], idx;
int dist[N], res[M];
int p[N];
int st[N];
vector<PII> query[N];

void add(int a, int b, int c)
{
    e[idx] = b, ne[idx] = h[a], w[idx] = c, h[a] = idx ++;
}

int find(int x)
{
    if(x != p[x]) p[x] = find(p[x]);

    return p[x];
}

void dfs(int u, int fa)
{
    for(int i = h[u]; ~i; i = ne[i])
    {
        int j = e[i];
        if(j == fa) continue;
            dist[j] = dist[u] + w[i];
            dfs(j, u);
    }
}

void tarjan(int u)
{
    st[u] = 1; //1表示该点正在遍历

    for(int i = h[u]; ~i; i = ne[i])
    {
        int j = e[i];
        if(!st[j])
        {
            tarjan(j);
            p[j] = u;
        }
    }

    for(auto item : query[u])
    {
        int y = item.first, id = item.second;
        if(st[y] == 2)
        {
            int anc = find(y);
            res[id] = dist[u] + dist[y] - 2 * dist[anc];
        }
    }

    st[u] = 2; //表示该点已经被遍历过了已经遍历过
}

int main()
{
    cin >> n >> m;
    memset(h, -1, sizeof h);
    for(int i = 0; i < n - 1; i++)
    {
        int a, b, c;
        cin >> a >> b >> c;
        add(a, b, c), add(b, a, c);
    }

    for(int i = 0; i < m; i ++)
    {
        int a, b;
        cin >> a >> b;
        if(a != b)
        {
            query[a].push_back({b, i});
            query[b].push_back({a, i});
        }
    }

    dfs(1, -1);

    for(int i = 1; i <= n; i ++) p[i] = i;

    tarjan(1);

    for(int i = 0; i < m; i ++) cout << res[i] << endl;
}

作者:Rhymes
链接:https://www.acwing.com/activity/content/code/content/1733489/
来源:AcWing
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

二分图

二分图定义

节 点 由 两 个 集 合 组 成 , 且 两 个 集 合 内 部 没 有 边 的 图 。 节点由两个集合组成,且两个集合内部没有边的图。

下 图 就 是 一 个 二 分 图 下图就是一个二分图
这是一个二分图

二分图的判定

染色法判断二分图

利用二分图不存在奇数环这个性质来判断是否为二分图。
如果两个集合中的点分别染成黑色和白色,可以发现二分图中的每一条边都一定是连接一个黑色点和一个白色点
这是因为每一条边都是从一个集合走到另一个集合,只有走偶数次才可能回到同一个集合。
代 码 : 代码:

bool dfs(int u, int c)
{
    color[u] = c;
    for(int i = h[u]; ~i; i = ne[i])
    {
        int j = e[i];
        if(color[j])
        {
            if(color[j] == c) return false;
        }
        else if(!dfs(j, 3 - c, mid)) return false;
    }

    return true;
}

这里利用了 3 − 1 = 2 3 - 1 = 2 31=2 3 − 2 = 1 3 - 2 = 1 32=1 来进行递归染色。

求二分图的最大匹配

匈牙利算法( 牛头人

a a a 需要匹配的点 b b b,如果该匹配点 b b b已经被匹配,则当点 b b b 的匹配点 c c c 可以更换为其他点 d d d的时候, a a a 就和b匹配, c c c就和 d d d 匹配。

bool find(int x)
{
    for(int i = 0; i < m; i ++)
        if(!st[i] && g[x][i])
        {
            st[i] = true;
            if(match[i] == -1 || find(match[i]))
            {
                match[i] = x;
                return true;
            }
        }

    return false;
}

int main()
{
	int res = 0; //最大匹配数
        for(int i = 0; i < n; i ++)
        {
            memset(st, 0, sizeof st);
            if(find(i)) res ++;
        }
}
二分图常用性质

二分图中

  1. 最大独立集
    选最多的点,满足两两之间没有边相连。
    最 大 独 立 集 = 总 点 数 − 最 大 匹 配 最大独立集 = 总点数 - 最大匹配 =
  2. 最小点覆盖
    选最少的点,满足每条边至少有一个端点被选
    最 小 点 覆 盖 = 总 点 数 − 最 大 独 立 集 最小点覆盖 = 总点数 - 最大独立集 =
  3. 最大匹配数 = 最小点覆盖 = 总点数 - 最大独立集 = 总点数 - 最小路径覆盖
二分图常用操作
求传递闭包

代码:

    //传递闭包
    for(int k = 1; k <= n; k ++)
        for(int i = 1; i <= n; i ++)
            for(int j = 1; j <= n; j ++)
                d[i][j] |= d[i][k] & d[k][j];

欧拉回路

定义

  • 通过图中所有边恰好一次且行遍所有顶点的通路称为欧拉通路。

  • 通过图中所有边恰好一次且行遍所有顶点的回路称为欧拉回路。

  • 具有欧拉回路的无向图或有向图称为欧拉图

  • 具有欧拉通路但不具有欧拉回路的无向图或有向图称为半欧拉图

非形式化地讲,欧拉图就是从任意一个点开始都可以一笔画完整个图,半欧拉图必须从某个点开始才能一笔画完整个图。

判断是否为欧拉图

  • 对于无向图 G G G G G G 是欧拉图当且仅当 G G G 是连通的且没有奇度顶点。

  • 对于有向图 G G G G G G 是欧拉图当且仅当 的所有顶点属于同一个强连通分量且每个顶点的入度和出度相同。

  • 对于无向图 G G G 是半欧拉图当且仅当 是连通的且 中恰有 个或 个奇度顶点。

  • 对于有向图 G G G 是半欧拉图当且仅当:

  1. 如果将 G G G 中的所有有向边退化为无向边时,那么 G G G 的所有顶点属于同一个连通分量。
  2. 最多只有一个顶点的出度与入度差为 1 1 1
  3. 最多只有一个顶点的入度与出度差为 1 1 1
  4. 所有其他顶点的入度和出度相同。

判断是否为欧拉图:
例 题 : A c W i n g 1184 欧 拉 回 路 例题:AcWing1184 欧拉回路 AcWing1184

给定一张图,请你找出欧拉回路,即在图中找一个环使得每条边都在环上出现恰好一次。

输 入 格 式 输入格式
第一行包含一个整数 t,t∈{1,2},如果 t=1,表示所给图为无向图,如果 t=2,表示所给图为有向图。
第二行包含两个整数 n,m,表示图的结点数和边数。
接下来 m 行中,第 i 行两个整数 vi,ui,表示第 i 条边(从 1 开始编号)。

如果 t=1 则表示 vi 到 ui 有一条无向边。
如果 t=2 则表示 vi 到 ui 有一条有向边。
图中可能有重边也可能有自环。

点的编号从 1 到 n。

输 出 格 式 输出格式
如果无法一笔画出欧拉回路,则输出一行: N O 。 NO。 NO

否则,输出一行: Y E S YES YES,接下来一行输出 任意一组 合法方案即可。

如果 t = 1 t=1 t=1,输出 m m m 个整数 p 1 , p 2 , … , p m p_1,p_2,…,p_m p1,p2,,pm。令 e = ∣ p i ∣ e=|p_i| e=pi,那么 e e e 表示经过的第 i i i 条边的编号。如果 p i p_i pi 为正数表示从 v e v_e ve 走到 u e u_e ue,否则表示从 u e u_e ue 走到 v e v_e ve
如果 t = 2 t=2 t=2,输出 m m m 个整数 p 1 , p 2 , … , p m p_1,p_2,…,p_m p1,p2,,pm。其中 p i pi pi 表示经过的第 i i i 条边的编号。
数 据 范 围 数据范围
1 ≤ n ≤ 105 , 1≤n≤105, 1n105,
0 ≤ m ≤ 2 × 105 0≤m≤2×105 0m2×105
输 入 样 例 1 : 输入样例1: 1

1
3 3
1 2
2 3
1 3

输 出 样 例 1 : 输出样例1: 1

YES
1 2 -3

输 入 样 例 2 : 输入样例2: 2

2
5 6
2 3
2 5
3 4
1 2
4 2
5 1

输 出 样 例 2 : 输出样例2: 2

YES
4 1 3 5 2 6

在第一次深度遍历到终点的时候,会存在没有遍历完所有边的情况,所以为了保证我们可以遍历到所有边,我们在回溯的时候将遍历到的边逆序加入到数组中,这样可以保证所有通路中的点都被加入。

  • 如果有向图满足 ( d i n [ a ] ≠ d o u t [ a ] ) (din[a] \ne dout[a]) (din[a]=dout[a]) 则说明不是欧拉回路

  • 如果无向图满足 ( d i n [ a ] + d o u t [ a ] ) (din[a] + dout[a]) (din[a]+dout[a])是奇数 则说明不是欧拉回路

  • 同时,如果边的数量少于总边数,则说明这不是一个欧拉回路。

在处理边的编号的时候:

  • 无向图中的边的序号为: 0 0 0 m − 1 m-1 m1,而在题目中的编号是 1 1 1 m m m;
    所以转化边时: 0 , 1 0,1 0,1对应 1 1 1号边, 2 , 3 2,3 2,3对应 2 2 2号边,即 x x x号边对应 x 2 + 1 \dfrac{x}{2}+1 2x+1
  • 奇数: x x x ^ 1 1 1中, x x x 二进制前面的数不变,最后一位变成 0 0 0, x x x ^ 1 = x − 1 1=x-1 1=x1;
  • 偶数 x x x ^ 1 1 1中,x二进制前面的数不变,最后一位变成 1 1 1, x x x ^ 1 1 1 = x + 1 ; =x+1; =x+1;

为了降低时间复杂度, 我们应当先删边然后再遍历。
这样可以更快的回溯到未遍历的边上。

#include <iostream>
#include <cstring>
#include <algorithm>
#include <cstdio>

using namespace std;

const int N = 1e5 + 10, M = 4e5 + 10;

int type;
int n, m;
int h[N], e[M], ne[M], idx;
int din[N], dout[N];
int ans[M], cnt;
bool used[M];

void add(int a, int b)
{
    e[idx] = b, ne[idx] = h[a], h[a] = idx ++;
}

void dfs(int u)
{
    for(int &i = h[u]; ~i; )
    {
        if(used[i])
        {
            i = ne[i];
            continue;
        }
        
        used[i] = true;
        if(type == 1) used[i ^ 1] = true;
        
        int t;
        if(type == 1)
        {
            t = i / 2 + 1;
            if(i & 1) t = -t;
        }
        else t = i + 1;
        
        int j = e[i];
        i = ne[i];
        dfs(j);
        
        ans[ ++ cnt] = t;
    }
}

int main()
{
    cin >> type;
    cin >> n >> m;
    
    memset(h, -1 ,sizeof h);
    for(int i = 0; i < m; i ++)
    {
        int a, b;
        cin >> a >> b;
        add(a, b);
        if(type == 1)  add(b, a);
        din[b] ++, dout[a] ++;
    }
    
    if(type == 1)
    {
        for(int i = 1; i <= n; i ++)
            if(din[i] + dout[i] & 1)
            {
                puts("NO");
                return 0;
            }
    }
    else
    {
        for(int i = 1; i <= n; i ++)
            if(din[i] != dout[i])
            {
                puts("NO");
                return 0;
            }
    }
    
    
    for(int i = 1; i <= n; i ++)
        if(h[i] != -1) //找到一个非孤立点
        {
            dfs(i);
            break;
        }
        
    if(cnt < m) 
    {
        cout << "NO" << endl;
        return 0;
    }
    
    puts("YES");
    for(int i = cnt ; i > 0; i --) cout << ans[i] << ' ';
    
    return 0;
}
  • 邻接矩阵中的删边操作

例 二 : 例二:
A c W i n g 1124. 骑 马 修 栅 栏 AcWing1124. 骑马修栅栏 AcWing1124.
农民 J o h n John John 每年有很多栅栏要修理。

他总是骑着马穿过每一个栅栏并修复它破损的地方。

J o h n John John 是一个与其他农民一样懒的人。

他讨厌骑马,因此从来不两次经过一个栅栏。

你必须编一个程序,读入栅栏网络的描述,并计算出一条修栅栏的路径,使每个栅栏都恰好被经过一次。

J o h n John John能从任何一个顶点(即两个栅栏的交点)开始骑马,在任意一个顶点结束。

每一个栅栏连接两个顶点,顶点用 1 1 1 500 500 500 标号(虽然有的农场并没有 500 500 500 个顶点)。

一个顶点上可连接任意多 ( ≥ 1 ) ( ≥1 ) (1)个栅栏。

所有栅栏都是连通的(也就是你可以从任意一个栅栏到达另外的所有栅栏)。

你的程序必须输出骑马的路径(用路上依次经过的顶点号码表示)。

我们如果把输出的路径看成是一个 500 500 500 进制的数,那么当存在多组解的情况下,输出 500 500 500 进制表示法中最小的一个 (也就是输出第一个数较小的,如果还有多组解,输出第二个数较小的,等等)。

输入数据保证至少有一个解。

输 入 格 式 输入格式
第 1 行:一个整数 F F F,表示栅栏的数目;

第 2 到 F + 1 F+1 F+1 行:每行两个整数 i , j i,j i,j 表示这条栅栏连接 i i i j j j 号顶点。

输 出 格 式 输出格式
输出应当有 F + 1 F+1 F+1 行,每行一个整数,依次表示路径经过的顶点号。

注意数据可能有多组解,但是只有上面题目要求的那一组解是认为正确的。

数 据 范 围 数据范围
1 ≤ F ≤ 1024 , 1≤F≤1024, 1F1024,
1 ≤ i , j ≤ 500 1≤i,j≤500 1i,j500
输 入 样 例 : 输入样例:

9
1 2
2 3
3 4
4 2
4 5
2 5
5 6
5 7
4 6

输 出 样 例 : 输出样例:

1
2
3
4
2
5
4
6
5
7

思 路 : 思路:

  1. 将栅栏的交点看成顶点,栅栏看成边,相当于给我们给我们一个无向图,让我们求一个欧拉路径。
  2. 本题需要先找到起始点位置,可能是一个度为 1 1 1 的点,也有可能是为一个度为非零偶数的点,我们需要先找到这这样的点再开始 d f s dfs dfs
#include <iostream>
#include <cstring>
#include <algorithm>

using namespace std;

const int N = 510;
int n = 500, m;
int g[N][N];
int ans[1100], cnt;
int d[N];

void dfs(int u)
{
    for(int i = 1; i <= n; i ++)
        if(g[u][i])
        {
            g[u][i] --, g[i][u] --;
            dfs(i);
        }
    ans[++ cnt] = u;
}

int main()
{
     cin >> m;
     while(m --)
     {
         int a, b;
         cin >> a >> b;
         g[a][b] ++, g[b][a] ++;
         d[a] ++, d[b] ++;
     }
     
      int start = 1;
      while(!d[start]) start ++; //农场不是从1开始并且无向图中的点的度都是偶数时这一行有用
      for(int i = 1; i <= n; i++)
        if(d[i] % 2)
        {
            start = i;
            break;
        }
        
    dfs(start);
    
    for(int i = cnt; i > 0; i --) cout << ans[i] << endl ;
    
    return 0;
}

拓扑排序

什么是拓扑排序

在图论中,拓扑排序 ( T o p o l o g i c a l S o r t i n g ) (Topological Sorting) TopologicalSorting是一个有向无环图 ( D A G , D i r e c t e d A c y c l i c G r a p h ) (DAG, Directed Acyclic Graph) DAG,DirectedAcyclicGraph的所有顶点的线性序列。 且该序列必须满足下面两个条件:

  • 每个顶点出现且只出现一次。
  • 若存在一条从顶点 A 到顶点 B 的路径,那么在序列中顶点 A A A 出现在顶点 B B B 的前面。

拓扑排序的目标是将所有节点排序,使得排在前面的节点不能依赖于排在后面的节点。

实现代码

void topsort()
{
    int hh = 0, tt = -1;
    //将所有入度为0的点加入队列
    for(int i = 1; i <= n; i ++)
        if(!d[i])
            q[++ tt] = i;

    while(hh <= tt)
    {
        int t = q[hh ++];

        for(int i = h[t]; ~i; i = ne[i])
        {
            int j = e[i];
            if(-- d[j] == 0) //将所有入度减为0的点加入队列
                q[++ tt] = j;
        }
    }
}

经过上述操作后队列 q q q 中的元素就已经是拓扑序了

网络流

写了太多好卡
点这里去网络流.

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值