无向图的联通分量
割点:在一个联通分量里面有一些关键点,如果删除它,会把这个联通分量分为更多。
割边——双连通问题
有多少个割点: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就是割边
int low[maxn],num[maxn]; vector<int> g[maxn]; bool iscut[maxn]; int dfn; void dfs(int u,int fa){ low[u]=num[u]=dfn++; //初始值为DFS访问的顺序 int child=0; for(int i=0;i<g[u].size();i++){ int v=g[u][i]; if(!num[v]){ child++; dfs(v,u); low[u]=min(low[u],low[v]); //用后代的Low更新爸爸的low if(low[v]>=num[u]&&u!=1) //判断割点1:不是根节点的判断 iscut[u]=1; } else if(num[v]<num[u]&&v!=fa){ low[u]=min(low[u],num[v]); //处理回退边,fa也是u的邻居,但是之前已经访问过,所以不需要处理 } } if(u==1&&child>=2) iscut[1]=1; //根节点 } int main(){ int ans,n; //输入图 memset(low,0,sizeof(low)); memset(num,0,sizeof(num)); dfn=0; memset(iscut,0,sizeof(iscut)); ans=0; dfs(1,-1); for(int i=1;i<=n;i++) ans+=iscut[i]; cout<<ans<<endl; return 0; }
双连通分量:
在一个联通图中任选两点,如果至少存在两条“点不重复”的路径,称为点双连通
点双连通极大子图:点双连通分量(没有割点)
边双连通分量:如果至少存在两条“边不重复”的路径,成为边双连通(没有割边)
点双连通分量:Tarjan,在dfs的时候,把遍历过程中的点保存起来,就可以得到点双连通分量,保存在栈,找到割点就拿出来,注意存在栈的是边
边双连通分量:缩点的技术:
1)首先找出图G的所有边双连通分量,DFS时,所有点生成low值,low值相同的就是同一个SCC,,有多少个SCC就有多少个边双连通分量
2)把每个边双连通分量看作一个点,low值相同的合并为一个点
3)转化为一棵树,即至少在缩点树上面增加多少条边才能变为一个边双连通图,即(度为1的点+1)/2
下面的是求需要连接多少条边才能变为双连通分量,只需要low数组
ps。也可以用tarjin的算法
#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 n,m,low[maxn],dfn; vector<int> g[maxn]; void dfs(int u,int fa){ low[u]=++dfn; for(int i=0;i<g[u].size();i++){ int v=g[u][i]; if(v==fa) continue; if(!low[v]) dfs(v,u); low[u]=min(low[u],low[v]); } } int degree[maxn]; //计算每个缩点的度数 int tarjan(){ memset(degree,0,sizeof(degree)); for(int i=1;i<=n;i++){ for(int j=0;j<g[i].size();j++){ if(low[i]!=low[g[i][j]]){ degree[low[i]]++; } } } int res=0; for(int i=1;i<=n;i++){ if(degree[i]==1) res++; } return res; } int main(){ while(~scanf("%d %d",&n,&m)){ memset(low,0,sizeof(low)); for(int i=0;i<=n;i++) g[i].clear(); for(int i=1;i<=m;i++){ int a,b; scanf("%d %d",&a,&b); g[a].push_back(b); g[b].push_back(a); } dfn=0; dfs(1,-1); int ans=tarjan(); printf("%d ",(ans+1)/2); } return 0; }
1520:【 例 1】分离的路径
给出一个无向图,求要变成一个边双连通分量,要加多少条边
#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 f,r; //得到的是边双连通分量 //下面自己额这个有一个通不过 int low[5010]; int num=0,dfn=0; vector<int> adj[5010]; void dfs(int x,int fa){ low[x]=++dfn; for(int i=0;i<adj[x].size();i++){ int v=adj[x][i]; if(v==fa) continue; if(!low[v]) dfs(v,x); low[x]=min(low[x],low[v]); } } //度为1 的个数,就是需要的边 int in[5010]; int js(){ memset(in,0,sizeof(in)); ///计算每个缩点的度数 for(int i=1;i<=f;i++){ for(int j=0;j<adj[i].size();j++){ if(low[i]!=low[adj[i][j]]) in[low[i]]++; } } for(int i=1;i<=f;i++){ if(in[i]==1) num++; } return (num+1)/2; } int main(){ scanf("%d %d",&f,&r); int x,y; for(int i=0;i<r;i++){ scanf("%d %d",&x,&y); adj[x].push_back(y); adj[y].push_back(x); } dfs(1,-1); int road=js(); cout<<road<<endl; return 0; } #include<iostream> #include<bits/stdc++.h> using namespace std; int dfn[5010]; int low[5010]; int f[5010],head[5010],in[5010],flag[5010]; int vis[5010],tot,cnt; //tarjin判环,统计入度为1的点的个数,新建道路可连接2个入度为1的点,故道路为入度为1的点的个数的一半。 stack<int> st; struct node{ int u,v,next; }e[5010*2]; int n,m; int summ=-1; void add(int u,int v){ e[++summ]=node{u,v,head[u]}; head[u]=summ; } void tarjin(int u){ dfn[u]=low[u]=++cnt; st.push(u); vis[u]=1; int v; for(int i=head[u];i;i=e[i].next){ v=e[i].v; if(flag[i]==0||flag[i^1]==0){ //相邻的两条边 flag[i]=flag[i^1]=1; if(!dfn[v]){ tarjin(v); low[u]=min(low[u],low[v]); } else if(vis[v]){ //回退边 ,如果要用这种判断方式的话,就需要在下面改变为vis[i]=0 low[u]=min(low[u],dfn[v]); } } } if(low[u]==dfn[u]){ //也是有缩点的技术 tot++; do{ v=st.top(); st.pop(); f[v]=tot; vis[v]=0; //可以出来了 }while(u!=v); } } int main() { cin>>n>>m; for(int i=1;i<=m;i++){ int u,v; cin>>u>>v; add(u,v);add(v,u); } for(int i=1;i<=n;i++){ if(!dfn[i]) tarjin(i); } for(int i=1;i<=2*m;i+=2){ //看这个循环 int u=e[i].u,v=e[i].v; if(f[u]==f[v]) continue; in[f[v]]++;in[f[u]]++; } int ans=1; for(int i=1;i<=tot;i++){ if(in[i]==1) ans++; } cout<<ans/2<<endl; }
1521:【 例 2】矿场搭建
煤矿工地可以看成是由隧道连接挖煤点组成的无向图。为安全起见,希望在工地发生事故时所有挖煤点的工人都能有一条出路逃到救援出口处。于是矿主决定在某些挖煤点设立救援出口,使得无论哪一个挖煤点坍塌之后,其他挖煤点的工人都有一条道路通向救援出口。
请写一个程序,用来计算至少需要设置几个救援出口,以及不同最少救援出口的设置方案总数。
这道题蛮复杂,要先用tarjin算割点,然后用DFS出每个分量里面的割点数量,根据不同的数量不同的处理
分类讨论:
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()==' ') 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; }
有向图的连通性
强连通:如果两个点:u,v是互相达到的
无向图:联通 有向图:强连通
图中有多少SCC:暴力O(V^2+E)
kosaraju算法O(V+E):反图
(1)有向图G,建立反图rG,不会改变连通性
(2)对原图G做DFS,标记点的先后顺序,递归在最底层的点标记最小,回退过程中,其他点的优先级加大
(3)在反图RG上做DFS,从优先级大的开始做
下面的代码可能有点问题
#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=10010; const int INF=0x3fffffff; typedef long long LL; vector<int> g[maxn],rg[maxn]; vector<int> S; //存第一次ds的结果,即标记的先后顺序 int vis[maxn],sccno[maxn]; //这个是所属的SCC int cnt; //联通分量个数 void dfs1(int u){ if(vis[u]) return ; vis[u]=1; for(int i=0;i<g[u].size();i++){ dfs1(g[u][i]); } S.push_back(u); //优先级大的在后面 } void dfs2(int u){ if(sccno[u]) return; sccno[u]=cnt; for(int i=0;i<rg[u].size();i++) dfs2(rg[u][i]); //对反图 } void kosaraju(int n){ cnt=0; S.clear(); memset(vis,0,sizeof(vis)); memset(sccno,0,sizeof(sccno)); for(int i=1;i<=n;i++) dfs1(i); for(int i=0;i<S.size();i++) cout<<S[i]<<" "; for(int i=n-1;i>=0;i--){ if(!sccno[i]) { cnt++; dfs2(S[i]); //倒着,因为倒着的优先级大 } } } int main(){ int n,m,u,v; while(scanf("%d %d",&n,&m),n!=0||m!=0){ for(int i=0;i<n;i++){ g[i].clear(); rg[i].clear(); } for(int i=0;i<m;i++){ scanf("%d %d",&u,&v); g[u].push_back(v); rg[v].push_back(u); } kosaraju(n); if(cnt==1) printf("Yes "); else printf("No "); } return 0; }
Tarjan算法O(V+E):用栈分离不同的SCC
最先入栈的点:是这个SCC的祖先,他的num[]=low[]
比上面要快些
#include<iostream> #include<cstring> #include<cmath> #include<algorithm> #include<cstdio> #include<queue> #include<map> #include<vector> #include<set> using namespace std; const int maxn=10010; const int INF=0x3fffffff; typedef long long LL; int cnt=0,dfn=0; int low[maxn],num[maxn]; int sccno[maxn],stack[maxn],top; vector<int> g[maxn]; void dfs(int u){ stack[top++]=u; low[u]=num[u]=++dfn; for(int i=0;i<g[u].size();i++){ int v=g[u][i]; if(!num[v]){ dfs(v); low[u]=min(low[u],low[v]); } else if(!sccno[v]){ //处理回退边 low[u]=min(low[u],num[v]); } } if(low[u]==num[u]){ //栈底的点是SCC的祖先, cnt++; while(1){ int v=stack[top--]; sccno[v]=cnt; if(u==v) break; //到达了祖先 } } } void tarjan(int n){ cnt=top=dfn=0; memset(sccno,0,sizeof(sccno)); memset(num,0,sizeof(num)); memset(low,0,sizeof(low)); for(int i=1;i<=n;i++){ if(!num[i]) dfs(i); } } int main(){ int n,m,u,v; while(scanf("%d %d",&n,&m),n!=0||m!=0){ for(int i=0;i<=n;i++){ g[i].clear(); } for(int i=0;i<m;i++){ scanf("%d %d",&u,&v); g[u].push_back(v); } tarjan(n); if(cnt==1) printf("Yes "); else printf("No "); } return 0; }
1513:【 例 1】受欢迎的牛
感觉很多题里面在缩点之后,是需要计算度的。
把每一个极大联通子图看成一个点(缩点),但后再组成一个图,这个图是没有环的在这个图中,至少存在1个出度为0的点,如果出度为0的点大于1,那么是无法确定的,但是如果出度为1的点只有1个,而且其他的点都能到达他的话,就可以确定答案就是那个点里面点数目
#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=10050; const int INF=0x3fffffff; typedef long long LL; //有向图:极大连通子图 //把每一个极大联通子图看成一个点(缩点),但后再组成一个图,这个图是没有环的 //在这个图中,至少存在1个出度为0的点,如果出度为0的点大于1,那么是无法确定的 //但是如果出度为1的点只有1个,而且其他的点都能到达他的话,就可以确定 //答案就是那个点里面点数目 struct node{ int to,next; }ed[maxn*20]; int head[maxn],num[maxn],low[maxn]; int id[maxn],col[maxn]; //分别是每个点所属的极大联通分量,然后是这个连通分量的数量 int deg[maxn]; //这个极大联通子图的出度 bool inst[maxn]; //在不在栈里面 stack<int> st; int cnt,tot,nu,dfn; void add(int x,int y){ ed[++nu].to=y; ed[nu].next=head[x]; head[x]=nu; } void tarjin(int u){ num[u]=low[u]=++dfn; inst[u]=1; st.push(u); 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]); } else if(inst[v]){ //有回退边 low[u]=min(low[u],num[v]); } } int k; if(low[u]==num[u]){ cnt++; do{ k=st.top(); st.pop(); inst[k]=0; //这里需要改变 id[k]=cnt; col[cnt]++; }while(k!=u); } } int main(){ int n,m; scanf("%d %d",&n,&m); for(int i=0;i<m;i++){ int x,y; scanf("%d %d",&x,&y); add(x,y); } for(int i=1;i<=n;i++){ if(!num[i]) tarjin(i); } for(int i=1;i<=n;i++){ //枚举每条边 for(int j=head[i];j;j=ed[j].next){ int u=ed[j].to; if(id[i]!=id[u]) deg[id[i]]++; //i这个极大连通子图的出度+1 } } for(int i=1;i<=cnt;i++){ if(!deg[i]){ if(tot) { printf("0 ");break; //因为只能有一个 } tot=i; } } printf("%d ",col[tot]); return 0; }
消息的传递2269
我们的郭嘉大大在曹操这过得逍遥自在,但是有一天曹操给了他一个任务,在建邺城内有N(<=1000)个袁绍的奸细,将他们从1到N进行编号,同时他们之间存在一种传递关系,即若C[i,j]=1,则奸细i能将消息直接传递给奸细j。
现在曹操要发布一个假消息,需要传达给所有奸细,而我们的郭嘉大大则需要传递给尽量少的奸细使所有的奸细都知道这一个消息,问我们至少要传给几个奸细。
道题求SCC,求完了之后还有看还需要给几个人说,才能让所有人都知道,所以要计算出入度为0的SCC数量
求scc的时候用tarjin、kosaraju都可以
#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; //这道题求SCC,求完了之后还有看还需要给几个人说,才能让所有人都知道,所以要计算出入度为0的SCC数量 //求scc的时候用tarjin、kosaraju都可以 int vis[maxn]; int cnt; int n,num; struct node{ int to,next; }edge[maxn*10],reedge[maxn*20]; int head[maxn],rehead[maxn]; void add(int x,int y){ edge[num].to=y; edge[num].next=head[x]; head[x]=num; reedge[num].to=x; reedge[num].next=head[y]; rehead[y]=num; num++; } int id[maxn]={0}; vector<int> ori; void dfs(int x){ vis[x]=1; for(int i=head[x];i!=-1;i=edge[i].next){ int v=edge[i].to; if(!vis[v]) dfs(v); } ori.push_back(x); } void redfs(int x){ vis[x]=1; id[x]=cnt; for(int i=rehead[x];i!=-1;i=reedge[i].next){ int v=reedge[i].to; if(!vis[v]) redfs(v); } } void kosaraju(){ for(int i=1;i<=n;i++){ if(!vis[i]) dfs(i); } memset(vis,0,sizeof(vis)); cnt=0; for(int i=ori.size()-1;i>=0;i--){ if(!vis[ori[i]]){ cnt++; redfs(ori[i]); } } } int main(){ scanf("%d",&n); int x; memset(head,-1,sizeof(head)); memset(rehead,-1,sizeof(rehead)); memset(vis,0,sizeof(vis)); for(int i=1;i<=n;i++){ for(int j=1;j<=n;j++){ scanf("%d",&x); if(x) { add(i,j); } } } memset(vis,0,sizeof(vis)); kosaraju(); printf("%d",cnt); return 0; } #include<iostream> #include<cstdio> #include<stack> #include<vector> using namespace std; int dfn[1010],low[1010],visitime,scc,Belong[1010],n,ans; bool Instack[1010],ru[1010]; stack<int> s; vector<int> G[1010]; inline int _min(int a,int b) { if(a<b) return a; return b; } inline bool jiancha(int a,int b) { for(int i=0;i<G[a].size();i++) if(G[a][i]==b) return 1; return 0; } inline void Tarjan(int u) { s.push(u); Instack[u]=1; dfn[u]=low[u]=++visitime; for(int v=0;v<G[u].size();v++) { if(!dfn[G[u][v]]) { Tarjan(G[u][v]); low[u]=_min(low[u],low[G[u][v]]); } else if(dfn[G[u][v]]&&Instack[G[u][v]]) { low[u]=_min(low[u],dfn[G[u][v]]); } } if(dfn[u]==low[u]) { scc++; int v; do { v=s.top(); s.pop(); Belong[v]=scc; Instack[v]=0; } while(u!=v); } } int main() { scanf("%d",&n); for(int i=1;i<=n;i++) { for(int j=1;j<=n;j++) { int a; scanf("%d",&a); if(a==1) G[i].push_back(j); } } for(int i=1;i<=n;i++) { if(dfn[i]==0) Tarjan(i); } for(int i=1;i<=n;i++) { for(int j=1;j<=n;j++) { if(Belong[i]!=Belong[j]&&jiancha(i,j)==1) //属于不同的连通块,但是彼此可达 { ru[Belong[j]]=1; } } } for(int i=1;i<=scc;i++) { if(ru[i]==0) ans++; } printf("%d",ans); return 0; }
Going from u to v or from v to u?(POJ2762)3601
给你一个有n个点m条边有向图,让你判断该图是否对于任意的一对顶点(X,Y),存在从X到Y的路径或者从Y到X的路径。
单连通!!!
虽然本题是求单连通的,但是我们需要先求强连通分量,因为,强连通分量中存在双向路径,因此可以缩点,缩点后就好处理多了。
如果要满足题意,缩点后的树必须是一条链,而且所有边的方向都是一样的,如果出现分支,很容易证明会出现不可到达的一对点。
那么剩下的就是求最长链的顶点数是否等于强连通分量的个数了。
那么就可以使用拓扑排序,或者直接DFS。
拓扑排序中,不能出现同时有两个点的入度为0。
DFS从入度为0的顶点开始搜,是求出深搜的层数,因为单链的话层数是等于顶点数的
https://blog.csdn.net/iteye_6233/article/details/82270727 但是最后有个点过不了,不知道为什么TAT
#include <iostream> #include <map> #include <cstdio> #include <stack> #include <cstring> #include <algorithm> #define MAXN 10005 #define MAXM 100005 #define INF 1000000000 using namespace std; int n, m; int scc;//强连通分量 int indexx;//每个节点的dfs访问次序编号 int dfn[MAXN];//标记结点i的dfs访问次序 int low[MAXN];//记录节点u或u的子树中的所有节点的最小标号 int fa[MAXN];//属于哪个分支 bool instack[MAXN];//是否在栈中 int in[MAXN], head[MAXN], e; int out[MAXN];//出度 stack <int>s; int tmp; int vis[MAXN]; struct Edge { int v, next; }edge[MAXM]; void insert(int x, int y) { edge[e].v = y; edge[e].next = head[x]; head[x] = e++; } void tarjan(int u) { dfn[u] = low[u] = ++indexx; s.push(u); instack[u] = true; for (int j = head[u]; j != -1; j = edge[j].next) { int v = edge[j].v; if(dfn[v] == 0)//未曾访问过 { tarjan(v); low[u] = min(low[u], low[v]); } else if(instack[v]) low[u] = min(low[u], dfn[v]); } if(dfn[u] == low[u]) { scc++; while(1) { int tmp = s.top(); s.pop(); instack[tmp] = 0; fa[tmp] = scc; if(tmp == u) break; } } } void init() { scc = indexx = 0; memset(dfn, 0, sizeof(dfn)); memset(instack, 0, sizeof(instack)); e = 0; memset(head, -1, sizeof(head)); memset(in, 0, sizeof(in)); memset(out, 0, sizeof(out)); } void dfs(int v, int deep) { tmp = max(tmp, deep); vis[v] = 1; for(int i = head[v]; i != -1; i = edge[i].next) if(!vis[edge[i].v]) { if(fa[edge[i].v] == fa[v]) dfs(edge[i].v, deep); else dfs(edge[i].v, deep + 1); } } void solve() { 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 != -1; j = edge[j].next) { int u = fa[i]; int v = fa[edge[j].v]; if(u != v) { out[u]++; in[v]++; } } } tmp = 0; memset(vis, 0, sizeof(vis)); for(int i = 1; i <= n; i++) if(in[fa[i]] == 0) { dfs(i, 1); break; } printf(tmp == scc ? "Yes " : "No "); } int main() { int T, x, y; scanf("%d", &T); while(T--) { init(); scanf("%d%d", &n, &m); while(m--) { scanf("%d%d", &x, &y); insert(x, y); } solve(); } return 0; }
1514:【例 2】最大半连通子图
//求极大半连通子图 极大半连通子图 极大半连通子图 极大半连通子图 极大半连通子图 极大半连通子图
// 此题题意就是:缩点后,求一条含有最多点数的DAG链,(这就是题中的最大半联通子图),并求出有多少个不同的最长链。
//主要有三个部分:1.缩点;2.DAG记忆化;3.离散化去重;(难点)4.链式前向星。
///思路:
//因为存在环,所以先缩点,缩完点以后,此时图就变成DAG,然后进行一遍dfs扫描,dfs时注意用记忆化,然后边扫边记录得到求最大值数组和求个数数组,最后统计答案即可。
//注意点:
//1.在求个数的时候要时刻取模,取模不能停!2.因为要求种类个数,所以要保证不重边(最大值与重边无关,但种类与重边有关)所以需要离散化
#include <bits/stdc++.h> using namespace std; const int N=1e5+5,M=1e6+5; int n,m,MOD,cnt,ans,MAX,k,now1,now2; int head[N],u[M],v[M];
//记录输入的u[]v[]是便于后面统计 int col,now,top,dfn[N],low[N],color[N],sta[N],si[N]; int rd[N],cd[N],dep[N],sum[N]; bool f[N]; struct edge{int next,from,to;}e[M]; struct node{int u,v;}C[M],b[M]; inline bool cmp(node a,node b){return (a.u<b.u || a.u==b.u && a.v<b.v|| a.u==b.u && a.v==b.v);} //排序,因为要离散化,去重 inline void add(int u,int v){cnt++;e[cnt].next=head[u];e[cnt].to=v;head[u]=cnt;} inline void tarjan(int u) { dfn[u]=low[u]=++now; sta[++top]=u; for (register int i=head[u]; i; i=e[i].next) { if (!dfn[e[i].to]) { tarjan(e[i].to); low[u]=min(low[u],low[e[i].to]); } else if (!color[e[i].to]) //还没有确定是哪个连通块,也就是代替了vis[]数组的作用 low[u]=min(low[u],dfn[e[i].to]); } if (low[u]==dfn[u]) { color[u]=++col; si[col]++; while (sta[top]!=u) color[sta[top]]=col,si[col]++,top--; top--; } } inline void dfs(int u,int fa) { f[u]=true; if (!cd[u]) {dep[u]=si[u]; sum[u]=1; return;} //出度为0,到了递归边界,开始会退,计算
//看出来了吗,这其实是回溯
//dep[]数组就是这条链能够拥有的节点数,sum[]数组是方案数 for (register int i=head[u]; i; i=e[i].next) if (e[i].to!=fa) { if (!f[e[i].to]) dfs(e[i].to,u); if (dep[e[i].to]+si[u]>dep[u]) dep[u]=dep[e[i].to]+si[u],sum[u]=sum[e[i].to]%MOD; else if (dep[e[i].to]+si[u]==dep[u]) sum[u]=(sum[u]+sum[e[i].to])%MOD; } } int main(){ memset(head,0,sizeof(head)); memset(dfn,0,sizeof(dfn)); scanf("%d%d%d",&n,&m,&MOD); for (register int i=1; i<=m; ++i) scanf("%d%d",&u[i],&v[i]),add(u[i],v[i]); //缩点 for (register int i=1; i<=n; ++i) if (!dfn[i]) tarjan(i); //离散化去重 for (register int i=1; i<=m; ++i) if (color[u[i]]!=color[v[i]]) k++,C[k].u=color[u[i]],C[k].v=color[v[i]]; //新边,但是还没有去重,之所以要这个是因为还需要排序 sort(C+1,C+k+1,cmp);
//在这里排序 cnt=0; cnt++;
b[cnt].u=C[1].u,b[cnt].v=C[1].v;
now1=b[cnt].u,now2=b[cnt].v; for (register int i=2; i<=k; ++i) if (C[i].u!=now1|| C[i].v!=now2) cnt++,b[cnt].u=C[i].u,b[cnt].v=C[i].v,now1=b[cnt].u,now2=b[cnt].v; //这才是真正的边,没有重复的 //重新建图 memset(head,0,sizeof(head)); memset(e,0,sizeof(e)); for (register int i=1; i<=cnt; ++i) { cd[b[i].u]++,rd[b[i].v]++; e[i].next=head[b[i].u]; e[i].from=b[i].u; e[i].to=b[i].v; head[b[i].u]=i; //这里 } //记忆化搜索 for (register int i=1; i<=col; ++i) if (!rd[i] && !f[i]) dfs(i,0); //入度为0,且没有访问过 //统计答案 MAX=0,ans=0; for (register int i=1; i<=col; ++i) { if (dep[i]>MAX) { MAX=dep[i]; ans=sum[i]; } else if (MAX==dep[i]) ans=(ans+sum[i])%MOD; } printf("%d ",MAX); printf("%d ",ans); return 0; }
1573 -- 【图的连通性】上白泽慧音(classroom)2916
第1行:两个正整数N,M。
第2..M+1行:每行三个正整数a,b,t, t = 1表示存在从村庄a到b的单向道路,t = 2表示村庄a,b之间存在双向通行的道路。保证每条道路只出现一次。
第1行: 1个整数,表示最大的绝对连通区域包含的村庄个数。
第2行:若干个整数,依次输出最大的绝对连通区域所包含的村庄编号。
#include<iostream> #include<cstdio> #include<cstring> using namespace std; const int maxn=2e5+7; const int inf=0x3f3f3f3f; struct note{ int to,nex; }e[maxn*3]; int col,num,head[maxn],dfn[maxn],low[maxn],de[maxn],co[maxn],si[maxn],stk[maxn]; int top,n,m; int cnt=-1; void add(int x,int y) { static int cnt=0; cnt++; e[cnt].to=y; e[cnt].nex=head[x]; head[x]=cnt; } void tarjan(int u) { dfn[u]=low[u]=++num; stk[++top]=u; for(int i=head[u];i;i=e[i].nex) { int v=e[i].to; if(!dfn[v]){ tarjan(v); low[u]=min(low[u],low[v]); } else if(!co[v])low[u]=min(low[u],dfn[v]); } if(low[u]==dfn[u]) { co[u]=++col; ++si[col]; while(stk[top]!=u) { ++si[col]; co[stk[top]]=col; --top; } --top; } } int main() { scanf("%d%d",&n,&m); for(int i=1;i<=m;i++) { int x,y,f; cin>>x>>y>>f; if(f==1)add(x,y); if(f==2)add(x,y),add(y,x); } for(int i=1;i<=n;i++)if(!dfn[i])tarjan(i); for(int i=1;i<=col;i++)cnt=max(cnt,si[i]); cout<<cnt<<endl; for(int i=1;i<=n;i++) { if(si[co[i]]==cnt) { int now=co[i]; for(int j=i;j<=n;j++)if(co[j]==now)cout<<j<<" "; return 0; } } return 0; }
1574 -- 【图的连通性】校园网2413
子任务a:请编一个程序,根据学校间支援协议(各个学校的支援名单),计算最少需要将一个新软件直接提供给多少个学校,才能使软件通过网络被传送到所有学校。
子任务b:如果允许在原有支援协议上添加新的支援关系。则总可以形成一个新的协议,使得此时只需将一个新软件提供给任何一个学校,其他所有学校就都可以通过网络获得该软件。编程计算最少需要添加几条新的支援关系。
分析:
任务A:
需要求最多在多少学校发送新软件,其实就是求缩点后入度为0的个数(如果入度不为0就可以从其他学校传过来)
任务B:
求入度为0的点数与出度为0的点的较大值。
因为任务B要求在任意学校投放软件使得所有学校都能收到,所以很明显是需要整张图形成一个环,而环中所有节点入度和出度都不为0,所以需要把所有入度和出度的点度数增加。
那么最少的方案就是把出度为0的点连上入度为0的点,所以任务B的答案就是入度为0与出度为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; /*任务A: 需要求最多在多少学校发送新软件,其实就是求缩点后入度为0的个数(如果入度不为0就可以从其他学校传过来) 任务B: 求入度为0的点数与出度为0的点的较大值。 因为任务B要求在任意学校投放软件使得所有学校都能收到,所以很明显是需要整张图形成一个环,而环中所有节点入度和出度都不为0,所以需要把所有入度和出度的点度数增加。 那么最少的方案就是把出度为0的点连上入度为0的点,所以任务B的答案就是入度为0与出度为0的点的较大值 */ int n; struct node{ int to,next; }edge[100000]; int head[110],num[110],low[110],vis[110]; int in[110],out[110]; int id[110],sta[110]; int cnt,col; void link(int a,int b){ cnt++; edge[cnt].to=b; edge[cnt].next=head[a]; head[a]=cnt; } int dfn,top; void tarjin(int u){ low[u]=num[u]=++dfn; sta[top++]=u; vis[u]=1; for(int i=head[u];i;i=edge[i].next){ int v=edge[i].to; if(!num[v]){ tarjin(v); low[u]=min(low[u],low[v]); } else if(!id[v]){ low[u]=min(low[u],num[v]); } } if(low[u]==num[u]){ col++; do{ top--; id[sta[top]]=col; }while(sta[top]!=u); } } int lin[100000][3]; int main(){ scanf("%d",&n); int x,k=0; for(int i=1;i<=n;i++){ scanf("%d",&x); while(x!=0){ link(i,x); k++; lin[k][1]=i; lin[k][2]=x; //记录下来 scanf("%d",&x); } } for(int i=1;i<=n;i++){ if(!num[i]) tarjin(i); } for(int i=1;i<=k;i++){ if(id[lin[i][1]]!=id[lin[i][2]]){ out[id[lin[i][1]]]++; in[id[lin[i][2]]]++; } } int ans1=0,ans2=0; for(int i=1;i<=col;i++){ if(in[i]==0) ans1++; if(out[i]==0) ans2++; } if(col==1) cout<<1<<endl<<0; else cout<<ans1<<endl<<max(ans2,ans1); return 0; }