比较重要的图论,tg 中考得比较多。
1. 定义
连通分量:可以互相到达的点集
强连通分量:极大的连通分量(即不可扩展的)
常见应用:缩点,可将有向图变为有向无环图( DAG ),无向图变为树 / 森林。
2. 求强连通分量( SCC ): DFS+Tarjan 注意,下面的定义是属于有向图的,无向图等一下再讨论!
1) DFS 的特殊定义 边可分为4类:
树枝边:搜索时所经历的边(树枝边构成树)
前向边:祖先指向儿子
后向边:儿子指向祖先
横叉边:不属于上面3种的边 ( 大雾 )
(黑色为树边,蓝色为前向边,红色为橫叉边,绿色为后向边)
2) 哪些边有用?
后向边:祖先先到儿子,儿子回到了祖先(一定是)
横叉边:点先到另外点,在有横叉边的话可能回到了祖先
3) Tarjan 算法 a. 又是定义( 雾 )
时间戳( dfn ):遍历到 u 的时间点
low :以 u 为根的子树,只走一条边能到达的 dfn 的最小值
u 是一个强连通分量 dfn 的最小值,等价于
b. 算法流程
保存一个栈,是搜到的点
搜索所有相邻的点,如果未搜过,就递归搜索,如果搜过(即不是树枝边),就有
如果 ,将栈里的点一一取出,合并为一个 SCC,直到取出自己
复杂度
c.证明:略(过于复杂) 3. 后续事务 1) 缩点 遍历每一条原图中的边,如果端点不在同一分量中,就将该两个 SCC 连起来
2) 在 DAG 上拓扑排序 其实可以不用,因为 SCC 的顺序就是拓扑序的倒序
证明: 拓扑序的第一个,一定会在搜的时候搜到其他的强连通分量,并在该 SCC 前就已经结束,第一个一定是最后搜到的 SCC
4. Code 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 void Tarjan (int x) { dfn[x]=low[x]=++tot; st[++top]=x, ins[x]=true ; for (int i=h[x];~i;i=ne[i]) if (!dfn[e[i]]) { Tarjan (e[i]); low[x]=min (low[x],low[e[i]]); } else if (ins[e[i]]) low[x] = min (low[x], dfn[e[i]]); if (low[x]!=dfn[x]) return ; cnt++;int now; do { now=st[top--]; ins[now]=false ; bel[now]=cnt; }while (now!=x); return ; } void suodian () { for (int x=1 ;x<=n;++x) for (int i=h[x];~i;i=ne[i]) if (bel[x]!=bel[e[i]]) add (h2, bel[x], bel[e[i]]); n=cnt; }
5. 无向图 既和有向图类似,有和有向图有区别。
首先,边的分类只有 3 种:树边,前向边,后向边。
为什么没有横叉边呢?其实原因很简单,首先,横叉边是指不同子树之间的边。试想,如果有横叉边的话,当我们走到先遍历的点 时,另一个子树还没有遍历,那 一定会走到 ,让横叉边变为了树边了。
注意,这个代码还应该传一个 ,,防止又走回 而导致死循环了。
代码(找不到了……
6. 点双联通分量与边双连通分量 这两个有区别,一般用于无向图。
1)定义
点双连通分量:不存在割点,也就是任意去掉一个点,原图仍然联通。
边双连通分量:不存在桥,也就是任意去掉一条边,原图仍然联通。
还要区分的是,点双连通分量与边双连通分量是互不包含的,这个有些书上可能是讲错了。
2)判定
割点:存在一个点 ,使得 (dfs 树的根节点需要两个 ),那么 是割点。
桥:存在一条边 ,使得 ,那么 是桥。
注意两个的判定区别。
3)简要证明
割点:因为 所在的子树不可能到达 以上的节点了,所以每一个节点向上遍历的话都要经过 ,所以 删去后会使原图不联通。但如果是根节点的话,没有向上的点了,所以需要多一个 ,那么删去根节点后就不联通了。
桥: 所在的子树不可能到达 及以上的节点了,所以删去 会使 的子树与其他不联通, 就是桥了。
左边的图中,红色的边下面的点 ,,那么红边就是桥,但 ,所以蓝边 不是桥。在右边的图中, 下面的点 一定满足 ,所以 就是割点。而 下面的点会有 ,所以 就不是割点。
4)缩点 a. 边双连通分量 这个要简单一些,所以先讲(?
根据定义,不存在桥的子图就是边联通图,而极大的边联通图就是边双连通分量。
(其实和前面的 Tarjan 差不多)(废话,前面的无向图 Tarjan 就是边双连通分量)
b. 点双连通分量 这个其实要难一些 很多
首先,我们将所有的割点处理出来。
如果我们得到 ,我们就将当前栈内的所有点一直弹出,直到弹出的点为 ,将所有弹出的点都加入一个新的点双连通分量,然后还要将 入栈。
如果要缩为一个新图的话,我们还要注意:每一个割点都是一个新图中的点,然后每一个点双连通分量也是新图中的一个点,连接就是割点向有它的点双连通分量连边。原图最后变为了一棵树(森林)。
缩完点后,就可以树形 DP 了。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 int h1[N], h2[N], e[M], ne[M], w[M], idx;int low[N], dfn[N], tot, stk[N], top, cnt;vector <int > dcc[N]; void add (int *h, int a, int b) { e[idx] = b, ne[idx] = h[a], h[a] = idx ++; } void tarjan (int x, int from) { low[x] = dfn[x] = ++ tot, stk[++ top] = x; for (int i = h[x]; ~i; i = ne[i]) { if (i == (from ^ 1 )) continue ; if (!dfn[e[i]]) { tarjan (e[i], i); if (low[e[i]] <= dfn[x]) { int now;cnt ++; while (stk[top] != x) { now = stk[top --]; dcc[cnt].push_back (now); } dcc[cnt].push_back (x); add (h2, x, cnt + n); } low[x] = min (low[x], low[e[i]]); } else low[x] = min (low[x], dfn[e[i]]); } }
7. 例题 T1: Luogu P2341 受欢迎的牛 题目传送门
如果是一个 DAG 的话,直接是出度为 0 的点就是答案
但是,如果有多个点的话,那么哪个点都不是。
考虑 Tarjan,缩点后出度为 0 的点的大小就是了。
Code:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 #include <iostream> #include <cstring> #include <cstdio> #include <algorithm> using namespace std;const int N=1e4 +10 ,M=5e4 +10 ;int h[N],e[M],ne[M],idx;int st[N],top,dfn[N],low[N],bel[N],cnt,tot,dout[N],din[N],n,m,s[N];bool ins[N];void add (int a,int b) { e[idx]=b,ne[idx]=h[a],h[a]=idx++; } void Tarjan (int x) { dfn[x]=low[x]=++tot; st[++top]=x, ins[x]=true ; for (int i=h[x];~i;i=ne[i]) if (!dfn[e[i]]) { Tarjan (e[i]); low[x]=min (low[x],low[e[i]]); } else if (ins[e[i]]) low[x] = min (low[x], dfn[e[i]]); if (low[x]!=dfn[x]) return ; cnt++;int now; do { now=st[top--]; ins[now]=0 ; bel[now]=cnt; s[cnt]++; }while (now!=x); return ; } int main () { memset (h,-1 ,sizeof h); cin>>n>>m; int x,y; while (m--) { scanf ("%d %d" ,&x,&y); add (x,y); } for (int i=1 ;i<=n;++i) if (!dfn[i]) Tarjan (i); for (int x=1 ;x<=n;++x) { for (int i=h[x];~i;i=ne[i]) if (bel[x]!=bel[e[i]]) { dout[bel[x]]++; din[bel[e[i]]]++; } } int b=0 ; for (int i=1 ;i<=cnt;++i) { if (!dout[i]) { if (b) { puts ("0" ); return 0 ; } b=s[i]; } } cout<<b<<endl; return 0 ; }
T2: AcWing 367 学校网络 / Luogu P2812 校园网络(加强版) 题目传送门_AcWing
题目传送门_Luogu
对于第一问,缩点后,入度点的个数即为答案(这些点必须得到,得到后,所有也都会得到)
对于第二问,设入度为0的点有 个,出度为0的点有 个,则答案为
证明有很多有些问题,这篇博客证明了(我也不知道有没有问题 雾 )
证明
Code:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 #include <iostream> #include <cstring> #include <cstdio> #include <algorithm> using namespace std;const int N=1e4 +10 ,M=5e6 +10 ;int h[N],e[M],ne[M],idx;int st[N],top,dfn[N],low[N],bel[N],cnt,tot,dout[N],din[N],n;bool ins[N];void add (int a,int b) { e[idx]=b,ne[idx]=h[a],h[a]=idx++; } void Tarjan (int x) { dfn[x]=low[x]=++tot; st[++top]=x, ins[x]=true ; for (int i=h[x];~i;i=ne[i]) if (!dfn[e[i]]) { Tarjan (e[i]); low[x]=min (low[x],low[e[i]]); } else if (ins[e[i]]) low[x] = min (low[x], dfn[e[i]]); if (low[x]!=dfn[x]) return ; cnt++;int now; do { now=st[top--]; ins[now]=0 ; bel[now]=cnt; }while (now!=x); return ; } int main () { memset (h,-1 ,sizeof h); cin>>n; int x; for (int i=1 ;i<=n;++i) { while (cin>>x,x) add (i,x); } for (int i=1 ;i<=n;++i) if (!dfn[i]) Tarjan (i); for (int x=1 ;x<=n;++x) { for (int i=h[x];~i;i=ne[i]) if (bel[x]!=bel[e[i]]) { dout[bel[x]]++; din[bel[e[i]]]++; } } int a=0 ,b=0 ; for (int i=1 ;i<=cnt;++i) { if (!dout[i]) a++; if (!din[i]) b++; } cout<<b<<endl; if (cnt==1 ) puts ("0" ); else cout<<max (a,b)<<endl; return 0 ; }
Gitalking ...