Tarjan 基础
dfn[i]: 在dfs中该节点被搜索的次序(时间戳)。
low[i]: 为i或i的子树能够追溯到的最早的栈中节点的次序号。
当 dfn[i] == low[i] 时,为i或i的子树可以构成一个强连通分量。
void tarjan(int x) { id++; dfn[x] = id; low[x] = id; vis[x] = 1;//是否在栈中 stk[++top] = x;//入栈 for(int i = head[x]; i != 0; i = edge[i].nxt){ int temp = edge[i].to; if(!dfn[temp]){ tarjan(temp); low[x] = min(low[x],low[temp]); } else if(vis[temp]){ low[x] = min(low[x],dfn[temp]); } } if(dfn[x] == low[x]){//构成强连通分量,进行染色 vis[x] = 0; color[x] = ++col; while(stk[top] != x){ color[stk[top]] = col; vis[stk[top--]] = 0; } top--; } }
割边、割点
一、基本概念
桥:无向连通图中,如果删除某边后,图变成不连通,则称该边为桥。
割点:无向连通图中,如果删除某点后,图变成不连通,则称该点为割点。
二、Tarjan算法求解桥和割点
1.割点:1)当前节点为树根的时候,条件是要有至少两颗子树。
2)当前节点u不是树根的时候,条件是存在u的一个子节点v使得 low[v]>=dfn[u]。
2.桥:当且仅当无向边(u,v)是树枝边的时候,条件是 dfn[u]<low[v]。
#include<bits/stdc++.h> using namespace std; const int N = 201; vector<int>G[N]; int n,m,low[N],dfn[N]; bool is_cut[N]; int father[N],tim; void input() { scanf("%d%d",&n,&m); int a,b; for(int i=1;i<=m;++i) { scanf("%d%d",&a,&b); G[a].push_back(b); G[b].push_back(a); } } void Tarjan(int i,int Father) { father[i]=Father; dfn[i]=low[i]=tim++; for(int j=0;j<G[i].size();++j) { int k=G[i][j]; if(dfn[k]==-1) { Tarjan(k,i); low[i]=min(low[i],low[k]); } else if(Father!=k)/*假如k是i的父亲的话,那么这就是无向边中的重边,有重边那么一定不是桥*/ low[i]=min(low[i],dfn[k]); } } void _count() { int rootson=0; Tarjan(1,0); for(int i=2;i<=n;++i) { int v=father[i]; if(v==1) rootson++; else{ if(low[i]>=dfn[v])/*割点的条件*/ is_cut[v]=true; } } if(rootson>1) is_cut[1]=true; for(int i=1;i<=n;++i) if(is_cut[i]) printf("%d ",i); for(int i=1;i<=n;++i) { int v=father[i]; if(v>0&&low[i]>dfn[v])/*桥的条件*/ printf("%d,%d ",v,i); } } int main() { input(); memset(dfn,-1,sizeof(dfn)); memset(father,0,sizeof(father)); memset(low,-1,sizeof(low)); memset(is_cut,false,sizeof(is_cut)); _count(); return 0; }
有向图缩点
思想:将一个有向图强连通分量缩点为一个点去代替一堆点,要修改两个属性,一个是边,一个是点。
方法:运用Tarjan算法找出一个强连通分量,每次找出一个强连通分量,我们就用其中的一个点去代表这一堆点。记这个点为代表点( ̄□ ̄||只是自己这么叫),那么这堆点的contract[x] = 代表点。
对于非代表点:
①、将它的出边全部复制给代表点。
②、将它的点权加给代表点。
③、所有指向它的点在使用时用指向 contract[x] 代替即可,不需要做修改。
#include<bits/stdc++.h> using namespace std; typedef long long ll; const int maxn = 200005; vector<int> e[maxn]; int ins[maxn], dfn[maxn], low[maxn], contract[maxn]; ll w[maxn]; int ind; stack<int> s; void tarjan(int u) { dfn[u] = low[u] = ++ind; ins[u] = 1; s.push(u); for(int i = 0; i < e[u].size(); i++) { int v = e[u][i]; if(!dfn[v]) { tarjan(v); low[u] = min(low[u], low[v]); } else if(ins[v]) low[u] = min(low[u], dfn[v]); } if(dfn[u] == low[u]) { int v; do { v = s.top(); s.pop(); ins[v] = 0; contract[v] = u; if(u != v) { w[u] += w[v]; while(!e[v].empty()) { e[u].push_back(e[v].back()); e[v].pop_back(); } } } while(u != v); } } ll dfs(int u, ll cnt) { cnt += w[u]; ll ret = cnt; for(int i = 0; i < e[u].size(); i++) { int v = contract[e[u][i]]; if(v != u) ret = max(ret, dfs(v, cnt)); } return ret; } int main() { int n, m; scanf("%d%d",&n,&m); for(int i = 1; i <= n; i++) { scanf("%d",&w[i]); } for(int i = 1; i <= m; i++) { int u, v; scanf("%d%d",&u,&v); e[u].push_back(v); } tarjan(1); ll ans = dfs(1,0); printf("%lld ",ans); return 0; }
向图的双连通分量
一、点双连通分量
定义:对于一个连通图,如果任意两点至少存在两条点不重复路径,则称这个图为点双连通的(简称双连通)。点双连通图的定义等价于任意两条边都同在一个简单环中。对一个无向图,点双连通的极大子图称为点双连通分量(简称双连通分量)。
#include<bits/stdc++.h> using namespace std; const int maxn = 1000; struct Edge { int u,v; Edge(int uu,int vv) { u = uu; v = vv; } }; stack<Edge> s; struct edge //链式前向星建图的边结构 { int v,next; }edges[maxn]; int n,m; //节点的数目,无向边的数目 int e,head[maxn]; int dfn[maxn]; //第一次访问的时间戳 int dfs_clock; //时间戳 int iscut[maxn]; //标记节点是否为割点 int bcc_cnt; //点_双连通分量的数目 int bccno[maxn]; //节点属于的点_双连通分量的编号 vector<int> bcc[maxn]; //点_双连通分量 void addedges(int u,int v) //加边 { edges[e].v = v; edges[e].next = head[u]; head[u] = e++; edges[e].v = u; edges[e].next = head[v]; head[v] = e++; } int dfs(int u,int fa) { int low = dfn[u] = ++dfs_clock; int child = 0; for(int i=head[u];i!=-1;i=edges[i].next) { int v = edges[i].v; Edge e = (Edge){u,v}; if(!dfn[v]) { s.push(e); child++; int lowv = dfs(v,u); low = min(low,lowv); //用后代更新low if(lowv >= dfn[u]) //找到了一个子树满足割顶的条件 { iscut[u] = 1; bcc_cnt++; bcc[bcc_cnt].clear(); for(;;) //保存bcc信息 { Edge x = s.top(); s.pop(); if(bccno[x.u] != bcc_cnt) {bcc[bcc_cnt].push_back(x.u); bccno[x.u] = bcc_cnt;} if(bccno[x.v] != bcc_cnt) {bcc[bcc_cnt].push_back(x.v); bccno[x.v] = bcc_cnt;} if(x.u == u && x.v == v) break; } } } else if(dfn[v] < dfn[u] && v != fa) //用反向边更新low { s.push(e); low = min(low,dfn[v]); } } if(fa < 0 && child == 1) iscut[u] = 0; //对于根节点若只有一个子树则不是割顶 return low; } void init() { memset(dfn,0,sizeof(dfn)); memset(iscut,0,sizeof(iscut)); memset(head,-1,sizeof(head)); memset(bccno,0,sizeof(bccno)); e = 0; dfs_clock = 0; bcc_cnt = 0; } int main() { int u,v; while(scanf("%d%d",&n,&m)!=EOF) { init(); for(int i=0;i<m;i++) { scanf("%d%d",&u,&v); addedges(u,v); } dfs(1,-1); for(int i=1;i<=bcc_cnt;i++) { for(int j=0;j<bcc[i].size();j++) cout<<bcc[i][j]<<" "; cout<<endl; } } return 0; }
二、边双连通分量
定义:对于一个连通图,如果任意两点至少存在两条边不重复路径,则称该图为边双连通的。边双连通图的定义等价于任意一条边至少在一个简单环中。对一个无向图,边双连通的极大子图称为边双连通分量。
#include<bits/stdc++.h> using namespace std; const int maxn = 1005; struct Edge { int no,v,next; //no:边的编号 }edges[maxn]; int n,m,ebcnum; //节点数目,无向边的数目,边_双连通分量的数目 int e,head[maxn]; int dfn[maxn]; //第一次访问的时间戳 int dfs_clock; //时间戳 int isbridge[maxn]; //标记边是否为桥 vector<int> ebc[maxn]; //边_双连通分量 void addedges(int num,int u,int v) //加边 { edges[e].no = num; edges[e].v = v; edges[e].next = head[u]; head[u] = e++; edges[e].no = num++; edges[e].v = u; edges[e].next = head[v]; head[v] = e++; } int dfs_findbridge(int u,int fa) //找出所有的桥 { int lowu = dfn[u] = ++dfs_clock; for(int i=head[u];i!=-1;i=edges[i].next) { int v = edges[i].v; if(!dfn[v]) { int lowv = dfs_findbridge(v,u); lowu = min(lowu,lowv); if(lowv > dfn[u]) { isbridge[edges[i].no] = 1; //桥 } } else if(dfn[v] < dfn[u] && v != fa) { lowu = min(lowu,dfn[v]); } } return lowu; } void dfs_coutbridge(int u,int fa) //保存边_双连通分量的信息 { ebc[ebcnum].push_back(u); dfn[u] = ++dfs_clock; for(int i=head[u];i!=-1;i=edges[i].next) { int v = edges[i].v; if(!isbridge[edges[i].no] && !dfn[v]) dfs_coutbridge(v,u); } } void init() { memset(dfn,0,sizeof(dfn)); memset(isbridge,0,sizeof(isbridge)); memset(head,-1,sizeof(head)); e = 0; ebcnum = 0; } int main() { int u,v; while(scanf("%d%d",&n,&m)!=EOF) { init(); for(int i=0;i<m;i++) { scanf("%d%d",&u,&v); addedges(i,u,v); } dfs_findbridge(1,-1); memset(dfn,0,sizeof(dfn)); for(int i=1;i<=n;i++) { if(!dfn[i]) { ebc[ebcnum].clear(); dfs_coutbridge(i,-1); ebcnum++; } } for(int i=0;i<ebcnum;i++) { for(int j=0;j<ebc[i].size();j++) cout<<ebc[i][j]<<" "; cout<<endl; } } return 0; }
三、点双连通分量和边双连通分量的区别和联系
①、二者都是基于无向图。
②、边双连通分量是删边后还连通,而后者是删点。
③、点双连通分量一定是边双连通分量(除两点一线的特殊情况),反之不一定。
④、点双连通分量可以有公共点,而边双连通分量不能有公共边。
Tarjan离线算法求LCA
思路:dfs...
#include<bits/stdc++.h> using namespace std; const int N = 500005; struct EDGE{ int next; int to; int lca; }; EDGE edge[N];//树的链表 EDGE qedge[N];//需要查询LCA的两节点的链表 int n,m,p,x,y; int num_edge,num_qedge,head[N],qhead[N]; int father[N]; int visit[N];//判断是否被找过 void add_edge(int from,int to){//建立树的链表 edge[++num_edge].next=head[from]; edge[num_edge].to=to; head[from]=num_edge; } void add_qedge(int from,int to){//建立需要查询LCA的两节点的链表 qedge[++num_qedge].next=qhead[from]; qedge[num_qedge].to=to; qhead[from]=num_qedge; } int fin(int z){//找爹函数 if(father[z]!=z) father[z]=fin(father[z]); return father[z]; } int dfs(int x){//把整棵树的一部分看作以节点x为根节点的小树 father[x]=x;//由于节点x被看作是根节点,所以把x的father设为它自己 visit[x]=1;//标记为已被搜索过 for(int k=head[x];k;k=edge[k].next)//遍历所有与x相连的节点 if(!visit[edge[k].to]){//若未被搜索 dfs(edge[k].to);//以该节点为根节点搞小树 father[edge[k].to]=x;//把x的孩子节点的father重新设为x } for(int k=qhead[x];k;k=qedge[k].next)//搜索包含节点x的所有询问 if(visit[qedge[k].to]){//如果另一节点已被搜索过 qedge[k].lca=fin(qedge[k].to);//把另一节点的祖先设为这两个节点的最近公共祖先 if(k%2)//由于将每一组查询变为两组,所以2n-1和2n的结果是一样的 qedge[k+1].lca=qedge[k].lca; else qedge[k-1].lca=qedge[k].lca; } } int main(){ scanf("%d%d%d",&n,&m,&p);//输入节点数,查询数和根节点 for(int i=1;i<n;++i){ scanf("%d%d",&x,&y);//输入每条边 add_edge(x,y); add_edge(y,x); } for(int i=1;i<=m;++i){ scanf("%d%d",&x,&y);//输入每次查询,考虑(u,v)时若查找到u但v未被查找,所以将(u,v)(v,u)全部记录 add_qedge(x,y); add_qedge(y,x); } dfs(p);//进入以p为根节点的树的深搜 for(int i=1;i<=m;i++) printf("%d ",qedge[i*2].lca);//两者结果一样,只输出一组即可 return 0; }