给定一张有向图,若对于图中任意两个节点x,y,既存在从x到y的路径,也存在y到x的路径,则该有向图是强连通图。
有向图的极大强连通子图被称为:强连通分量,简称SCC(Strongly connected component)
Tarjan 算法基于dfs,可以在线性时间内求出一张有向图的所有连通分量。
1.树枝边:搜索树中给定两个点x,y,x是y的父亲节点,那么这条边就是树枝边
2.前向边:搜索树中给定两个点x,y,x是y的祖先节点,那么这条边就是前向边
3.后向边:搜索树中给定两个点x,y,y是x的祖先节点,那么这条边就是后向边
4.横叉边:搜索树中给定两个点x,y,不属于以上三种的边,就叫横叉边,且y在搜索树中的编号一定小于x在搜索树中的编号。即dfn[y]
举个例子:
dfn数组存储的即是各个深度优先遍历过程中,每个节点第一次被访问的时间顺序,读者不妨对该图做一次深度优先遍历加以验证。
例如:1,2即是树枝边,注意:树枝边一定也是前向边
1,8即是前向边,4,6即是后向边,8,7是横叉边
在搜索的时候,按照深度优先遍历顺序给每个点一个编号,即时间戳。
dfn[u]表示遍历到u的时间戳。
void tarjan(int u)
{
dfn[u] = low[u] = ++timestamp;//时间戳
stk[++top] = u,in_stk[u] = true;//把当前点放到栈里面去,随后标记u已经入栈
for(int i = h[u];~i;i = ne[i])
{
int j = e[i];
if(!dfn[j])
{
tarjan(j);//先看一下u能到的点
//这里做完tarjan(j)以后,low[j]已经被更新了,由定义可知,low[j]保存的就是从j出发能到达的时间戳最小的点
low[u] = min(low[u],low[j]);//更新low[u]
}
else if(in_stk[j])
{
//可能通过其他边能搜到j,但不是当前分支搜到的,读者这里可以好好考虑考虑
//stk不一定只存了当前分支的点,还可能存有前面其他分支的点,比如横叉边所到达的点
//stk存的是当前还没有遍历完的强连通分量的所有点
low[u] = min(low[u],dfn[j]);
}
}
if(dfn[u]==low[u])
{
//u必然是u所在的强连通分量的最高点(由定义)
int y;
++scc_cnt;
do{
y = stk[top--];//把栈顶弹出
in_stk[y] = false;//标记它没有在栈里面
id[y] = scc_cnt;//标记y这个点属于scc_cnt这个强连通分量
}while(y!=u);
}
}
// y总原话:《最好背过》 :)
for(int i = 1;i<=n;i++)
{
for(i的所有邻点j)
{
if(i和j不在同一个强连通分量中)
{
把i所在的强连通分量向j所在的强连通分量加一条边;
}
}
}
//缩完点就是DAG(有向无环图)
注:缩完点以后,连通分量编号递减的顺序一定是拓扑序
给出注的证明:
对于一个图做一遍深度优先遍历,把每个点加入序列seq中
显然有seq的逆序即是拓扑序。
for(int i = h[u];~i;i = ne[i])
{
int j = e[i];
if(!st[j])
{
seq<-j;//把j存入seq序列中
dfs(j);
}
}
//读者不妨验证一下
最后,观察tarjan求有向图强连通分量的过程实际上就是对图做深度优先遍历的过程,所以最后按连通分量编号递减的顺序输出一定是拓扑序
一个思路是建反图,对每个奶牛做一遍dfs,统计完美奶牛个数,时间复杂度O(nm),显然超时。
再观察:
题意知当是有向无环图时,只要有两个点出度为0,那么完美奶牛个数为0,如果有一个出度为0,那么完美奶牛个数为1,
于是可以tarjan缩点,如果只有一个出度为0的强连通分量,那么答案就是该强连通分量的size。
#include
using namespace std;
const int N = 1e4+10,M = 5e4+10;
int n,m;
int h[N], e[M], ne[M], idx;
int dfn[N], low[N], timestamp; // 时间戳
int stk[N], top;
bool in_stk[N];
int id[N], scc_cnt; // 每个点所属分量编号
int dout[N],sz[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] = ++timestamp;
stk[++top] = u,in_stk[u] = true;
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])
{
low[u] = min(low[u],dfn[j]);
}
}
if(dfn[u]==low[u])
{
++scc_cnt;
int y;
do{
y = stk[top--];
in_stk[y] = false;
id[y] = scc_cnt;
sz[scc_cnt]++;
}while(y!=u);
}
}
int main()
{
memset(h, -1, sizeof h);
cin>>n>>m;
while (m -- ){
int a,b;
cin>>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;j = ne[j])
{
int k = e[j];
int a = id[i],b = id[k];
if(a!=b)
{
//说明i和j不在同一个连通分量中
dout[a]++;//这里没有实际把边加出来,因为不在同一个连通分量中,所以要把a的出度+1,等价于加了一条边
}
}
}
int sum = 0,zeros = 0;//zeros记录出度为0的连通分量的个数
for(int i = 1;i<=scc_cnt;i++)
{
if(!dout[i])
{
zeros++;
sum+=sz[i];
if(zeros>1)
{
cout<<0;
return 0;
}
}
}
cout<<sum;
return 0;
}
第一个问类似题目1,第二个问需要找小性质:如何让有向无环图变成强连通图添加的边的数量最少?
设 a 为 入 度 为 0 的 点 的 个 数 , b 为 出 度 为 0 的 点 的 个 数 那 么 易 有 a n s = m a x ( a , b ) 设a为入度为0的点的个数,b为出度为0的点的个数\\ 那么易有ans = max(a,b) 设a为入度为0的点的个数,b为出度为0的点的个数那么易有ans=max(a,b)
题解来自《算法竞赛进阶指南》:
AC code:
#include
using namespace std;
const int N = 1e4+10,M = 5e4+10;
int n,m;
int h[N], e[M], ne[M], idx;
int dfn[N], low[N], timestamp; // 时间戳
int stk[N], top;
bool in_stk[N];
int id[N], scc_cnt; // 每个点所属分量编号
int dout[N],sz[N],din[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] = ++timestamp;
stk[++top] = u,in_stk[u] = true;
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])
{
low[u] = min(low[u],dfn[j]);
}
}
if(dfn[u]==low[u])
{
++scc_cnt;
int y;
do{
y = stk[top--];
in_stk[y] = false;
id[y] = scc_cnt;
sz[scc_cnt]++;
}while(y!=u);
}
}
int main()
{
memset(h, -1, sizeof h);
cin>>n;
for(int i = 1;i<=n;i++)
{
int x;
while(cin>>x,x)
{
add(i,x);
}
}
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;j = ne[j])
{
int k = e[j];
int a = id[i],b = id[k];
if(a!=b)
{
//说明i和j不在同一个连通分量中
dout[a]++;//这里没有实际把边加出来,因为不在同一个连通分量中,所以要把a的出度+1,等价于加了一条边
din[b]++;
}
}
}
int sum1 = 0,sum2 = 0,cnt1 = 0,cnt2 = 0;
//cnt1记录出度为0的连通分量的个数
//cnt2记录入度为0的连通分量的个数
for(int i = 1;i<=scc_cnt;i++)
{
if(!dout[i])
{
cnt1++;
}
if(!din[i])
{
cnt2++;
}
}
if(scc_cnt!=1)
cout<<cnt2<<endl<<max(cnt1,cnt2);
else
cout<<cnt2<<endl<<"0";
return 0;
}
陆续更新~