算法竞赛进阶指南 0x67 Tarjan 算法与有向图连通性
相关概念
有向图中,如果存在一个点,使得从出发,那么就可以到达所有的节点,那么称G为一个流图,记作
有向图的强连通分量
对于强连通子图的等价条件就是具有一条经过所有节点的环(环的路径可以重复走)
在深度优先搜索树中,容易发现,需要找到最外层的环即可,这样,环里面由于具有树枝边,所以与外面的环一起,构成强连通。
对于有向图的4类边,前向边没有任何作用(完全可以有树枝边来进行充当),所以不进行考虑。
对于一个环,
- 由树枝边以及后向边组成。
- 有横叉边的参与。条件是横叉边所连接的另一个分支里面的点可以到达x的祖先。
实现的代码
#include <bits/stdc++.h> using namespace std; #define N 305 #define M 305 int n, m; int head[N], tot, ver[M], nxt[M]; vector<int> scc[N]; int cnt;//scc的个数 int dfn[N], low[N], num; int stac[N], top; bool instac[N]; inline void add(int x, int y) { ver[++tot] = y; nxt[tot] = head[x]; head[x] = tot; } void tarjan(int x) { instac[x] = 1; stac[++top] = x; low[x] = dfn[x] = ++num; for(int i = head[x]; i; i = nxt[i]) { int y = ver[i]; if(!dfn[y]) { tarjan(y); low[x] = min(low[x], low[y]); } else if(instac[y]) { low[x] = min(low[x], dfn[y]); } } int xx; if(low[x] == dfn[x]) { cnt++; do{ xx = stac[top--]; instac[xx] = false; scc[cnt].push_back(xx); c[xx] = cnt; }while(xx != x); } } int main() { tot = 1; scanf("%d%d", &n, &m); for(int i = 1; i <= n; i++) { int x, y; scanf("%d%d", &x, &y); add(x, y); } for(int i = 1; i <= n; i++) { if(!dfn[i]) tarjan(i); } for(int i = 1; i <= cnt; i++) { printf("Case#%d:\n", i); for(int j = 0; j < scc[i].size(); i++) { printf("%d ", scc[i][j]); } puts(""); } return 0; }
AcWing367. 学校网络
一些学校连接在一个计算机网络上,学校之间存在软件支援协议,每个学校都有它应支援的学校名单(学校 A 支援学校 B,并不表示学校 B 一定要支援学校 A)。
当某校获得一个新软件时,无论是直接获得还是通过网络获得,该校都应立即将这个软件通过网络传送给它应支援的学校。
因此,一个新软件若想让所有学校都能使用,只需将其提供给一些学校即可。
现在请问最少需要将一个新软件直接提供给多少个学校,才能使软件能够通过网络被传送到所有学校?
最少需要添加几条新的支援关系,使得将一个新软件提供给任何一个学校,其他所有学校就都可以通过网络获得该软件?
输入格式
第 1 行包含整数 N,表示学校数量。
第 2..N+1 行,每行包含一个或多个整数,第 i+1 行表示学校 i 应该支援的学校名单,每行最后都有一个 0 表示名单结束(只有一个 0 即表示该学校没有需要支援的学校)。
输出格式
输出两个问题的结果,每个结果占一行。
数据范围
2≤N≤100
输入样例:
5 2 4 3 0 4 5 0 0 0 1 0
输出样例:
1 2
对于一个强连通分支,任意给一个学校就可以使得这一个强连通分支中的学校宣布拥有软件。因此可以进行缩点。在缩点之后,便得到了一个有向无环图。
零入度点记作起点,零出度点记作终点
所以可以通过tarjan求出强连通分支,然后缩点,就可以得到一张有向无环图。
对于第一问,明显需要给新出来的图的起点发放软件
对于第二问,就是要增加几条路径,使得缩点之后的有向无环图连通。
答案应该是,但是一定要注意如果对于一个点,那么就是0
结论
- 若 scc_cnt=1(只有一个强连通分量),则不需要连新的边,答案为 0。
- 若 scc_cnt>1,则答案为 max(src,des)。
证明(yxc讲解总结)
结论 1
正确性显然,下面证明结论 2。设缩点后的 DAG中,起点(入度为 0)的集合为 P,终点(出度为 0)的集合为 Q
。分以下两种情况讨论: |P|≤|Q|
① 若 |P|=1,则只有一个起点,并且这个起点能走到所有点,只要将每一个终点都向这个起点连一条边,那么对于图中任意一点,都可以到达所有点,新加的边数为 |Q|。
② 若 |P|≥2,则 |Q|≥|P|≥2,此时至少存在 2 个起点 p1,p2,2 个终点 q1,q2,满足 p1 能走到 q1,p2 能走到 q2。(反证法:如果不存在两个起点能走到不同的终点,则所有的起点一定只能走到同一个终点,而终点至少有两个,发生矛盾,假设不成立)。如下图:
那么我们可以从 q1向 p2 新连一条边,那么此时起点和终点的个数都会减少一个(p2 不再是起点,q1 不再是终点),因此只要以这种方式,连接新边 |P|−1 条,则 |P′|=1,而 |Q′|=|Q|−(|P|−1),由 ① 得,当 |P′|=1 时,需要再连 |Q′| 条新边,那么总添加的新边数量为 |P|−1+|Q|−(|P|−1)=|Q|。
|Q|≤|P|与情况 1对称,此时答案为 |P|。
综上所述,scc_cnt>1时,问题二的答案为 max(|P|,|Q|) 即 max(src,des)。
作者:番茄酱
链接:https://www.acwing.com/solution/content/4663/
来源:AcWing
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
最优性也很容易证明,对于每一个终点,必须要有一个出边(与起点相连较优),否则这一个终点就不可能到达其他的点,自然就不是强连通图。对于每一个起点,必须要有一条边(从终点出发最优),否则起点就不会被访问,所以不是强连通图。因此,最小值就是max(p,q)
代码实现
#include <bits/stdc++.h> using namespace std; #define N 105 #define M (105*105) int n; int head[N], tot, ver[M], nxt[M]; int dfn[N], low[N], num; int cnt; vector<int> scc[N]; int stac[N], top; bool instac[N]; int c[N];//相当于染色的块 int hc[N], tc, vc[M], nc[M]; int indeg[N], outdeg[N]; void tarjan(int x) { dfn[x] = low[x] = ++num; stac[++top] = x; instac[x] = true; for(int i = head[x]; i; i = nxt[i]) { int y = ver[i]; if(!dfn[y]) { tarjan(y); low[x] = min(low[x], low[y]); } else if(instac[y]) { low[x] = min(low[x], dfn[y]); } } if(dfn[x] == low[x]) { cnt++; int xx; do{ xx = stac[top--]; instac[xx] = false;//以后永远不会使用xx这样的东西,如果少一个就寄了 scc[cnt].push_back(xx); c[xx] = cnt; }while(xx != x); } } inline void add(int x, int y) { ver[++tot] = y; nxt[tot] = head[x]; head[x] = tot; } inline void add_c(int x, int y) { vc[++tc] = y; nc[tc] = hc[x]; hc[x] = tc; } int main() { tot = 1; scanf("%d", &n); for(int i = 1; i <= n; i++) { int buf; scanf("%d", &buf); while(buf) { add(i, buf); scanf("%d", &buf); } } for (int i = 1; i <= n; i++) { if(!dfn[i]) tarjan(i); } for(int x = 1; x <= n; x++) { for(int i = head[x]; i; i = nxt[i]) { int y = ver[i]; if(c[x] != c[y]){ add_c(c[x], c[y]); //outdeg[c[x]]++, indeg[c[y]]++; //其实根本没有必要建立新图,我只是练一下手 } } } if(cnt == 1) { puts("1\n0"); return 0; } for(int x = 1; x <= cnt; x++) { for(int i = hc[x]; i; i = nc[i]) { int y = vc[i]; indeg[y]++; outdeg[x]++; } } int p = 0, q = 0; for(int i = 1; i <= cnt; i++) { if(indeg[i] == 0) p ++; if(outdeg[i] == 0) q++; } printf("%d\n", p); printf("%d", max(p, q)); return 0; }
AcWing368. 银河
银河中的恒星浩如烟海,但是我们只关注那些最亮的恒星。
我们用一个正整数来表示恒星的亮度,数值越大则恒星就越亮,恒星的亮度最暗是 1。
现在对于 N 颗我们关注的恒星,有 M 对亮度之间的相对关系已经判明。
你的任务就是求出这 N 颗恒星的亮度值总和至少有多大。
输入格式
第一行给出两个整数 N 和 M。
之后 M 行,每行三个整数 T,A,B,表示一对恒星 (A,B) 之间的亮度关系。恒星的编号从 1 开始。
如果 T=1,说明 A 和 B 亮度相等。
如果 T=2,说明 A 的亮度小于 B 的亮度。
如果 T=3,说明 A 的亮度不小于 B 的亮度。
如果 T=4,说明 A 的亮度大于 B 的亮度。
如果 T=5,说明 A 的亮度不大于 B 的亮度。
输出格式
输出一个整数表示结果。
若无解,则输出 −1。
数据范围
N≤100000,M≤100000
输入样例:
5 7 1 1 2 2 3 2 4 4 1 3 4 5 5 4 5 2 3 5 4 5 1
输出样例:
11
题解
但凡是涉及到偏序,拟序关系,可以搞一个图与之对应。
-
首先,会想到差分约束系统,这样,就可以在图中方便地表示关系了。
如果使用spfa,由于所有点的亮度最小是1(需要进行初始化),并且所有的点并不一定连通(要全部添加队列),因此可以整一个“超级原点”,到所有的点都会有边,并且边的权值是1.
but有一个问题就是数据太过于大,并不可以使用spfa来进行求解。 -
可能会有01bfs,但是这个是求的最短路。
-
这个时候会想起拓扑排序,但是拓扑排序是针对有向无环图的。
在题目中有关键的一点,如果要是有一个环,那么环上的一定是相等的,如果有一个不相等,那么就不符合条件。
所以可以采用求出强连通分量,并且缩点。如果强连通分量上的边的权值不是0,那么就不合法。
然后进行拓扑排序即可
#include <bits/stdc++.h> using namespace std; int n, m; #define N 100005 int head[N], tot, ver[N*3], nxt[N*3], edge[N*3];//如果亮度相等,那么就是2M,因为要加一个原点,与所有的点连接 int dfn[N], low[N], num; int c[N]; int stac[N], top; bool instac[N]; int cnt;//新的图的数量。 int hc[N], tc, vc[N*3], nc[N*3], ec[N*3];//注意,这里也要使用N*3,虽然明面上是有n-1个边,但是由于就有重边,所以实际的边数会有更多 int d[N], indeg[N]; inline void add(int x, int y, int z) { ver[++tot] = y; edge[tot] = z; nxt[tot] = head[x]; head[x] = tot; } inline void add_c(int x, int y, int z) { vc[++tc] = y; ec[tc] = z; nc[tc] = hc[x]; hc[x] = tc; } void tarjan(int x) { stac[++top] = x; instac[x] = true; dfn[x] = low[x] = ++num; for(int i = head[x]; i; i = nxt[i]) { int y = ver[i]; if(!dfn[y]) { tarjan(y); low[x] = min(low[x], low[y]); } else if(instac[y]) { low[x] = min(low[x], dfn[y]); } } if (dfn[x] == low[x]) { cnt++; int z; do{ z = stac[top--]; instac[z] = false; c[z] = cnt; }while(z != x); } } void topu() { top = 0; stac[++top] = c[0];//仅仅有0是没有入度的点 while(top > 0) { int x = stac[top--]; for(int i = hc[x]; i; i = nc[i]) { int y = vc[i], z = ec[i]; d[y] = max(d[y], d[x] + z); if(--indeg[y] == 0) stac[++top] = y; //cout << x << " " << y << " " << z << "\n"; } } } int main() { tot = 1;//其实没有必要,只不过是怕在成对存储的时候忘记 scanf("%d%d", &n, &m); for(int i = 1; i <= m; i++) { int k, x, y; scanf("%d%d%d", &k, &x, &y); switch(k) { case 1: add(x, y, 0); add(y, x, 0); break; case 2: add(x, y, 1); break; case 3: add(y, x, 0); break; case 4: add(y, x, 1);; break; case 5: add(x, y, 0); break; } } for(int i = 1; i <= n; i++) { add(0, i, 1); } //for (int i = 0; i <= n; i++)//****************** // if (!dfn[i]) tarjan(i); tarjan(0); //既然是超级源点,那么一定是流图,所以仅仅来一次就可以了! for(int x = 0; x <= n; x++) { for(int i = head[x]; i; i = nxt[i]) { int y = ver[i]; if(c[x] == c[y]) { if(edge[i] != 0) { puts("-1"); return 0; } } else { indeg[c[y]] ++; add_c(c[x], c[y], edge[i]); //cout << edge[i] << " "; //cout << c[x] << " " << c[y] << " " << edge[i]<< "\n"; } } } topu(); long long ans = 0; for(int i = 1; i <= n; i++) ans += d[c[i]]; printf("%lld", ans); //cout << "\n" << cnt << " \n"; //for(int i = 0; i <= n; i++) cout << indeg[i] << " "; //for(int i = 0; i <= n; i++) cout << d[i] << " "; //cout << c[0] << endl; return 0; }
天大的启示:当发现自己的程序超时,也有一种可能是数组开小了,导致越界访问,造成死循环,进而导致超时。。。。。
2-SAT问题
定义:具有N个变量,取值范围仅仅是1或者0,现在有M个条件,格式为若x == 0/1
,则y == 0/1
的情况。
要求出一组解,使得满足这M个条件(可能没有解)
特殊情况下使用并查集
如果是对称的二元关系,可以使用拓展域的并查集进行解决.
对称的二元关系是指原命题以及逆命题等价。
使用图论来进行求解
对于条件,从前提取值到结论取值连接一条有向边,并且注意逆否命题也算。
这样,如果一个变量的一种取值可以推出另一种取值(返过来一定可以,因为有了逆否命题的加入),那么就是矛盾,即同一个变量的两个值有两种情况:
- 可以互相可达。
- 不可以由一种到达另一种
这个时候,仅仅需要判断所有的变量的一个值能不能推出另一个值。
但是如果暴力判断,肯定是不行的,这时候使用tarjan。
如果一个变量的两个值位于同一个强连通分支,那么就是不合法。
全部遍历完成以后如果没有冲突,那么就是合理的。
假如描述一个变量仅仅能取1,那么添加边 add(x, x+n)
,如果取0,那么导致一定取1.
这是不满足当另一个点取0还是取1都能对出x取0,那么就不满足。
方案的求解
注意:如果在一个强连通分量中,选择了一个变量(取值为0/1)的值,那么其他的变量的值均已经确定。所以可以进行缩点。
对于所有的点,按照拓扑序不断搜索零出度点进行遍历,
AcWing370. 卡图难题
有 N 个变量 X0∼XN−1,每个变量的可能取值为 0 或 1。
给定 M 个算式,每个算式形如 Xa** op** Xb=c,其中 a,b 是变量编号,c 是数字 0 或 1,op 是 AND,OR,XOR 三个位运算之一。
求是否存在对每个变量的合法赋值,使所有算式都成立。
输入格式
第一行包含两个整数 N 和 M。
接下来 M 行,每行包含三个整数 a,b,c,以及一个位运算(AND,OR,XOR 中的一个)。
输出格式
输出结果,如果存在,输出 YES
,否则输出 NO
。
数据范围
1≤N≤1000,
1≤M≤
输入样例:
4 4 0 1 1 AND 1 2 1 OR 3 2 0 AND 3 0 0 XOR
输出样例:
YES
#include <bits/stdc++.h> using namespace std; #define N 2005 #define M 4000005 int n, m; int head[N], tot, nxt[M], ver[M]; int dfn[N], low[N], num; int c[N], cnt; int stac[N], top; bool instac[N]; inline void add(int x, int y) { ver[++tot] = y; nxt[tot] = head[x]; head[x] = tot; } void tarjan(int x) { dfn[x] = low[x] = ++num; stac[++top] = x; instac[x] = true; for(int i = head[x]; i; i = nxt[i]) { int y = ver[i]; if(!dfn[y]) { tarjan(y); low[x] = min(low[x], low[y]); } else if(instac[y]) { low[x] = min(low[x], dfn[y]); } } if(low[x] == dfn[x]) { cnt++; int z; do{ z = stac[top--]; instac[z] = false; c[z] = cnt; }while(z != x); } } int main() { tot = 1; scanf("%d%d", &n, &m); for(int i = 1; i <= m; i++) { int a, b, k; char buf[12]; a++, b++; scanf("%d%d%d%s", &a, &b, &k, buf); if (buf[0] == 'A' && k == 0) { add(a+n, b); add(b+n, a); } if (buf[0] == 'A' && k == 1) { add(a, a+n); add(b, b+n); } if (buf[0] == 'O' && k == 0) { add(a+n, a); add(b+n, b); } if (buf[0] == 'O' && k == 1) { add(a, b+n); add(b, a+n); } if (buf[0] == 'X' && k == 0) { add(a, b); add(b, a); add(a+n, b+n); add(b+n, a+n); } if (buf[0] == 'X' && k == 1) { add(a, b+n); add(a+n, b); add(b+n, a); add(b, a+n); } } for(int i = 1; i <= n*2; i++) { if(!dfn[i]) tarjan(i); } for(int i = 1; i <= n; i++) { if(c[i+n] == c[i]) { puts("NO"); return 0; } } puts("YES"); return 0; }
AcWing371. 牧师约翰最忙碌的一天
牧师约翰在 9 月 1 日这天非常的忙碌。
有 N 对情侣在这天准备结婚,每对情侣都预先计划好了婚礼举办的时间,其中第 i 对情侣的婚礼从时刻 Si 开始,到时刻 Ti 结束。
婚礼有一个必须的仪式:站在牧师面前聆听上帝的祝福。
这个仪式要么在婚礼开始时举行,要么在结束时举行。
第 i 对情侣需要 Di 分钟完成这个仪式,即必须选择 Si∼Si+Di 或 Ti−Di∼Ti 两个时间段之一。
牧师想知道他能否满足每场婚礼的要求,即给每对情侣安排Si∼Si+Di 或 Ti−Di∼Ti,使得这些仪式的时间段不重叠。
若能满足,还需要帮牧师求出任意一种具体方案。
注意,约翰不能同时主持两场婚礼,且 所有婚礼的仪式均发生在 9月 1 日当天 。
如果一场仪式的结束时间与另一场仪式的开始时间相同,则不算重叠。
例如:一场仪式安排在 08:00∼09:00,另一场仪式安排在 09:00∼10:00,则不认为两场仪式出现重叠。
输入格式
第一行包含整数 N。
接下来 N 行,每行包含 Si,Ti,Di,其中 Si 和 Ti 是 hh:mm 形式。
输出格式
第一行输出能否满足,能则输出 YES
,否则输出 NO
。
接下来 N 行,每行给出一个具体时间段安排。
数据范围
1≤N≤1000
输入样例:
2 08:00 09:00 30 08:15 09:00 20
输出样例:
YES 08:00 08:30 08:40 09:00
来自《算法竞赛进阶指南》
#include <bits/stdc++.h> using namespace std; #define N 2010//因为一个变量需要建立两个图 #define M (N*N*8) int n; struct { int s, t, d; }a[1005]; int head[N], tot, ver[M], nxt[M]; int dfn[N], low[N], num; int c[N], cnt; int stac[N], top; bool instac[N]; inline void add(int x, int y) { ver[++tot] = y; nxt[tot] = head[x]; head[x] = tot; } bool conflict(int a, int b, int c, int d) { //1.如果a在[c, d]中 //2.如果b在[c, d]中 //3.如果[a, b]与[c, d]相同或者已经完全包含[c, d] return (a >= c && a < d) || (b >c && b <= d) || (a <= c && d <= b); } void tarjan(int x) { stac[++top] = x; instac[x] = true; dfn[x] = low[x] = ++num; for(int i = head[x]; i; i = nxt[i]) { int y = ver[i]; if(!dfn[y]) { tarjan(y); low[x] = min(low[x], low[y]); } else if(instac[y]) { low[x] = min(low[x], dfn[y]); } } if(dfn[x] == low[x]) { cnt++; int z; do{ z = stac[top--]; instac[z] = false; c[z] = cnt; }while(z != x); } } int main() { tot = 1; scanf("%d", &n); //读入时间 for(int i = 1; i <= n; i++) { int sh, sm, th, tm; scanf("%d:%d%d:%d%d", &sh, &sm, &th, &tm, &a[i].d); a[i].s = sh*60+sm; a[i].t = th*60+tm; } //判断冲突 for(int i = 1; i <= n; i++) { /* i的前半部分:a[i].s----a[i].s+a[i].d i的后半部分:a[i].t-a[i].d----a[i].t; */ for(int j = i+1; j <= n; j++)//如果两两冲突,最多需要添加8条边,所以最多是8*N*N { if(conflict(a[i].s, a[i].s+a[i].d, a[j].s, a[j].s+a[j].d)) { add(i, j+n), add(j, i+n); } if(conflict(a[i].s, a[i].s+a[i].d, a[j].t-a[j].d, a[j].t)) { add(i, j), add(j+n, i+n); } if(conflict(a[i].t-a[i].d, a[i].t, a[j].s, a[j].s+a[j].d)) { add(j, i), add(i+n, j+n); } if(conflict(a[i].t-a[i].d, a[i].t, a[j].t-a[j].d, a[j].t)) { add(i+n, j), add(j+n, i); } } } for(int i = 1; i <= 2*n; i++) { if(!dfn[i]) tarjan(i); } for(int i = 1; i <= n; i++) { if(c[i] == c[i+n]) { puts("NO"); return 0; } } puts("YES"); for(int i = 1; i <= n; i++) { if(c[i] < c[i+n]) { int l = a[i].s; int r = a[i].s+a[i].d; printf("%02d:%02d %02d:%02d\n", l/60, l%60, r/60, r%60); } else { int l = a[i].t-a[i].d; int r = a[i].t; printf("%02d:%02d %02d:%02d\n", l/60, l%60, r/60, r%60); } } return 0; }
本文来自博客园,作者:心坚石穿,转载请注明原文链接:https://www.cnblogs.com/xjsc01/p/16616728.html
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 地球OL攻略 —— 某应届生求职总结
· 周边上新:园子的第一款马克杯温暖上架
· Open-Sora 2.0 重磅开源!
· 提示词工程——AI应用必不可少的技术
· .NET周刊【3月第1期 2025-03-02】