并不想把题解和模板解释写在一块。
一、有向图
强连通子图顶点两两可达,强连通分量是尽可能大的强连通子图。
Kosaraju
有两次dfs:第一次对原图dfs,记录每个点被访问完的顺序(注意:是访问完的顺序,不是dfs序!);第二次对反向图进行dfs,每次从最晚被访问完的点出发,能走到的所有点构成一个强连通分量,并删除这些点。
这是为什么呢?直觉上讲,感觉一遍原图正常dfs,一遍反向图按倒序的dfs完成顺序dfs,好像表示正着反着都能走到。
那么我们就开始理性的证明把,先从几条看上去像是废话的定理开始吧(深度指第一次dfs形成的dfs树中的深度):a.强连通分量顶点两两可达;b.不相互可达的两点一定不在同一个强连通分量里;c.dfs树中的父亲比儿子在完成序列中更靠后。
a.=>所有强连通分量上的点都存在一条路径走到该强连通分量中深度最小的点(d.)
b.=>第一遍dfs时从该强连通分量中深度最小的点出发(e.)
d.+e.=>反向图中dfs时,从该强连通分量中深度最小的点出发,一定会走到强连通分量中所有点(f.)
b.=>不在该强连通分量的点中,dfs树中只有比该强连通分量中深度最小的点深度小的点能走到该点,其它点在走不到该点,因此从该点反向dfs时走不到(g.)
c.=>第二次dfs时,出发点在dfs树中的祖先已经全部被删除(h.)
h.+g.=>反向图中dfs时,从该强连通分量中深度最小的点出发,一定不会走到不在强连通分量中所有点(i.)
f.+i.=>与出发点在同一强连通分量的点有且仅有反向图中dfs能走到的点.
#include<algorithm> #include<cmath> #include<cstdio> #include<cstdlib> #include<cstring> #include<iomanip> #include<iostream> #include<map> #include<queue> #include<stack> #include<vector> #define maxn 10010 #define maxm 100010 using namespace std; inline int read() { int x=0,f=1; char ch=getchar(); while(isdigit(ch)==0 && ch!='-')ch=getchar(); if(ch=='-')f=-1,ch=getchar(); while(isdigit(ch))x=(x<<3)+(x<<1)+ch-'0',ch=getchar(); return x*f; } inline void write(int x) { int f=0;char ch[20]; if(!x){puts("0");return;} if(x<0){putchar('-');x=-x;} while(x)ch[++f]=x%10+'0',x/=10; while(f)putchar(ch[f--]); putchar(' '); } int fir[maxn][2],nxt[maxm][2],v[maxm][2],cnt[2],vis[maxn]; int n,m,w[maxn],col[maxn],num,ord[maxn],cntord,out[maxn]; void ade(int u1,int v1,int f){v[cnt[f]][f]=v1,nxt[cnt[f]][f]=fir[u1][f],fir[u1][f]=cnt[f]++;} void kor1(int u) { vis[u]=1; for(int k=fir[u][0];k!=-1;k=nxt[k][0])if((!(k&1))&&!vis[v[k][0]])kor1(v[k][0]); ord[++cntord]=u; } void kor2(int u,int fa) { col[u]=fa; for(int k=fir[u][0];k!=-1;k=nxt[k][0])if((k&1)&&!col[v[k][0]])kor2(v[k][0],fa); } int main() { memset(fir,-1,sizeof(fir)); n=read(),m=read(); for(int i=1;i<=m;i++){int x=read(),y=read();ade(x,y,0),ade(y,x,0);} for(int i=1;i<=n;i++)if(!vis[i])kor1(i); for(int i=cntord;i>=1;i--)if(!col[ord[i]])num++,kor2(ord[i],num); for(int i=1;i<=n;i++) { w[col[i]]++; for(int k=fir[i][0];k!=-1;k=nxt[k][0]) if(col[v[k][0]]!=col[i]&&!(k&1))ade(col[i],col[v[k][0]],1),out[col[i]]++; } int ans=0; for(int i=1;i<=num;i++)if(out[i]==0){if(ans){write(0);return 0;}ans=w[i];} write(ans); return 0; }
感觉两遍dfs还是有点麻烦?
Tarjan
这玩意儿比较方便,代码好写,而且只用dfs一遍,所以比较常用。
对于每一个点要记两个值:dfn(在dfs序中的位置)和low(它能走到的dfs树中dfn最小的祖先)。
然后在dfs求这两个值的过程中,将点按dfs序放入栈。当某点彻底访问完时,且dfn==low,弹出栈中在该点上方的所有点(包含该点),这些点属于同一个强连通分量。
需要注意的是,不能走横叉边。
这听上去正确性很不显然。
先证明走横叉边肯定是没有出路的:假设A子树中有点a,B子树中有点b,A、B是不同的子树,有横叉边a->b。假设a、b在同一个强连通分量,那么一定还存在一条从b走到a的路径。那么dfs时无论先走A还是先走B,都会走到另一棵子树上,这样A、B在dfs树中就不是不同的子树了,与假设矛盾。所以,横叉边的两端点一定不在同一个强连通分量里。根据这个,可以发现如果把dfs树变成无向的,并删去所有非树边,每个强连通分量一定是连通的。
关于正确性,其实也很好证明。对于所有不在这个强连通分量中的点,访问那些以这个强连通分量中某点为后代的点时该强连通分量中的点已经退栈了;对于以这个强连通分量中某点为祖先的点,在走到以这个强连通分量中某点为父亲时,因为它与它的父亲不在同一强连通分量,所以它的dfn==low,它的所有后代都会退栈。而对于那些在强连通分量中的点,在访问完成强连通分量中深度最小的点时,全部都在栈里,所以不会漏记。
#include<algorithm> #include<cmath> #include<cstdio> #include<cstdlib> #include<cstring> #include<iomanip> #include<iostream> #include<map> #include<queue> #include<stack> #include<vector> #define maxn 3010 #define maxm 8010 using namespace std; inline int read() { int x=0,f=1; char ch=getchar(); while(isdigit(ch)==0 && ch!='-')ch=getchar(); if(ch=='-')f=-1,ch=getchar(); while(isdigit(ch))x=(x<<3)+(x<<1)+ch-'0',ch=getchar(); return x*f; } inline void write(int x) { int f=0;char ch[20]; if(!x){puts("0");return;} if(x<0){putchar('-');x=-x;} while(x)ch[++f]=x%10+'0',x/=10; while(f)putchar(ch[f--]); putchar(' '); } int dfn[maxn],low[maxn],fir[maxn][2],nxt[maxm][2],v[maxm][2],cnt[2],cost[maxn],minc[maxn],n,m,p,s[maxn],tp,tim,num,col[maxn],in[maxn],minx[maxn]; int q[maxn],hd=1,tl=0; void ade(int u1,int v1,int f){v[cnt[f]][f]=v1,nxt[cnt[f]][f]=fir[u1][f],fir[u1][f]=cnt[f]++;} void tar(int u) { dfn[u]=low[u]=++tim; s[++tp]=u; for(int k=fir[u][0];k!=-1;k=nxt[k][0]) { if(!dfn[v[k][0]])tar(v[k][0]),low[u]=min(low[u],low[v[k][0]]); else if(!col[v[k][0]])low[u]=min(low[u],dfn[v[k][0]]); } if(dfn[u]==low[u]) { col[u]=++num; while(s[tp]!=u)col[s[tp--]]=num; tp--; } } void getno() { for(int i=1;i<=num;i++)if(!in[i]&&minc[i]==minc[0])q[++tl]=i; int ans=minc[0]; while(hd<=tl) { int u=q[hd++];ans=min(ans,minx[u]); for(int k=fir[u][1];k!=-1;k=nxt[k][1]) if(minc[v[k][1]]==minc[0])q[++tl]=v[k][1]; } write(ans); } int main() { memset(minc,0x7f,sizeof(minc)); memset(minx,0x7f,sizeof(minx)); memset(cost,0x7f,sizeof(cost)); memset(fir,-1,sizeof(fir)); n=read(),p=read(); for(int i=1;i<=p;i++){int x=read(),y=read();cost[x]=y;} m=read(); for(int i=1;i<=m;i++){int x=read(),y=read();ade(x,y,0);} for(int i=1;i<=n;i++)if(!col[i])tar(i); for(int i=1;i<=n;i++) { minc[col[i]]=min(minc[col[i]],cost[i]); minx[col[i]]=min(minx[col[i]],i); for(int k=fir[i][0];k!=-1;k=nxt[k][0]) if(col[v[k][0]]!=col[i])in[col[v[k][0]]]++,ade(col[i],col[v[k][0]],1); } int sum=0; for(int i=1;i<=num;i++) { if(!in[i]) { if(minc[i]==minc[0]){puts("NO"),getno();return 0;} sum+=minc[i]; } } puts("YES"),write(sum); return 0; }
二、无向图
1.点双连通分量
删去任何一个点后仍连通的图是点双连通子图,一个尽量大的强连通子图是点双连通分量。
tarjan也可以用来求点双连通分量。可以先求出割点,然后通过删除割点判双连通性就可以了。
该怎么判断呢?和强连通分量类似,也需要找dfs树。对于树根而言,由于无向图的dfs树没有横叉边,所以只要有多于一个子树,树根就是割点。对于不是树根的点,只要存在一个儿子不能在不走它的情况下走到更往上的位置,删除这个点后,那个儿子就和它的祖先不连通,这样该点就是割点。
void tar(int u) { dfn[u]=low[u]=++tim; int cnt=0; for(int k=fir[u];k!=-1;k=nxt[k]) { if(!dfn[v[k]]) { cnt++; tar(v[k]); low[u]=min(low[u],low[v[k]]); if((x==rt&&cnt>1)||(x!=rt&&dfn[x]<=low[y]))yes[x]=1; } else low[x]=min(low[x],dfn[y]); } }
2.边双连通分量
删去任何一条边后仍连通的图是边双连通子图,一个尽量大的强连通子图是边双连通分量。
先说一个并不对劲的方法:先随便弄一个生成树,再枚举每一条非树边。将这条边的两个端点在生成树上的路径(如果不是直链,就拆成a->LCA和b->LCA)合并成一个边双连通分量,用并查集维护每个点所在边双连通分量中深度最小的点是哪个。
但是这样的复杂度并不比tarjan优秀,是O(n log n + m),但是正确性显然,考场上忘记tarjan时不妨尝试。
tarjan的做法和点双类似,如果一条边链接的两个点中,深度较大的点不能在不通过该边的情况下走到深度较小的点或它的祖先,那么该边就是割边。删掉所有割边后,剩下的连通分量就是原图的边双连通分量。
void tar(int u) { dfn[u]=low[u]=++tim; for(int k=fir[u];k!=-1;k=nxt[k]) { if(k==bac[u]^1)continue; if(!dfn[v[k]]) { bac[v[k]]=k; tar(v[k]); low[u]=min(low[u],low[v[k]]); if(low[v[k]]>dfn[u])yes[k]=yes[k^1]=1; } else low[u]=min(low[u],dfn[v[k]]); } }
三、并不对劲的看法(和连通分量无关,不闲者请出门左转)
最近机房里总有人互相瞎膜,但是对某些知识点、某些题很不尊重,还说出题人是**(脏话,自动和谐)。但这样毫无意义,就像你花10分钟膜wzj100次,不如花同样的时间想透一个知识点、弄懂一道题有效。也有些人是因为题涉及的知识点是noip难度的,就十分轻视,觉得很弱智。但你们就不想想,你们能做到次次模拟赛290左右吗?能保证今年11月份AK吗?不能就别轻看它,因为你说这种题弱智,你这种无法一遍AC这种题的人更弱智。就算按那种互膜的思路来讲,也不该这么做,因为出题人的实力肯定是远在题目难度之上的,你们觉得简单的noip题,也是经过很多实力至少在省队级别的人,多次讨论、修改、审核才出到noip中的。那些连身边拿个省一的人都膜的人,干嘛不去膜出题人,反而骂他们?还有些人觉得题难想、难写、难调,就对出题人有意见。做题本来就是为了挑战自己,不想思考、不想写、不想调就别写题,别学信息了。反正这种态度学了也白学,信竞不缺这一两个自以为是的失败者,不如去学文化课。