tarjan
基于深度优先搜索,用于寻找有向图中的连通块。
主要代码如下:
inline void tarjan(int x){ dfn[x]=low[x]=++idx; vis[x]=1; sta[++top]=x; for(int i=head[x];i;i=e[i].nxt){ int v=e[i].v; if(!dfn[v]){//还未访问过这个节点 tarjan(v); low[x]=min(low[x],low[v]); } else if(vis[v]==1){//这个节点在栈中 low[x]=min(low[x],dfn[v]); } } if(dfn[x]==low[x]){//找到一个强联通分量 ++cnt; while(x!=sta[top]){ vis[sta[top]]=0; qlt[sta[top]]=cnt;//染色,标记这个节点属于第几号强联通分量 siz[cnt]++;//记录这一个连通块的节点数 top--; } vis[x]=0;//还要将这个节点弹出来 qlt[x]=cnt; siz[cnt]++; top--; } }
如果是要遍历整张图找到所有的连通块,通常还要加这句
for(int i=1;i<=n;i++){ if(!dfn[i]) tarjan(i); }
因为图中不一定全部都是联通的。
每一个点只访问了一次,所以tarjan的时间复杂度是O(n)的。
通常还要用到tarjan缩点。缩点的时候将全部边遍历一次,
如果两个节点不属于同一个连通块,那么就连一条边,为了避免这种情况:
a->c b->c a,b属于同一个连通块。
可以用并查集处理一下,避免重复将两个相同的连通块连边。(重复连边好像也并没有什么影响)
题目
USACO 2003 Fall:
popular cow
这道题首先要知道:对于一个有向无环图,出度为零的点,从其他任意一个点都可以到达这个点。
所以我们只需要将原图用tarjan缩点变成一个有向无环图,找到出度为零的点,输出那个连通块的大小就行了。
#include<bits/stdc++.h> #define cg c=getchar() #define N 10020 #define M 50050 using namespace std; template<typename xxx>inline void read(xxx &x){ x=0;int f=1;char cg; while(c<'0'||c>'9'){if(c=='-')f=-1;cg;} while(c>='0'&&c<='9'){x=(x<<1)+(x<<3)+(c^48);cg;} x*=f; } template<typename xxx>inline void print(xxx x){ if(x<0) x=-x,putchar('-'); if(x>9) print(x/10); putchar(x%10+48); } int n,m,aa,bb; struct node{ int v,nxt; }e[M]; int head[N]; int tot; inline void add(int a,int b){ ++tot; e[tot].v=b; e[tot].nxt=head[a]; head[a]=tot; } int idx; int dfn[N]; int low[N]; int sta[N]; int vis[N]; int top; int cnt; int qlt[N]; int siz[N]; int cd[N]; inline void tarjan(int x){ dfn[x]=low[x]=++idx; vis[x]=1; sta[++top]=x; for(int i=head[x];i;i=e[i].nxt){ int v=e[i].v; if(!dfn[v]){ tarjan(v); low[x]=min(low[x],low[v]); } else if(vis[v]==1){ low[x]=min(low[x],dfn[v]); } } if(dfn[x]==low[x]){ ++cnt; while(x!=sta[top]){ vis[sta[top]]=0; qlt[sta[top]]=cnt; siz[cnt]++; top--; } vis[x]=0; qlt[x]=cnt; siz[cnt]++; top--; } } int main(){ read(n);read(m); for(int i=1;i<=m;i++){ read(aa);read(bb); add(aa,bb); } for(int i=1;i<=n;i++){ if(!dfn[i]) tarjan(i); } //进行缩点 for(int i=1;i<=n;i++){ for(int j=head[i];j;j=e[j].nxt){ int v=e[j].v; if(qlt[i]!=qlt[v]){ cd[qlt[i]]++;//因为不用重建一张图,只需要记录出度就行了 } } } //因为重构的图也有可能存在两个连通块之间没有边连接,它们的出度也为零,所以要考虑这种情况 int ans=0; int id; for(int i=1;i<=cnt;i++){ if(cd[i]==0){ ans++; id=i; } } if(ans==1){//只能有一个出度为零的连通块 print(siz[id]); } else print(0); }
IOI 1996 网络协议
这道题有两个问题,第一个问题还是比较明显,就是缩点后看有几个入度为0的点,每一个入度为零的点都必须放消息。
第二问就相当于将缩点后的图变成一个连通块。每一个入度为零的边都要增加一条入度,每一个出度为零的边都要增加一条出度。最后取较大值。
有个小细节,如果缩点后只有一个点,那么就不用加边了,所以要特判一下。
code:
#include<bits/stdc++.h> #define N 120 #define M 13000 #define cg c=getchar() using namespace std; template<typename xxx>inline void read(xxx &x){ x=0;int f=1;char cg; while(c<'0'||c>'9'){if(c=='-')f=-1;cg;} while(c>='0'&&c<='9'){x=(x<<1)+(x<<3)+(c^48);cg;} x*=f; } template<typename xxx>inline void print(xxx x){ if(x<0) x=-x,putchar('-'); if(x>9) print(x/10); putchar(x%10+48); } struct node{ int u,v,nxt; }e[M],e2[M]; int head[N]; int tot; inline void add(int a,int b){ ++tot; e[tot].u=a; e[tot].v=b; e[tot].nxt=head[a]; head[a]=tot; } int n; int aa; int idx; int top; int dfn[N]; int low[N]; int vis[N]; int sta[N]; int qlt[N]; int cnt; inline void tarjan(int x){ dfn[x]=low[x]=++idx; vis[x]=1; sta[++top]=x; for(int i=head[x];i;i=e[i].nxt){ int v=e[i].v; if(!dfn[v]) { tarjan(v); low[x]=min(low[x],low[v]); } else if(vis[v]==1){ low[x]=min(low[x],dfn[v]); } } if(dfn[x]==low[x]){ ++cnt; while(x!=sta[top]){ qlt[sta[top]]=cnt; vis[sta[top]]=0; top--; } qlt[x]=cnt; vis[x]=0; top--; } } int rd[N]; int cd[N]; int ans; int main(){ // freopen("protocols10.in","r",stdin); read(n); for(int i=1;i<=n;i++){ while(true){ read(aa); if(aa==0) break; add(i,aa); } } for(int i=1;i<=n;i++){ if(!dfn[i]) tarjan(i); } for(int i=1;i<=tot;i++){ int x=e[i].u; int y=e[i].v; if(qlt[x]!=qlt[y]) { rd[qlt[y]]++; cd[qlt[x]]++; } } int ans1=0; int ans2=0; for(int i=1;i<=cnt;i++){ if(rd[i]==0){ ans1++; ans++; } if(cd[i]==0){ ans2++; } } print(ans);putchar('\n'); if(cnt==1) print(0);//特判 只有一个连通块,不需要加边。 else print(max(ans1,ans2)); }
消息的传递:
跟上一题的第一问一样。。。只不过输入变成了邻接矩阵。
code:
#include<bits/stdc++.h> #define cg c=getchar() #define N 1200 #define M N*(N-1)/2 using namespace std; template<typename xxx>inline void read(xxx &x){ x=0;int f=1;char cg; while(c<'0'||c>'9'){if(c=='-')f=-1;cg;} while(c>='0'&&c<='9'){x=(x<<1)+(x<<3)+(c^48);cg;} x*=f; } template<typename xxx>inline void print(xxx x){ if(x<0) x=-x,putchar('-'); if(x>9) print(x/10); putchar(x%10+48); } struct node{ int u,v,nxt; }e[M]; int head[N]; int tot; inline void add(int a,int b){ ++tot; e[tot].u=a; e[tot].v=b; e[tot].nxt=head[a]; head[a]=tot; } int n; int aa; /*--------------------------*/ int idx; int dfn[N]; int low[N]; int vis[N]; int sta[N]; int top; int cnt; int qlt[N]; inline void tarjan(int x){ dfn[x]=low[x]=++idx; sta[++top]=x; vis[x]=1; for(int i=head[x];i;i=e[i].nxt){ int v=e[i].v; if(!dfn[v]){ tarjan(v); low[x]=min(low[x],low[v]); } else if(vis[v]==1){ low[x]=min(low[x],dfn[v]); } } if(dfn[x]==low[x]){ ++cnt; while(x!=sta[top]){ qlt[sta[top]]=cnt; vis[sta[top]]=0; top--; } qlt[x]=cnt; vis[x]=0; top--; } } int rd[N]; int main(){ read(n); for(int i=1;i<=n;i++){ for(int j=1;j<=n;j++){ read(aa); if(aa==1){ add(i,j); } } } for(int i=1;i<=n;i++){ if(!dfn[i]) tarjan(i); } for(int i=1;i<=tot;i++){ int x=e[i].u; int y=e[i].v; if(qlt[x]!=qlt[y]){ rd[qlt[y]]++; } } int ans=0; for(int i=1;i<=cnt;i++){ if(rd[i]==0){ ans++; } } print(ans); }
心得
- 有向无环图中一个出度为零的点,从任意一个点出发都可到达它。
- 有向无环图至少有一个入度为零的点。
- 有向无环图的生成树个数等于入度非零的节点的入度积。
- 强联通分量的题好像多数与入度出度有关,想题的时候可以往这方面考虑。