其实也就是无向图的连通性
复习:
无向图的联通分量
割点:在一个联通分量里面有一些关键点,如果删除它,会把这个联通分量分为更多。
割边——双连通问题
有多少个割点:DFS,深搜优先生成树
对任意一个点s做DFS,生成一棵树
1)如果树的根节点s有两个或更多的孩子:s是割点
2)T的非根节点u是割点:当且仅当u存在一个子节点v,v及其后代都没有回退边连回u的祖先
HOW:u的直接后代v,数组num[]表示DFS时候的顺序,low[i]表示i及其后代能够回退回的祖先的num,一开始是num[i]=low[i]=dfn++
如果low[v]>=num[u],那么u就是割点
如果low[v]>num[u],那么u-->v就是割边
判断割点
void tarjin(int x){ low[x]=dfn[x]=++cnt; int child=0; // for(int i=head[x];i;i=e[i].nex){ int v=e[i].to; if(!dfn[v]){ tarjin(v); low[x]=min(low[x],low[y]); if(low[y]>=dfn[x]){ child++; if(x!=root||child>1) cut[x]=1; } } else low[x]=min(low[x],dfn[y]); } }
判断割边(要判断是不是反边,所以要记录访问到了哪条边)
#include<iostream> #include<cstdio> #include <cstring> #include <algorithm> #include <vector> using namespace std; const int maxn=155; const int maxm=10005; int n,m,a,b,cnt; struct node{ int to,nex; }ed[maxm]; int head[maxn],low[maxn],dfn[maxn],tot; struct bridge{ int x,y; bool operator < (const bridge &t)const{ if(x==t.x) return y<t.y; return x<t.x; } }bri[maxm]; int idx=1; //从2,3开始配对 void adde(int x,int y){ ed[++idx]={y,head[x]}; head[x]=idx; }void tarjin(int x,int num){ //num是指x是从哪条边来的,避免遇到反边 dfn[x]=low[x]=++tot; for(int i=head[x];i;i=ed[i].nex){ int v=ed[i].to; if(!dfn[v]){ tarjin(v,i); low[x]=min(low[x],low[v]); if(low[v]>dfn[x]){ bri[cnt++]={x,v}; } } else if(i!=(num^1)){ //不是反边 low[x]=min(low[x],dfn[v]); } } } int main(){ cin>>n>>m; while(m--){ cin>>a>>b; adde(a,b);adde(b,a); } for(int i=1;i<=n;i++){ if(!dfn[i]) tarjin(i,0); } sort(bri,bri+cnt); for(int i=0;i<cnt;i++){ cout<<bri[i].x<<" "<<bri[i].y<<endl; } return 0; }
双连通分量:
在一个联通图中任选两点,如果至少存在两条“点不重复”的路径,称为点双连通。点双连通极大子图:点双连通分量(没有割点)
边双连通分量eDCC:如果至少存在两条“边不重复”的路径,成为边双连通(没有割边)
点双连通分量vDCC:如果至少存在两条“点不重复”的路径,称为点双连通(没有割点)
Tarjan,在dfs的时候,把遍历过程中的点保存起来,就可以得到点双连通分量,保存在栈,找到割点就拿出来,注意存在栈的是边
边双连通分量:缩点的技术:
1)首先找出图G的所有边双连通分量,DFS时,所有点生成low值,low值相同的就是同一个SCC,,有多少个SCC就有多少个边双连通分量
2)把每个边双连通分量看作一个点,low值相同的合并为一个点
3)转化为一棵树,即至少在缩点树上面增加多少条边才能变为一个边双连通图,即(度为1的点+1)/2
下面的是求需要连接多少条边才能变为双连通分量,只需要low数组
边双连通分量的模板(离x的时候记录edcc)
#include<iostream> #include<cstdio> #include <cstring> #include <algorithm> #include <vector> #include<stack> using namespace std; const int maxn=5050; const int maxm=1e4+10; int n,m; //边双联通分量 割边---桥 int dfn[maxn],low[maxn]; struct node{ int to,nex; }e[maxm]; int head[maxn],idx=1; //从2,3开始配对 int cnt,num,dcc[maxn],bri[maxn],d[maxn]; void adde(int x,int y){ e[++idx].to=y; e[idx].nex=head[x]; head[x]=idx; } stack<int> sta; void tarjin(int x,int in_ed){ dfn[x]=low[x]=++num; sta.push(x); for(int i=head[x];i;i=e[i].nex){ int y=e[i].to; if(!dfn[y]){ tarjin(y,i); low[x]=min(low[x],low[y]); if(low[y]>dfn[x]){ //回的时候判断割边 bri[i]=bri[i^1]=1; //判断割边只有这一个条件 } } else if(i!=(in_ed^1)) { //但是有更新条件 low[x]=min(low[x],dfn[y]); } } if(dfn[x]==low[x]){ ++cnt; int y; do{ y=sta.top();sta.pop(); dcc[y]=cnt; }while(y!=x); } } int main(){ cin>>n>>m; while(m--){ int a,b; cin>>a>>b; adde(a,b);adde(b,a); } tarjin(1,0); for(int i=2;i<=idx;i++){ //看桥连了多少个缩之后的点 edcc if(bri[i]) d[dcc[e[i].to]]++; //度数 } int summ=0; for(int i=1;i<=cnt;i++){ if(d[i]==1) summ++; } cout<<(summ+1)/2; return 0; }
点双联通分量模板(遇到了割点就要记录,对割点要裂点,如果找完了之后需要重新建图,那就把所有的vdcc和割边相连)
#include<iostream> #include<cstdio> #include <cstring> #include <algorithm> #include <vector> using namespace std; const int maxn=1e4+10; const int maxm=5e4+10; int n,m; int dfn[maxn],low[maxn]; vector<int> e[maxn],ne[maxn]; bool cut[maxn]; int root,num,top; vector<int> dcc[maxn]; int sta[maxn],id[maxn]; void tarjin(int x){ dfn[x]=low[x]=++num; int child=0; // sta[++top]=x; if(x==root&&!e[x].size()){//孤立点 dcc[++cnt].push_back(x); return; } for(int i=0;i<e[x].size();i++){ int y=e[x][i]; if(!dfn[y]){ tarjin(y); low[x]=min(low[x],low[y]); if(low[y]>=dfn[x]){ child++; // if(x!=root||child>1){ cut[x]=1; } int z;cnt++; cout<<"vDCC:"; do{ z=sta[top--]; dcc[cnt].push_back(z); cout<<z<<" "; }while(z!=y); dcc[cnt].push_back(x); cout<<x<<endl; } } else low[x]=min(low[x],dfn[y]); } } int main(){ cin>>n>>m; int x,y; while(m--){ scanf("%d %d",&x,&y); e[x].push_back(y); e[y].push_back(x); } for(root=1;root<=n;root++){ if(!dfn[root]) tarjin(root); } //给每个割点一个新编号(cnt+1开始) num=cnt; for(int i=1;i<=n;i++) if(cut[i]) id[i]=++num; //建新图,从每个vDCC向对应割点连边 for(int i=1;i<=cnt;i++){ for(int j=0;j<dcc[i].size();j++){ int x=dcc[i][j]; if(cut[x]){ ne[i].push_pack(id[x]); ne[id[x]].push_pack(i); } } } return 0; }
例题:
1520:【 例 1】分离的路径
给出一个无向图,求要变成一个边双连通分量,要加多少条边。
和上面求双连通分量一样的
tarjin判环,统计入度为1的点的个数,新建道路可连接2个入度为1的点,故道路为入度为1的点的个数的一半。
#include<iostream> #include<cstdio> #include <cstring> #include <algorithm> #include <vector> #include<stack> using namespace std; const int maxn=5050; const int maxm=1e4+10; int n,m; //边双联通分量 割边---桥 int dfn[maxn],low[maxn]; struct node{ int to,nex; }e[maxm]; int head[maxn],idx=1; //从2,3开始配对 int cnt,num,dcc[maxn],bri[maxn],d[maxn]; void adde(int x,int y){ e[++idx].to=y; e[idx].nex=head[x]; head[x]=idx; } stack<int> sta; void tarjin(int x,int in_ed){ dfn[x]=low[x]=++num; sta.push(x); for(int i=head[x];i;i=e[i].nex){ int y=e[i].to; if(!dfn[y]){ tarjin(y,i); low[x]=min(low[x],low[y]); if(low[y]>dfn[x]){ //回的时候判断割边 bri[i]=bri[i^1]=1; //判断割边只有这一个条件 } } else if(i!=(in_ed^1)) { //但是有更新条件 low[x]=min(low[x],dfn[y]); } } if(dfn[x]==low[x]){ ++cnt; int y; do{ y=sta.top();sta.pop(); dcc[y]=cnt; }while(y!=x); } } int main(){ cin>>n>>m; while(m--){ int a,b; cin>>a>>b; adde(a,b);adde(b,a); } tarjin(1,0); for(int i=2;i<=idx;i++){ //看桥连了多少个缩之后的点 edcc if(bri[i]) d[dcc[e[i].to]]++; //度数 } int summ=0; for(int i=1;i<=cnt;i++){ if(d[i]==1) summ++; } cout<<(summ+1)/2; return 0; }
1521:【 例 2】矿场搭建
煤矿工地可以看成是由隧道连接挖煤点组成的无向图。为安全起见,希望在工地发生事故时所有挖煤点的工人都能有一条出路逃到救援出口处。于是矿主决定在某些挖煤点设立救援出口,使得无论哪一个挖煤点坍塌之后,其他挖煤点的工人都有一条道路通向救援出口。
请写一个程序,用来计算至少需要设置几个救援出口,以及不同最少救援出口的设置方案总数。
这道题蛮复杂,要先用tarjin算割点,然后用DFS出每个分量里面的割点数量,根据不同的数量不同的处理(画图看看吧)
解析:https://www.luogu.com.cn/problemnew/solution/P3225
分类讨论:
1)如果没有割点,至少需要建立两个出口,从任意非割点的地方选择两个点建立
2)如果这个分组只有一个割点,只需要在分组内设立一个出口,可以设立在任意一个非割点的地方
3)如果有两个及以上个割点,则无需建立,可以直接到达其他联通块
#include<iostream> #include<cstring> #include<cmath> #include<algorithm> #include<stack> #include<cstdio> #include<queue> #include<map> #include<vector> #include<set> using namespace std; const int maxn=510; const int INF=0x3fffffff; typedef long long LL; //最后得到点双连通分量 //还要求不同的方案数 //用Tarjan跑出割点,然后DFS搜索所有的联通快 //计算每一个联通快中的割点数目 //分类讨论: //如果没有割点 //至少需要建立两个出口 //从任意非割点的地方选择两个点建立 //如果这个分组只有一个割点 //只需要在分组内设立一个出口 //可以设立在任意一个非割点的地方 //如果有两个及以上个割点,则无需建立,可以直接到达其他联通块 //https://www.luogu.com.cn/problemnew/solution/P3225 int dfn[maxn],vis[maxn],low[maxn]; bool cut[maxn]; //是不是割点 int head[maxn]; LL ans,cnt,dfnn,ans1,ans2,group,cases,root,rs,cuts; int n,m; void inti(){ memset(head,-1,sizeof(head)); memset(dfn,0,sizeof(dfn)); memset(low,0,sizeof(low)); memset(cut,0,sizeof(cut)); memset(vis,0,sizeof(vis)); dfnn=0; //至少需要的出口数 cnt=0;ans1=0;group=0; ans2=1; //方案数 } struct edge{ int to,next; }e[maxn*maxn]; int num=0; void add(int u,int v){ e[num]=edge{v,head[u]}; head[u]=num++; } void tarjin(int u,int fa){ //得到所有的割点 int v; low[u]=dfn[u]=++dfnn; for(int i=head[u];i!=-1;i=e[i].next){ v=e[i].to; if(!dfn[v]){ tarjin(v,u); low[u]=min(low[u],low[v]); if(low[v]>=dfn[u]){ //割点 if(u!=root){ ////如果u不是子树的根节点 cut[u]=1; } else rs++; //根节点子节点数增加 } } else if(v!=fa){ //如果v不是u的父节点,但是v已经访问过 low[u]=min(low[u],dfn[v]); //判断是否能够更新Low } } } void dfs(int u){//DFS搜索一边联通块 ,得到这个连通块中的割点数 int v; vis[u]=group; ans++; //非割点数 for(int i=head[u];i!=-1;i=e[i].next){ v=e[i].to; if(cut[v]&&vis[v]!=group){ ////如果v是割点并且v没有在这个分组内被访问过 cuts++; //割点数增加 vis[v]=group; } if(!vis[v]){ dfs(v); } } } int main(){ LL u,v; cases=1; while(cin>>m&&m){ inti(); n=-1; for(int i=1;i<=m;i++){ cin>>u>>v; add(u,v);add(v,u); if(max(u,v)>n) n=max(u,v); } for(int i=1;i<=n;i++){ if(!dfn[i]){ root=i; rs=0; tarjin(i,i); if(rs>=2) cut[i]=1; ////如果子树根节点的儿子数不少于2个,则这个根节点才是割点 } } for(int i=1;i<=n;i++){ ////枚举所有点来搜索分组 if(!vis[i]&&!cut[i]){ ++group; ans=0;cuts=0; dfs(i); if(cuts==0){ //如果这个连通块没有割点,就是两个出口 ans1+=2; ans2*=(ans)*(ans-1)/2; } else if(cuts==1){ //如果有一个割点 ans1+=1; ans2*=ans;//可以设立在任意一个非割点的地方 } //如果有两个及以上个割点,则无需建立,可以直接到达其他联通块 } } cout<<"Case "<<cases++<<": "<<ans1<<" "<<ans2<<endl; } return 0; }
1522:网络
求割点数量,但是关键是输入比较难以控制。
不确定行数,一组的数据以0结束
#include<iostream> #include<cstring> #include<cmath> #include<algorithm> #include<stack> #include<cstdio> #include<queue> #include<map> #include<vector> #include<set> using namespace std; const int maxn=1010; const int INF=0x3fffffff; typedef long long LL; //求割点数 int low[110],num[110],vis[110]; int n,dfn; bool iscut[110]; //我觉得难点在于输入的控制,之后就是模板了 vector<int> adj[110]; void dfs(int u,int fa){ num[u]=low[u]=++dfn; int child=0; //为了根节点 for(int i=0;i<adj[u].size();i++){ int v=adj[u][i]; if(!num[v]){ child++; dfs(v,u); low[u]=min(low[u],low[v]); if(low[v]>=num[u]&&u!=1) iscut[u]=1; //如果不是根节点而且有回退变 } else if(num[v]<num[u]&&v!=fa){ low[u]=min(low[u],num[v]); //有回退变 } } if(u==1&&child>1) iscut[1]=1; } int main(){ int x,y; while(cin>>n&&n){ for(int i=1;i<=n;i++) adj[i].clear(); while(cin>>x&&x){ while(cin>>y){ adj[x].push_back(y); adj[y].push_back(x); if(getchar()=='\n') break; //!!!! } } dfn=0; int ans=0; memset(iscut,0,sizeof(iscut)); memset(low,0,sizeof(low)); memset(num,0,sizeof(num)); dfs(1,-1); for(int i=1;i<=n;i++) if(iscut[i]) ans++; //计算割点数量 cout<<ans<<endl; } return 0; }
1523:嗅探器
考虑缩点时的过程,我们通过找v为根的搜索子树是否能延伸到时间戳小于u的节点来判断u是否为割点。如果该子树满足这一条件,则去掉u后该子树会与其余部分“失去联系”。
由此我们这样想:如果我们以一个信息中心a为根开始搜索,找到一个非根的割点u;此时若对应的子树根v的时间戳小于等于b的时间戳,则说明b存在于v为根的这颗子树内。
很容易通过阐述说明这一点。由于dfn随dfs序更新,若还没搜到b,则其dfn为0;或者dfn不为0而小于v,则说明b在进入v以前已经被搜到了。
那么如果把u断掉,v的整棵子树都会与根a失去联系,u就是所求的点之一。
or 我们要找出编号最小的割点,并且使这个割点能断开a和b的连接,所以要特判一下
一个点在割点的基础上,还要满足以下条件
要确保终点是从这一条边上通过的,还有终点不能通往这个点以前的任何点
所以:
考虑我们有一个u点,它连了一个v点,那么u点需要满足4个条件。
1、u不是起点终点。因为题目要求在中间服务器上建嗅探器。
2、u是割点。废话
3、终点(y)的dfn应该大于等于v点的dfn,因为要确保终点在v点或之后被访问到,即u点为必经的点。
4、终点(y)的low应该大于等于u点的dfn,因为要确保终点必须要经过u点。
#include<iostream> #include<cstring> #include<cmath> #include<algorithm> #include<stack> #include<cstdio> #include<queue> #include<map> #include<vector> #include<set> using namespace std; const int maxn=500010; const int INF=0x3fffffff; typedef long long LL; /* 考虑缩点时的过程,我们通过找v为根的搜索子树是否能延伸到时间戳小于u的节点来判断u是否为割点。如果该子树满足这一条件,则去掉u后该子树会与其余部分“失去联系”。 由此我们这样想:如果我们以一个信息中心a为根开始搜索,找到一个非根的割点u;此时若对应的子树根v的时间戳小于等于b的时间戳,则说明b存在于v为根的这颗子树内。 很容易通过阐述说明这一点。由于dfn随dfs序更新,若还没搜到b,则其dfn为0;或者dfn不为0而小于v,则说明b在进入v以前已经被搜到了。 那么如果把u断掉,v的整棵子树都会与根a失去联系,u就是所求的点之一。 */int n; int low[maxn],num[maxn],cut[maxn],head[maxn]; int a,b; struct node{ int to,next; }ed[maxn*2]; int dfn,cnt; void add(int f,int t){ ed[++cnt].next=head[f]; ed[cnt].to=t; head[f]=cnt; } void tarjin(int u){ low[u]=num[u]=++dfn; for(int i=head[u];i;i=ed[i].next){ int v=ed[i].to; if(!num[v]){ tarjin(v); low[u]=min(low[u],low[v]); if(low[v]>=num[u]&&u!=a&&num[b]>=num[v]){//此时若对应的子树根v的时间戳小于等于b的时间戳,则说明b存在于v为根的这颗子树内。 cut[u]=1; } } else low[u]=min(low[u],num[v]); } return ; } int main(){ scanf("%d",&n); int x,y; while(scanf("%d %d",&x,&y)){ if(x==0&&y==0) break; add(x,y); add(y,x); } scanf("%d %d",&a,&b); tarjin(a); for(int i=1;i<=n;i++){ if(cut[i]){ printf("%d\n",i); return 0; } } printf("No solution\n"); return 0; }
1524:旅游航道
其实就是计算割边的数目
#include<iostream> #include<cstdio> #include <cstring> #include <algorithm> #include <vector> #include<queue> #define LL long long #define INF 1e12 const int maxn=3e4+10; const int maxm=6e4+10; using namespace std; struct node{ int to,nex; }ed[maxm]; int head[maxn],low[maxn],dfn[maxn],bri,idx=1; void adde(int x,int y){ ed[++idx].nex=head[x]; ed[idx].to=y; head[x]=idx; } int num,n,m; void tarjin(int x,int edge){ low[x]=dfn[x]=++num; for(int i=head[x];i;i=ed[i].nex){ int v=ed[i].to; if(!dfn[v]){ //没访问 tarjin(v,i); low[x]=min(low[x],low[v]); if(low[v]>dfn[x]){ bri++; } } else if(edge!=(i^1)) low[x]=min(low[x],dfn[v]); } } int main(){ while(scanf("%d %d",&n,&m)&&n){ memset(head,0,sizeof(head)); memset(low,0,sizeof(low)); memset(dfn,0,sizeof(dfn)); idx=1;num=0; for(int i=0;i<m;i++){ int a,b;cin>>a>>b; adde(a,b);adde(b,a); } bri=0; tarjin(1,0); cout<<bri<<endl; } return 0; }
1525:电力
求一个图删除一个点之后,联通块最多有多少。
回想我们判断割点时的条件
1° 搜索树 子树中存在点 low[v]>=dfn[u],即返回的时间戳不小于当前点u,删除这个点u,子树分离
2° 根节点 一个根节点root 满足超过两棵子树即为割点,很好理解
回到此题
对于情况一。当前点u,每有一个满足的子树,删除点u后就会有一个联通块,记录个数即可;
此外,还需要考虑点u的父亲,删除割点u后他的父亲与他的子树们也会分裂,个数++
对于情况二。好办,子树的个数
这样下来就得到了每个点删除后形成的新联通块个数
每断开一个点,原联通块消失,分裂成几部分,这也就是答案统计
like bool cut[] -------> int cut[]
#include<iostream> #include<cstring> #include<cmath> #include<algorithm> #include<stack> #include<cstdio> #include<queue> #include<map> #include<vector> #include<set> using namespace std; const int maxn=1e5+10; const int INF=0x3fffffff; typedef long long LL; /* 回想我们判断割点时的条件 1° 搜索树 子树中存在点 low[v]>=dfn[u],即返回的时间戳不小于当前点u,删除这个点u,子树分离 2° 根节点 一个根节点root 满足超过两棵子树即为割点,很好理解 回到此题 对于情况一。当前点u,每有一个满足的子树,删除点u后就会有一个联通块,记录个数即可; 此外,还需要考虑点u的父亲,删除割点u后他的父亲与他的子树们也会分裂,个数++ 对于情况二。好办,子树的个数 这样下来就得到了每个点删除后形成的新联通块个数 每断开一个点,原联通块消失,分裂成几部分,这也就是答案统计 */ int ans,n,m; int cnt; struct node{ int to,next; }ed[maxn<<1]; int head[maxn],low[maxn],num[maxn],dfn; int cut[maxn]; //记录割点个数 void adde(int a,int b){ ed[++cnt].next=head[a]; ed[cnt].to=b; head[a]=cnt; } void tarjin(int x,int fa){ int child=0; low[x]=num[x]=++dfn; for(int i=head[x];i;i=ed[i].next){ int t=ed[i].to; if(!num[t]){ tarjin(t,fa); low[x]=min(low[x],low[t]); if(low[t]>=num[x]&&x!=fa){ //被作为割点的次数 cut[x]++; } if(x==fa) child++; //根节点的孩子数 } else low[x]=min(low[x],num[t]); } if(x!=fa&&cut[x]) cut[x]++; //这个节点消失后上面的fa也会变成1个连通块 if(x==fa&&child>=2) cut[x]+=child; //加上孩子 } int main(){ while(1){ scanf("%d %d",&n,&m); if(n==0&&m==0) break; if(m==0){ printf("%d\n",n-1); continue; } memset(cut,0,sizeof(cut)); memset(low,0,sizeof(low)); memset(num,0,sizeof(num)); memset(head,0,sizeof(head)); memset(ed,0,sizeof(ed)); dfn=0; ans=0; cnt=0; for(int i=1;i<=m;i++){ int x,y; scanf("%d %d",&x,&y); adde(++x,++y); //细节 adde(y,x); } int temp; for(int i=1;i<=n;i++){ if(!num[i]){ ans++; tarjin(i,i); } } temp=ans; for(int i=1;i<=n;i++){ ans=max(ans,temp-1+cut[i]); //在所有的情况里面找最大 } printf("%d\n",ans); } return 0; } //运行错误???什么鬼 #include<bits/stdc++.h> using namespace std; const int N = 1e5+50; struct node{ int next, to; } edge[N<<1]; int head[N], cnt; inline void add(int from, int to) { edge[++cnt] = (node) {head[from], to}, head[from] = cnt; } int tot, low[N], dfn[N], cut[N]; void tarjan(int u, int fa) { low[u] = dfn[u] = ++tot; int child = 0; for(int i = head[u]; i; i = edge[i].next) { int v = edge[i].to; if(!dfn[v]) { tarjan(v, fa), low[u] = min(low[u], low[v]); if(low[v] >= dfn[u] && u != fa) cut[u] ++; if(u == fa) child ++; } else low[u] = min(low[u], dfn[v]); } if(u != fa && cut[u]) cut[u] ++;//fa 所构成的一块 if(u == fa && child >= 2) cut[u] += child ; } int main() { while(1) { #define clear(a) memset(a, 0, sizeof a) int n, m; cin>>n>>m; if(n + m == 0) break; if(m == 0) { printf("%d\n", n - 1); continue; } clear(low), clear(cut), clear(dfn),clear(head),clear(edge); cnt = tot = 0; for(int i = 1, a, b; i <= m; i++) scanf("%d%d", &a, &b), add(++a, ++b), add(b, a); int ans = 0; for(int i = 1; i <= n; i++) if(!dfn[i]) ans ++, tarjan(i, i); int temp = ans; for(int i = 1; i <= n; i++) ans = max(ans, temp - 1 + cut[i]); printf("%d\n", ans); } return 0; }
1526:Blockade
yteotia 城市有 n 个城镇,m 条双向道路。每条道路连接两个不同的城镇,没有重复的道路,所有城镇连通。
输出 n个数,代表如果把第 i 个点去掉,将有多少对点不能互通。
输入 &lt;span id="MathJax-Span-14" class="mrow"&gt;&lt;span id="MathJax-Span-15" class="mi"&gt;n&lt;span id="MathJax-Span-16" class="mo"&gt;,&lt;span id="MathJax-Span-17" class="mi"&gt;m 及 &lt;span id="MathJax-Span-19" class="mrow"&gt;&lt;span id="MathJax-Span-20" class="mi"&gt;m 条边。
输出 n 个数,代表如果把第 i 个点去掉,将有多少对点不能互通。
点无非分为两种
割点 & 非割点
根据题目可得 , 对于非割点答案显然是
ans = (n - 1) * 2;
那么对于割点怎么处理答案呐?
把她分成两部分处理
对于一个点 fa
他的所有子树(搜索树中)的 size 互相乘起来 , 因为子树间互相断开不能联系
然后把所有子树的 size 加起来得 sum , 然后 ans += sum * (n - sum - 1) , 因为所有的子树都无法与外界联系
因为不算 fa 本身 , 所以是 (n - sum - 1) 而非 (n - sum)
但是对于一个点对 (a, b) 断开了显然有 (b, a) 也断开
我们只处理了一个 , 所以最后要把答案乘二
最后对于割点也要再加上 (n - 1) * 2 来处理上文没有处理的 fa 的情况
。。。洛谷上面的题目描述是没有删除i点,而是删除了i点的所有边,所以这个公式才成立的。。。
实在不懂,就画个图,画图比较好理解
/* 点无非分为两种 割点 & 非割点 根据题目可得 , 对于非割点答案显然是 ans = (n - 1) * 2; 那么对于割点怎么处理答案呐? 把她分成两部分处理 对于一个点 fa 他的所有子树(搜索树中)的 size 互相乘起来 , 因为子树间互相断开不能联系 然后把所有子树的 size 加起来得 sum , 然后 ans += sum * (n - sum - 1) , 因为所有的子树都无法与外界联系 因为不算 fa 本身 , 所以是 (n - sum - 1) 而非 (n - sum) 但是对于一个点对 (a, b) 断开了显然有 (b, a) 也断开 我们只处理了一个 , 所以最后要把答案乘二 最后对于割点也要再加上 (n - 1) * 2 来处理上文没有处理的 fa 的情况 */ #include<iostream> #include<cstring> #include<cmath> #include<algorithm> #include<stack> #include<cstdio> #include<queue> #include<map> #include<vector> #include<set> using namespace std; const int maxn=1e5+10; const int INF=0x3fffffff; typedef long long LL; int head[maxn]; //https://blog.csdn.net/weixin_30274627/article/details/97911416 //我要气哭了,为什么还是错的 struct node{ int to,next; }ed[maxn*10]; int n,m; int num[maxn],size[maxn],low[maxn],cut[maxn]; LL ans[maxn]; int cnt,dfn; void adde(int x,int y){ ed[++cnt].to=y; ed[cnt].next=head[x]; head[x]=cnt; } void tarjin(int u){ low[u]=num[u]=++dfn; // printf("%d\n",u); int child=0; size[u]=1; //大小 int tot=0; for(int i=head[u];i;i=ed[i].next){ //妈了个鸡 int v=ed[i].to; if(!num[v]){ tarjin(v); size[u]+=size[v]; low[u]=min(low[u],low[v]); if(low[v]>=num[u]){ tot+=size[v]; ans[u]+=1ll*size[v]*(n-size[v]); //他的每个子树和其他节点的对数 if(u!=1||++child>1){ cut[u]=1;//是割点 } } } else low[u]=min(low[u],num[v]); } if(cut[u]){ ans[u]+=1ll*(tot+1)*(n-tot-1)+(n-1); //莫法,画图理解 } else ans[u]=2*(n-1); } int main(){ scanf("%d %d",&n,&m); for(int i=1;i<=m;i++){ int x,y; scanf("%d %d",&x,&y); adde(x,y); adde(y,x); } //不用循环,以为整个图是联通的 tarjin(1); for(int i=1;i<=n;i++) printf("%lld\n",ans[i]); return 0; }