Tarjan算法各种&RMQ& POJ 3694
关于tarjan 的思想可以在网上搜到,具体我也不太清楚,应该说自己理解也不深,下面是做题经验得到的一些模板。
其中有很多转载,包括BYVoid等,感谢让我转。。。望各路大神愿谅
有向图求连通分量的一般方法:
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
1 void Tarjan(u) { 2 dfn[u]=low[u]=++index 3 stack.push(u) 4 for each (u, v) in E { 5 if (v is not visted) { 6 tarjan(v) 7 low[u] = min(low[u], low[v]) 8 } 9 else if (v in stack) { //可设置stack数组 10 low[u] = min(low[u], dfn[v]) 11 } 12 } 13 if (dfn[u] == low[u]) { //u是一个强连通分量的根 14 repeat 15 v = stack.pop 16 print v 17 until (u== v) 18 } //退栈,把整个强连通分量都弹出来 19 //此时可为所弹出的联通分量标号,便于0重建图 20 } //复杂度是O(E+V)的
无相连通图割点满足条件 ( 重边对割点不影响 ,这很重要)
一个顶点u是割点,当且仅当满足(1)或(2)
(1) u为树根,且u有多于一个子树。
(2) u不为树根,且满足存在(u,v)为树枝边(或称父子边,即u为v在搜索树中的父亲),使得dfn(u)<=low(v)。
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
1 void tarjan(int x){ 2 v[x]=1; 3 dfn[x]=low[x]=++num; 4 for(int i=head[x];i;i=next[i]) 5 if(!v[ver[i]]){ 6 tarjan(ver[i]); 7 low[x]=min(low[x],low[ver[i]]); 8 if(dfn[x]<=low[ver[i]]) 9 v[x]++; 10 } 11 else 12 low[x]=min(low[x],dfn[ver[i]]); 13 if((x==root&&v[x]>2)||(x!=root&&v[x]>1)) 14 v[x]=2; 15 else 16 v[x]=1;//v[x]=2表示该点为割点,注意其中第一个点要特判 17 }
void tarjan(int u,int ufa) { int rootson=0;//根节点的儿子节点数目,如果大于1,则根节点是割点 low[u]=dfn[u]=++num;//标记次序的节点编号一定要从1开始 for(int i=head[u]; i!=-1; i=e[i].next) { int j=e[i].to; if(!dfn[j]) { if(u==root)//根的情况要特殊处理,j不可能等于root,j是u的子节点 { if(++rootson>1) ans[u]=true; } tarjan(j,u);//u是j的父节点 low[u]=min(low[j],low[u]);//low值是更新当前的父节点,利用儿子节点的low来更新 if(u!=root&&dfn[u]<=low[j])//根的情况已经讨论,避免重复 ans[u]=true;//都是对目前传入的父亲节点进行讨论,不能置其子节点为true. } else if(j!=ufa) low[u]=min(low[u],dfn[j]); } }
无相连通图(可能存在重边的情况下)桥
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
1 void tarjan(int u){ 2 dfn[u]=low[u]=++clock; 3 for(int e=head[u];e!=-1;e=edge[e].next){ 4 int v=edge[e].v; 5 if(dfn[v]==-1){ 6 vis_e[e]=vis_e[e^1]=true; 7 tarjan(v); 8 low[u]=min(low[u],low[v]); 9 if(dfn[u] >=low[v]){ 10 // Union(u,v); 两点在同一个分量中,合并可得边双连通图 11 } 12 else{ 13 is_bridge[e] = is_bridge[e^1] = true; 14 } 15 } 16 else if(dfn[v] < dfn[u] && !vis_e[e]){ 17 vis_e[e] = vis_e[e^1] = true; 18 low[u]=min(low[u],dfn[v]); 19 } 20 } 21 }
把求桥和求割点合并在一个程序中(不考虑重边)
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
1 void Tarjan(int u, int father){ //father 是u的父节点 2 Father[u] = father; 3 int i,j,k; 4 low[u] = dfn[u] = nTime ++; 5 for( i = 0;i < G[u].size() ;i ++ ) { 6 int v = G[u][i]; 7 if( ! dfn[v]) { 8 Tarjan(v,u); 9 low[u] = min(low[u],low[v]); 10 } 11 else if( father != v ) //连到父节点的回边不考虑,否则求不出桥 12 low[u] = min(low[u],dfn[v]); 13 } 14 } 15 void Count(){ //计算割点和桥 16 int nRootSons = 0; int i; 17 Tarjan(1,0); 18 for( i = 2;i <= n;i ++ ) { 19 int v = Father[i]; 20 if( v == 1 ) 21 nRootSons ++; //DFS树中根节点有几个子树 22 else { 23 if( dfn[v] <= low[i]) //判断割点条件 24 bIsCutVetext[v] = true; 25 } 26 } 27 if( nRootSons > 1) //根结点至少有两个子树 28 bIsCutVetext[1] = true; 29 for( i = 1;i <= n;i ++ ) 30 if( bIsCutVetext[i] ) //割点 31 cout << i << endl; 32 for( i = 1;i <= n;i ++) { 33 int v = Father[i]; 34 if(v >0 && dfn[v] < low[i]) // 割边 35 cout << v << "," << i <<endl; 36 } 37 }
求点双连通实际上是在求割点时得到的。(无重边)
对于点双连通分支,实际上在求割点的过程中就能顺便把每个点双连通分支求出。建立一个栈,存储当前双连通分支,在搜索图时,每找到一条树枝边或反向边,就把这条边加入栈中。如果遇到某时满足dfn(u)<=low(v),说明u是一个割点,同时把边从栈顶一个个取出,直到遇到了边(u,v),取出的这些边与其关联的点,组成一个点双连通分支。割点可以属于多个点双连通分支,其余点和每条边只属于且属于一个点双连通分支。
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
1 void Tarjan(int u, int father){ 2 int i,j,k; 3 low[u] = dfn[u] = nTime ++; 4 for( i = 0;i < G[u].size() ;i ++ ) { 5 int v = G[u][i]; 6 if( ! dfn[v]) { //v没有访问过 7 //树边要入栈 8 Edges.push_back(Edge2(u,v)); 9 Tarjan(v,u); 10 low[u] = min(low[u],low[v]); 11 Edge2 tmp(0,0); 12 if(dfn[u] <= low[v]) { 13 //从一条边往下走,走完后发现自己是割点,则栈中的边一定全是和自己在一个双连通分量里面 14 //根节点总是和其下的某些点在同一个双连通分量里面 15 cout << "Block No: " << ++ nBlockNo << endl; 16 do { 17 tmp = Edges.back(); 18 Edges.pop_back (); 19 cout << tmp.u << "," << tmp.v << endl; 20 }while ( !(tmp.u == u && tmp.v == v) ); 21 } 22 } // 对应if( ! dfn[v]) { 23 else { 24 if( v != father ) {//u连到父节点的回边不考虑 25 low[u] = min(low[u],dfn[v]); 26 if( dfn[u] > dfn[v]) 27 //子孙连接到祖先的回边要入栈,但是子孙连接到自己的边,此处肯定已经入过栈了,不能再入栈 28 Edges.push_back(Edge2(u,v)); 29 } 30 } 31 } //对应 for( i = 0;i < G[u].size() ;i ++ ) { 32 }
有重边
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
1 void Tarjan(int u, int father){ 2 2 int i,j,k; 3 3 low[u] = dfn[u] = nTime ++; 4 4 for( each (u,v) ){ 5 6 if( ! dfn[v]) { //v没有访问过 6 7 //树边要入栈 7 vis[e]=vis[e^1]=true; 8 8 Edges.push_back(Edge2(u,v)); 9 9 Tarjan(v,u); 10 10 low[u] = min(low[u],low[v]); 11 11 Edge2 tmp(0,0); 12 12 if(dfn[u] <= low[v]) { 13 13 //从一条边往下走,走完后发现自己是割点,则栈中的边一定全是和自己在一个双连通分量里面 14 14 //根节点总是和其下的某些点在同一个双连通分量里面 15 15 cout << "Block No: " << ++ nBlockNo << endl; 16 16 do { 17 17 tmp = Edges.back(); 18 18 Edges.pop_back (); 19 19 cout << tmp.u << "," << tmp.v << endl; 20 20 }while ( !(tmp.u == u && tmp.v == v) ); 21 //还应注意,一条边也是点双联通 22 21 } 23 22 } // 对应if( ! dfn[v]) { 24 23 else { 25 24 if( !vis[e] ) {//u连到父节点的回边不考虑 26 25 low[u] = min(low[u],dfn[v]); 27 26 if( dfn[u] > dfn[v]) 28 27 //子孙连接到祖先的回边要入栈,但是子孙连接到自己的边,此处肯定已经入过栈了,不能再入栈 29 28 Edges.push_back(Edge2(u,v)); 30 29 } 31 30 } 32 31 } //对应 for( i = 0;i < G[u].size() ;i ++ ) { 33 32 }
一个有桥的连通图,如何把它通过加边变成边双连通图?
方法为首先求出所有的桥,然后删除这些桥边,剩下的每个连通块都是一个双连通子图。把每个双连通子图收缩为一个顶点,再把桥边加回来,最后的这个图一定是一棵树,边连通度为1。
统计出树中度为1的节点的个数,即为叶节点的个数,记为leaf。则至少在树上添加(leaf+1)/2条边,就能使树达到边二连通,所以至少添加的边数就是(leaf+1)/2。具体方法为,首先把两个最近公共祖先最远的两个叶节点之间连接一条边,这样可以把这两个点到祖先的路径上所有点收缩到一起,因为一个形成的环一定是双连通的。然后再找两个最近公共祖先最远的两个叶节点,这样一对一对找完,恰好是(leaf+1)/2次,把所有点收缩到了一起。
LCA问题
LCA
Tarjan作为离线off-line算法,在程序开始前,需要将所有等待询问的节点对提前存储,然后程序从树根开始执行TarjanLCA()。假如有如下一棵多叉树
根据TarjanLCA的实现算法可以看出,只有当某一棵子树全部遍历处理完成后,才将该子树的根节点标记为黑色(初始化是白色),假设程序按上面的树形结构进行遍历,首先从节点1开始,然后递归处理根为2的子树,当子树2处理完毕后,节点2, 5, 6均为黑色;接着要回溯处理3子树,首先被染黑的是节点7(因为节点7作为叶子不用深搜,直接处理),接着节点7就会查看所有询问(7, x)的节点对,假如存在(7, 5),因为节点5已经被染黑,所以就可以断定(7, 5)的最近公共祖先就是find(5).ancestor,即节点1(因为2子树处理完毕后,子树2和节点1进行了union,find(5)返回了合并后的树的根1,此时树根的ancestor的值就是1)。 有人会问如果没有(7, 5),而是有(5, 7)询问对怎么处理呢?我们可以在程序初始化的时候做个技巧,将询问对(a, b)和(b, a)全部存储,这样就能保证完整性。
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
1 function TarjanLCA(u) 2 MakeSet(u); 3 u.ancestor := u; 4 for each v in u.children do 5 TarjanLCA(v); 6 Union(u,v); 7 Find(v).ancestor := u; //把v的父亲置为u 8 u.colour := black; 9 for each v such that {u,v} in P do // P 存储所有等待询问的节点对 10 if v.colour == black 11 print "Tarjan's Least Common Ancestor of " + u + 12 " and " + v + " is " + Find(v).ancestor + "." //此处一定是v
RMQ
LCA向RMQ转化
对有根树T进行DFS,将遍历到的结点按照顺序记下,我们将得到一个长度为2N – 1的序列,称之为T的欧拉序列F
每个结点都在欧拉序列中出现,我们记录结点u在欧拉序列中第一次出现的位置为pos(u)
此处要先介绍人ST算法:
ST算法是使用预处理来获得效率的。
预处理:
预处理使用DP的思想,f(i, j)表示[i, i+2^j - 1]区间中的最小值,我们可以开辟一个数组专门来保存f(i, j)的值。
例如,f(0, 0)表示[0,0]之间的最小值,就是num[0], f(0, 2)表示[0, 3]之间的最小值, f(2, 4)表示[2, 17]之间的最小值
注意, 因为f(i, j)可以由f(i, j - 1)和f(i+2^(j-1), j-1)导出, 而递推的初值(所有的f(i, 0) = i)都是已知的
所以我们可以采用自底向上的算法递推地给出所有符合条件的f(i, j)的值。
查询:
假设要查询从m到n这一段的最小值, 那么我们先求出一个最大的k, 使得k满足2^k <= (n - m + 1).
于是我们就可以把[m, n]分成两个(部分重叠的)长度为2^k的区间: [m, m+2^k-1], [n-2^k+1, n];
而我们之前已经求出了f(m, k)为[m, m+2^k-1]的最小值, f(n-2^k+1, k)为[n-2^k+1, n]的最小值
我们只要返回其中更小的那个, 就是我们想要的答案, 这个算法的时间复杂度是O(1)的.
例如, rmq(0, 11) = min(f(0, 3), f(4, 3))
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
1 void st(int n){ 2 int i, j, k, m; 3 k = (int) (log((double)n) / log(2.0)); 4 for(i = 0; i < n; i++) { 5 f1[i][0] = num[i]; //递推的初值 6 f2[i][0] = num[i]; 7 } 8 for(j = 1; j <= k; j++) { //自底向上递推 9 for(i = 0; i + (1 << j) - 1 < n; i++) { 10 m = i + (1 << (j - 1)); //求出中间的那个值 11 f1[i][j] = mmax(f1[i][j-1], f1[m][j-1]); 12 f2[i][j] = mmin(f2[i][j-1], f2[m][j-1]); 13 } 14 } 15 }
查询
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
1 void rmq(int i, int j) { 2 int k = (int)(log(double(j-i+1)) / log(2.0)), t1, t2; //用对2去对数的方法求出k 3 t1 = mmax(f1[i][k], f1[j - (1<<k) + 1][k]); 4 t2 = mmin(f2[i][k], f2[j - (1<<k) + 1][k]); 5 printf("%d\n",t1 - t2); 6 }
LCA转RMQ中
则LCA中求最近公共祖先就是在最先出现的位置区间内求
求该深度序列深度最小值所对应的欧拉序列的点
//这个算法是利用了RMQ的思想,DP记录的是某段区间最小值的位置
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
1 void st(){ 2 int i, j, k, m; 3 for(i=1;i<=time;i++) 4 dp[i][0]=i; 5 for(j=1;(1<<j)<=time;j++) 6 { 7 for(i=1;i+(1<<j)-1<=time;i++) 8 { 9 if(depth[dp[i][j-1]]<depth[dp[i+(1<<(j-1))][j-1]]) 10 dp[i][j]=dp[i][j-1]; 11 else 12 dp[i][j]=dp[i+(1<<(j-1))][j-1]; 13 } 14 } 15 } 16 //depth是深度序列 17 int rmq(int i, int j) { 18 int k = (int)(log(double(j-i+1)) / log(2.0)); 19 return k; 20 } 21 //squ是欧拉序列 22 int find(int s,int x){ 23 int t1=first[s],t2=first[x]; // first是首次出现的位置 24 if(t1>t2){ 25 int tmp=t1; 26 t1=t2; 27 t2=tmp; 28 } 29 int t=rmq(t1,t2); int ancestor; 30 if(depth[dp[t1][t]]<depth[dp[t2-(1<<t)+1][t]]) 31 ancestor=squ[dp[t1][t]]; 32 else 33 ancestor=squ[dp[t2-(1<<t)+1][t]]; 34 return ancestor; 35 }
POJ 3694
求出所有的桥后缩点,则变成树。树的每一条边均为桥。
那么,要求增加一条边后还剩多少条桥,可知,两点到其LCA的路径均不成为桥。
但此处求LCA时有一个小技巧,因为要统计数目。所以,可能通过减少深度的操作完成。先把两点提至同一深度,然后再两点同时向根回溯,每经过边则减少一条桥。
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
1 #include <iostream> 2 #include <cstdio> 3 using namespace std; 4 const int MAXN=100010; 5 const int MAXM=200100; 6 7 struct { 8 int u,v; 9 int next; 10 }edge[MAXM*2],tree[MAXM*2]; 11 int tot,n,m,clock,k; 12 int head[MAXN],dfn[MAXN],low[MAXN],pre[MAXN],rank[MAXN]; 13 bool vis_e[MAXM*2],is_bridge[MAXM*2],visT[MAXN]; 14 int depth[MAXN],p[MAXN],head_T[MAXN]; 15 void addedge(int u,int v){ 16 vis_e[tot]=is_bridge[tot]=false; 17 edge[tot].u=u; 18 edge[tot].v=v; 19 edge[tot].next=head[u]; 20 head[u]=tot++; 21 vis_e[tot]=is_bridge[tot]=false; 22 edge[tot].u=v; 23 edge[tot].v=u; 24 edge[tot].next=head[v]; 25 head[v]=tot++; 26 } 27 28 void addTree(int u,int v){ 29 //vis_e[tot]=false; 30 tree[tot].u=u; 31 tree[tot].v=v; 32 tree[tot].next=head_T[u]; 33 head_T[u]=tot++; 34 // vis_e[tot]=false; 35 tree[tot].u=v; 36 tree[tot].v=u; 37 tree[tot].next=head_T[v]; 38 head_T[v]=tot++; 39 } 40 41 int find(int x){ 42 int r=x; 43 while(pre[r]!=-1) 44 r=pre[r]; 45 while(pre[x]!=-1){ 46 int tx=pre[x]; 47 pre[x]=r; 48 x=tx; 49 } 50 return r; 51 } 52 int min(int a,int b){ 53 if(a<b)return a; 54 return b; 55 } 56 57 void Union(int x,int y){ 58 int xx=find(x); 59 int yy=find(y); 60 if(rank[xx]>rank[yy]) 61 pre[yy]=xx; 62 else if(rank[xx]<rank[yy]) 63 pre[xx]=yy; 64 else { 65 pre[xx]=yy; 66 rank[yy]++; 67 } 68 } 69 70 void tarjan(int u){ 71 dfn[u]=low[u]=++clock; 72 for(int e=head[u];e!=-1;e=edge[e].next){ 73 int v=edge[e].v; 74 if(dfn[v]==-1){ 75 vis_e[e]=vis_e[e^1]=true; 76 tarjan(v); 77 low[u]=min(low[u],low[v]); 78 if(dfn[u] >=low[v]){ 79 // cout<<u<<' '<<v<<endl; 80 Union(u,v); 81 } 82 else{ 83 is_bridge[e] = is_bridge[e^1] = true; 84 // cout<<u<<' '<<v<<endl; 85 } 86 } 87 else if(dfn[v] < dfn[u] && !vis_e[e]){ 88 vis_e[e] = vis_e[e^1] = true; 89 low[u]=min(low[u],dfn[v]); 90 } 91 } 92 } 93 94 int count; 95 void dfs(int r,int c){ 96 int i,j,u,v; 97 depth[r]=c; 98 visT[r]=true; 99 count++; 100 for(i=head_T[r];i!=-1;i=tree[i].next){ 101 v=tree[i].v; 102 //cout<<v<<endl; 103 if(!visT[v]){ 104 // cout<<v<<endl; 105 p[v]=r; 106 dfs(v,c+1); 107 } 108 } 109 visT[r]=false; 110 } 111 112 void lca(int x,int y){ 113 if(depth[x]>depth[y]){ 114 while(depth[x]!=depth[y]){ 115 if(!visT[x]){ 116 visT[x]=true; 117 count--; 118 } 119 x=p[x]; 120 } 121 } 122 if(depth[x]<depth[y]){ 123 while(depth[x]!=depth[y]){ 124 if(!visT[y]){ 125 visT[y]=true; 126 count--; 127 } 128 y=p[y]; 129 } 130 } 131 if(depth[x]==depth[y]){ 132 while(x!=y){ 133 if(!visT[x]){ 134 visT[x]=true; 135 count--; 136 } 137 x=p[x]; 138 if(!visT[y]){ 139 visT[y]=true; 140 count--; 141 } 142 y=p[y]; 143 } 144 } 145 } 146 147 int main(){ 148 int u,v,j,i; int tt=0; 149 while(scanf("%d%d",&n,&m)!=EOF){ 150 tt++; 151 if(!n&&!m) break; 152 for(i=1;i<=n;i++){ 153 head[i]=pre[i]=head_T[i]=-1; 154 dfn[i]=low[i]=-1; 155 rank[i]=0; 156 p[i]=-1; 157 } 158 tot=0; clock=0; 159 for(i=1;i<=m;i++){ 160 scanf("%d%d",&u,&v); 161 addedge(u,v); 162 } 163 tarjan(1); 164 // cout<<"YES"<<endl; 165 // system("pause"); 166 tot=0; 167 int root; 168 for(i=1;i<=n;i++){ 169 visT[i]=false; 170 for(j=head[i];j!=-1;j=edge[j].next){ 171 v=edge[j].v; 172 if(is_bridge[j]){ 173 int x=find(i); 174 int y=find(v); 175 // cout<<x<<' '<<y<<endl; 176 root=x; 177 addTree(x,y); 178 is_bridge[j]=is_bridge[j^1]=false; 179 } 180 } 181 } 182 // cout<<"YES"<<endl; 183 count=0; 184 dfs(root,0); count--; 185 // cout<<"YES"<<endl; 186 scanf("%d",&k); 187 printf("Case %d:\n",tt); 188 while(k--){ 189 scanf("%d%d",&u,&v); 190 int x=find(u); 191 int y=find(v); 192 if(x==y||(visT[x]&&visT[y])) 193 printf("%d\n",count); 194 else{ 195 lca(x,y); 196 printf("%d\n",count); 197 } 198 } 199 printf("\n"); 200 } 201 return 0; 202 }