taj
首先,taj是用来干嘛的?
taj主要处理两种问题:
1.找强连通分量
2.找割点
均可在O(n)时间内完成
taj的大体思路就是把一张图改成一棵树,钦定一个点,作为根;
然后我们有两个数组:
dfn[u]:u的时间戳
low[u]:u这一坨东西的最早出现的时间(感性理解)
求强联通分量:
强连通分量:有向图中,如果一坨东西,能够互相到达,就是强连通分量,一个点也是
我们会发现,如果u的low>=dfn(实际上low不会>dfn最多=),那么它一定与上面没有联系,所以我们不用考虑它是否会与上面形成强联通分量
在它下面的东西(v)中,我们考虑:
如果说,v的low没有到达u,也就是v无法回到u,那它肯定会在之前就从栈里弹出
如果说,v的low能到达u的上面,那么u一定可以通过v这一条路到达上面,与上面产生联系,low[u]!=dfn[u]
所以,只要当low[u]=dfn[u]时,将栈中到u为止的元素都合并到一个强联通分量重,肯定是没问题的
void taj(int u) { dfn[u]=low[u]=++tim; sta[++top]=u; vis[u]=1; for(int i=head[u];i;i=p[i].next) { int v=p[i].to; if(!dfn[v]) { taj(v); low[u]=min(low[u],low[v]); } else if(vis[v]) low[u]=min(low[v],low[u]); } if(dfn[u]==low[u]) { int v; while(v=sta[top--]) { rt[v]=u; vis[v]=0; if(u==v) break; a[u]+=a[v]; } } }
求割点:
割点:在一个无向连通块中,删去一个点,将导致它不连通的点
然后就是怎么求:
如图,我们将一张图抽象如上,1表示图的上部,5表示下部,中间由环相连。
我们用dfn[x]表示x被访问到的时间,low[x]表示它所在的那一坨的最小值 ~~(感性理解一下)~~
我们从点1开始dfs,记录每一个点的时间戳,然后去搜所有没有搜过的,如果,他的儿子的最上面的时间戳(low[v])
low[v]>=dfn[x]说明v最早也是在x及之后了,也就意味着x上面的与v只能通过x相连,也就是说,只要x上面有(x不为本组祖先),那么x就是割点**
如果x是祖先呢?
那我们将x找到一个儿子便让x的儿子++,并让儿子搜一遍(保证和此儿子联通的都被搜过),那么下次找到的儿子就是和此儿子不连通的,那么只要把x去掉,他两个儿子就不连通了,所以,x为割点
不理解上面的算法结合图看,把①去掉,2,3这两个儿子就不连通了
还有一个问题:大佬都说这里low要这样写:low[x]=min(low[x],dfn[v])
至于为什么不像强联通分量里一样,写low[x]=min(low[x],low[v])呢?
因为强联通里只要在强联通中,怎么连边都可以(我们又不会在强联通中割点),但割点不行,我们不能随便连边,low[x]=min(low[x],low[v])表示所有v都连与最先祖先的上,如下图1,但很明显,3,4,5并没有这一条路,所以如果你这么写,代码会认为有这一条路,从而出错
当然,由图的关系得到的low并不会有假边所以写low[x]=min(low[x],low[v])(不理解看代码)
实际:
写low[x]=min(low[x],low[v]):
当然,你也可以这么理解:
这主要是防止它无向图咋搜下来的,就咋走回去了
void tag(int x,int zx) { int kid=0; dfn[x]=low[x]=++tim; for(int i=head[x];i;i=p[i].next) { int v=p[i].to; if(!dfn[v]) { tag(v,zx); low[x]=min(low[v],low[x]); //是有真边构成,不用改 if(low[v]>=dfn[x]&&x!=zx) cut[x]=1;//后面无法通过别的方法连接到前面,是割点 if(x==zx) kid++; } low[x]=min(low[x],dfn[v]); //要改的指这个 } if(kid>1&&x==zx) cut[x]=1; //祖先有两个+的孩子,是割点 }
#include <bits/stdc++.h> using namespace std; const int N = 2e5+5; int n,m; struct edge { int next,to; }p[N]; int head[N],num; void ad(int x,int y) { p[++num].next=head[x]; p[num].to=y; head[x]=num; } int dfn[N],low[N],tim,cut[N]; void tag(int x,int zx) { int kid=0; dfn[x]=low[x]=++tim; for(int i=head[x];i;i=p[i].next) { int v=p[i].to; if(!dfn[v]) { tag(v,zx); low[x]=min(low[v],low[x]); if(low[v]>=dfn[x]&&x!=zx) cut[x]=1; if(x==zx) kid++; } low[x]=min(low[x],dfn[v]); } if(kid>1&&x==zx) cut[x]=1; } int ans; int main() { cin>>n>>m; for(int i=1;i<=m;i++) { int x,y; scanf("%d%d",&x,&y); ad(x,y); ad(y,x); } for(int i=1;i<=n;i++) if(!dfn[i]) tag(i,i); for(int i=1;i<=n;i++) ans+=cut[i]; printf("%d\n",ans); for(int i=1;i<=n;i++) if(cut[i]) printf("%d ",i); return 0; }
同是,注意:
正确:
for(int i=head[u];i;i=p[i].next) { int v=p[i].to; if(!dfn[v]) { taj(v,zx); low[u]=min(low[u],low[v]); if(u==zx) kid++; if(low[v]>=dfn[u]&&u!=zx) cut[u]=1; } else if(vis[v]) low[u]=min(low[u],dfn[v]); }
错误:
for(int i=head[u];i;i=p[i].next) { int v=p[i].to; if(!dfn[v]) { taj(v,zx); low[u]=min(low[u],low[v]); if(u==zx) kid++; } else if(vis[v]) low[u]=min(low[u],dfn[v]); if(low[v]>=dfn[u]&&u!=zx) cut[u]=1; }
就是这个判断的时候,一定是要
只有直接儿子才可以按if(low[v]>=dfn[u]&&u!=zx) cut[u]=1;判,毕竟人家就是这么走的,但是如果是间接儿子,那么,实际上它应该是从别处走来的,所以这么写是错的
看一道题:
lgP3469:https://www.luogu.com.cn/problem/P3469
题目大意,n个点,m条边,(<=1e5),本来每两个点间可以互相到达,现在把点i给封锁,有多少到达关系会无法成立
分析:
如果封锁的点不是割点,那么显然:只有这个点到其他n-1个点和反过来的关系无法做到,所以答案为:2*(n-1)
如果封锁的是割点,那么这张图将被分割为若干块,若一块的大小为s,那么它与外界(n-s)个点的关系无法成立,所以这个联通块的贡献是s*(n-s);当然,被封锁的点也无法与外界联系,所以有(n-1)的贡献;总贡献就是所有连通块的贡献+n-1
这样子,我们就可以得到一个O(n^2)的做法:对于每个割点O(n)扫连通块
#include <bits/stdc++.h> #define random(a,b) rand()%(b-a+1)+a using namespace std; const int N = 1e5+5; int n,m; struct edge { int next,to; }p[10*N]; int head[N],num; void ad(int x,int y) { p[++num]=edge{head[x],y}; head[x]=num; } int dfn[N],low[N],mx[N],tim,cut[N],sd[N]; void taj(int u,int zx) { int kid=0; dfn[u]=low[u]=mx[u]=++tim; for(int i=head[u];i;i=p[i].next) { int v=p[i].to; if(!dfn[v]) { taj(v,zx); mx[u]=max(mx[u],mx[v]); low[u]=min(low[u],low[v]); if(low[v]>=low[u]&&u!=zx) { cut[u]=1; sd[u]=mx[u]-dfn[u]; } if(u==zx) kid++; } else low[u]=min(low[u],dfn[v]); } if(u==zx&&kid>1) cut[u]=1; } int s=0; bool vis[N]; void dfs(int u,int x) { vis[u]=1; s++; for(int i=head[u];i;i=p[i].next) { int v=p[i].to; if(v==x) continue; // printf("@%d %d\n",u,v); if(!vis[v]) dfs(v,x); } } void print(int x) { int ans=n-1; memset(vis,0,sizeof(vis)); // puts("Bear"); // for(int i=head[7];i;i=p[i].next) printf("%d ",p[i].to);puts(""); for(int i=1;i<=n;i++) { if(i==x) continue; if(!vis[i]) { s=0; dfs(i,x); ans+=s*(n-s); // printf("#%d %d\n",i,s); } } printf("%d\n",ans); // system("pause"); } int main() { // freopen("1.out","w",stdout); srand(time(0)); cin>>n>>m; for(int i=1;i<=m;i++) { int x,y; scanf("%d%d",&x,&y); ad(x,y);ad(y,x); } taj(1,1); for(int i=1;i<=n;i++) { if(!cut[i]) printf("%d\n",2*(n-1)); else print(i); } return 0; } //bear Br2 br2 2br BR2 BR nmdp Bear
然后我们就t了,我们需要对算法进行优化:
我们可以在taj的时候就处理出会分出的联通块大小
我们定义sz[u]为taj搜到u时u子树大小,sm[u]表示u被割后,与根分离的点
我们思考如果找到一个会u被割v会被影响的点v,那么sz[v]就是割下来连通块大小,同时,n-sm[u]就是u被割下来后,根那一边的大小
对于根节点,我们直接按上面O(n^2)的做法,O(n)扫一遍
#include <bits/stdc++.h> #define random(a,b) rand()%(b-a+1)+a #define int long long using namespace std; const int N = 1e5+5; int n,m; struct edge { int next,to; }p[10*N]; int head[N],num; void ad(int x,int y) { p[++num]=edge{head[x],y}; head[x]=num; } int sdkm(int x){return x*(n-x);} int dfn[N],low[N],mx[N],tim,cut[N],sd[N],sz[N],sm[N]; void taj(int u,int zx) { int kid=0,lst; sz[u]=sm[u]=1; dfn[u]=low[u]=mx[u]=lst=++tim; for(int i=head[u];i;i=p[i].next) { int v=p[i].to; if(!dfn[v]) { taj(v,zx); low[u]=min(low[u],low[v]); sz[u]+=sz[v]; if(low[v]>=dfn[u]&&u!=zx) { mx[u]=max(mx[u],mx[v]); cut[u]=1; sm[u]+=sz[v]; // if(u==3) printf("@%d %d\n",v,sz[v]); sd[u]+=sdkm(sz[v]); lst=mx[u]; } if(u==zx) kid++; } else low[u]=min(low[u],dfn[v]); // printf("%d %d %d %d\n",u,v,low[u],dfn[v]); } if(u==zx&&kid>1) cut[u]=1; } int s=0; bool vis[N]; void dfs(int u,int x) { vis[u]=1; s++; for(int i=head[u];i;i=p[i].next) { int v=p[i].to; if(v==x) continue; // printf("@%d %d\n",u,v); if(!vis[v]) dfs(v,x); } } void print(int x) { int ans=n-1; memset(vis,0,sizeof(vis)); // puts("Bear"); // for(int i=head[7];i;i=p[i].next) printf("%d ",p[i].to);puts(""); for(int i=1;i<=n;i++) { if(i==x) continue; if(!vis[i]) { s=0; dfs(i,x); ans+=s*(n-s); // printf("#%d %d\n",i,s); } } printf("%lld\n",ans); // system("pause"); } signed main() { // freopen("1.out","w",stdout); srand(time(0)); cin>>n>>m; for(int i=1;i<=m;i++) { int x,y; scanf("%lld%lld",&x,&y); ad(x,y);ad(y,x); } taj(1,1); if(cut[1]) print(1); else printf("%lld\n",2*(n-1)); for(int i=2;i<=n;i++) { if(!cut[i]) printf("%lld\n",2*(n-1)); else { // printf("#%d %d ",sd[i],sm[i]); printf("%lld\n",sd[i]+sdkm(n-sm[i])+n-1); } } return 0; } //bear Br2 br2 2br BR2 BR nmdp Bear