对于一个有向图有如下概念:
连通分量:对于分量中任意两点u,v,必然可以从u走到v,且从v走到u。
强连通分量:极大连通分量。(加上任一点后都不是连通分量)
强连通分量可以将任意一个有向图(通过缩点)转化成有向无环图(拓扑图,DAG)
缩点:将所有的强连通分量缩成一个点
DAG求最短路和最长路可以递推来做,时间复杂度是线性的
算法实现:
基于dfs,将所有的边分为四类:
树枝边(1):x是y的父节点
前向边(2):x是y的祖先节点
后向边(3):从子节点指向祖先节点
横叉边(4):往之前搜过的其他分支搜
判断一个点是否在强连通分量(scc)中:
情况1:通过后向边走到某一祖先节点
情况2:通过横叉边走到另一分支,再通过另一分支走到某一祖先节点
Tarjan算法求scc:
引入时间戳(按照dfs搜索顺序给每个点一个编号),具体实现:
对于每个点u,引入两个时间戳:
dfn[u]表示遍历到u的时间戳
low[u]表示从u开始遍历,所能遍历到的最小时间戳
如果u是所在强连通分量的最高点,那么dfn[u]==low[u]
代码模板:
void tarjan(int u)
{
dfn[u]=low[u]=++timestamp;
stk[++top]=u,in_stk[u]=1;//栈用来装当前正在遍历的强连通分量
for(int i=h[u];~i;i=ne[i])
{
int j=e[i];
if(!dfn[j])//当前点没有时间戳,证明之前没有搜到过
{
tarjan(j);
low[u]=min(low[u],low[j]);
}
else if(in_stk[j])//当前点在栈中,证明和u同在一个强连通分量中,而且时间戳肯定小于u的时间戳,故而去更新low[u]
{
low[u]=min(low[u],dfn[j]);
}
}
if(dfn[u]==low[u])
{
int y;
++scc_cnt;
do{
y=stk[top--];
in_stk[y]=0;
id[y]=scc_cnt;
}while(y!=u);
}
}
ps:最特殊的情况就是每个点自己是一个强连通分量。
时间复杂度:O(n+m)
然后缩点:
for i~n(遍历所有的点)
for h[i]~-1(遍历i的所有邻边)
if(i,j不在同一scc)
i->j(建边)
连通分量编号递减的顺序一定是拓扑序。按照上述的统计方式,在拓扑图中一个点指向另一个点的边不止一条,我们需要都记录下来。
1174. 受欢迎的牛(活动 - AcWing)
思路:这题由于喜爱关系是可以传递的,所以实际上可以用floyd算传递闭包,但是这里既然是scc的例题,那么我们就用tarjan算法来写。
如果a认为b受欢迎,那么我们就建立一条a指向b的边,那么如果得到得是一个有向无环图(DAG),答案取决于终点的数量,也即出度为0的点的数量。如果只有一个,那么肯定是这个点,如果有多个,那么这些终点之间没有指向关系,就不符合题意了。但是题目不能保证是有向无环图,所以我们可以通过计算强连通分量和缩点建立一张有向无环图。最后如果只有一个强连通分量终点,那么答案就是这个强连通分量中点的个数,我们可以在tarjan计算的时候统计一下个数。
这题其实还有一个比较特殊的点,我们只需要找终点,所以实际不用真的建图,只要统计每个分量的出度即可。
#include<bits/stdc++.h>
using namespace std;
const int N=10010,M=100010;
int n,m;
int h[N],e[M],ne[M],idx;
int scc;
int id[N],stk[N],sz[N],st[N];
int dfn[N],low[N];
int cd[N];
int tp;
int top;
void add(int a,int b)
{
e[idx]=b,ne[idx]=h[a],h[a]=idx++;
}
void tarjan(int u)
{
dfn[u]=low[u]=++tp;
stk[++top]=u,st[u]=1;
for(int i=h[u];i!=-1;i=ne[i])
{
int j=e[i];
if(!dfn[j])
{
tarjan(j);
low[u]=min(low[u],low[j]);
}
else if(st[j])
{
low[u]=min(low[u],dfn[j]);
}
}
if(low[u]==dfn[u])
{
int y;
++scc;
do{
y=stk[top--];
st[y]=0;
id[y]=scc;
sz[scc]++;
}while(y!=u);
}
}
int main()
{
scanf("%d%d",&n,&m);
memset(h,-1,sizeof h);
for(int i=1;i<=m;i++)
{
int a,b;
scanf("%d%d",&a,&b);
add(a,b);
}
for(int i=1;i<=n;i++)//整个图可能并不连通
if(!dfn[i])
tarjan(i);
for(int i=1;i<=n;i++)
{
for(int j=h[i];j!=-1;j=ne[j])
{
int z=e[j];
if(id[i]!=id[z]) cd[id[i]]++;
}
}
int z=0,sum=0;
for(int i=1;i<=scc;i++)
{
if(!cd[i])
{
z++;
sum += sz[i];
}
}
if(z==1) printf("%d",sum);
else printf("0");
}
367. 学校网络(367. 学校网络 - AcWing题库)
这里我们先看第一问,需要给几个学校才能使得每个学校都有,既然是有向图,那么我们就来考虑最特殊的拓扑图,反正非拓扑图也能通过缩点转化成有向图。显然对于一个拓扑图,我们只要给所有的起点,那么就可以传到所有的终点去。所以对于第一问,我们只需要把原图转化成拓扑图,然后统计起点个数即可。对于第二问,最少添加几条边可以使得我们提供给任何一个学校,其他学校都可以获得,我们还是按照拓扑图来看。
我们令起点各个数为怕p,终点的个数为q。我们先证p<=q的时候的情况,大于等于的时候类似:
如果p==1,那么我们只用从所有的终点连一条指向起点的边即可,因为起点可以到任何一个点
如果p>1,那么q>=p>=1,必然可以找到两个不同的起点p1,p2,可以走到两个不同的终点q1,q2,如下图:
我们可以在去q1和p2之间连一条边:
那么p2和q1就变成中间节点了,可以从起点和终点的集合中去掉,那么大小同时减1,然后可以去p-1次,变成p==1的情况,那么还需要加q-(p-1)条边,那么总共就加了q条边。
所以是max(p,q).
即使对于这种图也没关系,我们可以条边1和边6,同样满足上述条件,从不同的起点到不同的终点,那么去掉后还剩一个起点和两个终点,再从两个终点向起点连两条有向边即可。
#include<bits/stdc++.h>
using namespace std;
const int N=120,M=10010;
int n;
int h[N],e[M],ne[M],idx;
int dfn[N],low[N];
int stk[N],top,scc,id[N],tp,st[N];
int cd[N],rd[N];
void add(int a,int b)
{
e[idx]=b,ne[idx]=h[a],h[a]=idx++;
}
void tarjan(int u)
{
dfn[u]=low[u]=++tp;
stk[++top]=u,st[u]=1;
for(int i=h[u];~i;i=ne[i])
{
int j=e[i];
if(!dfn[j])
{
tarjan(j);
low[u]=min(low[u],low[j]);
}
else if(st[j]) low[u]=min(low[u],dfn[j]);
}
if(dfn[u]==low[u])
{
int y;
scc++;
do{
y=stk[top--];
st[y]=0;
id[y]=scc;
}while(y!=u);
}
}
int main()
{
scanf("%d",&n);
memset(h,-1,sizeof h);
for(int i=1;i<=n;i++)
{
int j;
while(~scanf("%d",&j))
{
if(!j) break;
add(i,j);
}
}
for(int i=1;i<=n;i++)
{
if(!dfn[i]) tarjan(i);
}
for(int u=1;u<=n;u++)
{
for(int i=h[u];~i;i=ne[i])
{
int j=e[i];
if(id[u]!=id[j])
{
cd[id[u]]++,rd[id[j]]++;
}
}
}
int p=0,q=0;
for(int i=1;i<=scc;i++)
{
if(!cd[i]) q++;
if(!rd[i]) p++;
}
printf("%d\n",p);
if(scc==1) printf("0");//这里需要特判,如果只有一个强连通分量的话,那么自然不需要再加边
else printf("%d",max(p,q));
}
1175. 最大半连通子图(活动 - AcWing)
我们之前求的都是强连通分量,u可以到v,那么v也可以到u。这里要求一个半连通分量,显然对于一个强连通分量来说去它的子集一定是半连通分量,取整个强连通分量也一定是半连通分量,所以我们不如取整个强连通分量。然后我们把原图缩点就可以得到一个拓扑图了,对于拓扑图中的一条链,显然它也是半连通分量,不过不能有分叉。那么答案就出来了,先求所有的强连通分量,然后缩点得到一个拓扑图,对于拓扑图,我们可以通过dp来统计答案。
#include<bits/stdc++.h>
using namespace std;
const int N=100010,M=2000010;
int n,m,mod;
int h[N],hs[N],e[M],ne[M],idx;
int dfn[N],low[N],stk[N],st[N],id[N],sz[N],top,scc,tp;
int f[N],g[N];
void add(int h[],int a,int b)
{
e[idx]=b,ne[idx]=h[a],h[a]=idx++;
}
void tarjan(int u)
{
dfn[u]=low[u]=++tp;
stk[++top]=u,st[u]=1;
for(int i=h[u];~i;i=ne[i])
{
int j=e[i];
if(!dfn[j])
{
tarjan(j);
low[u]=min(low[u],low[j]);
}
else if(st[j]) low[u]=min(low[u],dfn[j]);
}
if(low[u]==dfn[u])
{
int y;
scc++;
do{
y=stk[top--];
id[y]=scc;
sz[scc]++;
st[y]=0;
}while(u!=y);
}
}
int main()
{
scanf("%d%d%d",&n,&m,&mod);
memset(h,-1,sizeof h);
for(int i=1;i<=m;i++)
{
int a,b;
scanf("%d%d",&a,&b);
add(h,a,b);
}
for(int i=1;i<=n;i++)
if(!dfn[i])
tarjan(i);
unordered_set<long long>s;
memset(hs,-1,sizeof hs);
for(int u=1;u<=n;u++)
{
for(int i=h[u];~i;i=ne[i])
{
int j=e[i];
int a=id[u],b=id[j];
long long tmp=a*100000ll+b;
if(a!=b&&!s.count(tmp))//两个条件都不能少
{
add(hs,a,b);
s.insert(tmp);
}
}
}
for(int i=scc;i>=1;i--)
{
if(!f[i])
{
f[i]=sz[i];
g[i]=1;//方案数
}
for(int j=hs[i];~j;j=ne[j])
{
int k=e[j];
if(f[k]<f[i]+sz[k])
{
f[k]=f[i]+sz[k];
g[k] = g[i];
}
else if(f[k]==f[i]+sz[k]) g[k]=(g[k]+g[i])%mod;
}
}
int mx=0,sum=0;
for(int i=1;i<=scc;i++)
{
if(f[i]>mx)
{
mx=f[i];
sum=g[i];
}
else if(f[i]==mx) sum = (sum+g[i])%mod;
}
printf("%d\n%d",mx,sum);
}
368. 银河(368. 银河 - AcWing题库)
思路:这题跟糖果那道题几乎一摸一样(详见spfa的特殊用法-CSDN博客) ,但是这里我们还是用强连通分量来写。
我们建边的时候就跟糖果那道题一样建边,如果a>=b,那么就建一条b指向a,长度为0的边,如果a>b,那么就建一条b指向a,长度为1的边,然后计算强连通分量,再缩点成有向图,起点的亮度是1,递推求总和。另外,一个强连通分量的各个点亮度应该是相同的,所以所有的边权都应该是0,否则就不成立,输出-1,我们在缩点的时候记录一下
还有,这里因为最暗的边为1,同时为了方便更新,我们把0点放进去。那么第二次建边的时候就一定要记得把0点算上。
#include<bits/stdc++.h>
using namespace std;
const int N=100010,M=400010;
int n,m;
int h[N],hs[N],ne[M],e[M],w[M],idx;
int dfn[N],low[N],id[N],st[N],stk[N],sz[N],top,scc,tp;
int f[N];
void add(int h[],int a,int b,int c)
{
w[idx]=c,e[idx]=b,ne[idx]=h[a],h[a]=idx++;
}
void tarjan(int u)
{
dfn[u]=low[u]=++tp;
stk[++top]=u,st[u]=1;
for(int i=h[u];~i;i=ne[i])
{
int j=e[i];
if(!dfn[j])
{
tarjan(j);
low[u]=min(low[u],low[j]);
}
else if(st[j]) low[u]=min(low[u],dfn[j]);
}
if(low[u]==dfn[u])
{
int y;
++scc;
do{
y=stk[top--];
id[y]=scc;
st[y]=0;
sz[scc]++;
}while(y!=u);
}
}
int main()
{
scanf("%d%d",&n,&m);
memset(h,-1,sizeof h);
for(int i=1;i<=n;i++) add(h,0,i,1);
for(int i=1;i<=m;i++)
{
int t,a,b;
scanf("%d%d%d",&t,&a,&b);
if(t==1) add(h,a,b,0),add(h,b,a,0);
else if(t==2) add(h,a,b,1);
else if(t==3) add(h,b,a,0);
else if(t==4) add(h,b,a,1);
else add(h,a,b,0);
}
tarjan(0);
int flag=1;
memset(hs,-1,sizeof hs);
for(int i=0;i<=n;i++)//把0点算进去
{
for(int j=h[i];~j;j=ne[j])
{
int k=e[j];
int a=id[i],b=id[k];
if(a==b)
{
if(w[j])
{
flag=0;
break;
}
}
else add(hs,a,b,w[j]);
}
if(!flag) break;
}
if(!flag) printf("-1");
else
{
for(int i=scc;i>=1;i--)
{
for(int j=hs[i];~j;j=ne[j])
{
int k=e[j];
f[k]=max(f[k],f[i]+w[j]);
}
}
long long res=0;
for(int i=1;i<=scc;i++) res += sz[i]*f[i];
cout<<res;
}
}
糖果那道题的时间复杂度实际有点紧,但是这里就能保证时间复杂度是线性的。