先讲一些知识点(强联通都知道就不说了)
根据这个还有这个总结得极大极小连通子图(貌似有的离散课本讲了):
极大的就是本身,极小就是包含连通图中所有顶点,但是在多加一条边就会产生环,减少一条边就无法构成连通,也就是生成树
无向连通图的极大连通子图是其本身(唯一),极小连通子图是其生成树(不唯一)
无向不连通图的极大连通子图就是各个连通(不唯一),极小连通子图是各个连通的生成树(不唯一)
有向连通图的极大连通子图为其本身(唯一)
有向不连通图的极大连通子图为各个连通(不唯一)
有向图不存在极小连通子图的概念。
关于Tarjan的过程看这个
然后再来说这道题:
首先可以用Tarjan算法求出强联通的个数,然后压缩这个图,即每个强联通压缩成一个点(这叫缩点),完成后新图是一个DGA,那么只要找出最少的边加入这个新的DAG使得强联通个数为1就行了。
那么如何找出需要加的边的个数呢?
首先遍历所有的边,如果这边连接两个强联通,那么就记录下这条边,完成后检查每个强联通的入度和出度,分别记录下入度为0和出度为0的点的个数,那么最少的边数就是两者的最大值。
#include <cstdio>
#include <cstring>
#include <iostream>
#include <algorithm>
#include <vector>
#include <stack>
using namespace std;
const int N=20000+5;
int n,m,t,x,y;
vector<int> G[N];
int pre[N],lowlink[N],sccno[N],in[N],out[N];
int dfs_clock,scc_cnt;
stack<int> S;
void DFS(int u)
{
//pre表示u的搜索次序编号
//lowlink表示u和u的子树能追溯到的最早的节点的次序编号
pre[u]=lowlink[u]=++dfs_clock;//每次记录当前节点的次序编号,并且当前最早的能追溯到的初始化为自己的编号,也可以说一开始把每个点都看做一个强联通
S.push(u);
for(int i=0;i<G[u].size();i++)
{
int v=G[u][i];
if(!pre[v])//如果没有搜索过
{
DFS(v);
lowlink[u]=min(lowlink[u],lowlink[v]);//更新能追溯到的祖先的次序编号
}
else if(!sccno[v])//如果搜索过
lowlink[u]=min(lowlink[u],pre[v]);
}
if(lowlink[u]==pre[u])//找到当前的强连通中祖先编号最小的是自己的编号(也就是又到了当前这个强联通的起点),表明当前的强联通寻找完成
{
scc_cnt++;
while(1)
{
int x=S.top();
S.pop();
sccno[x]=scc_cnt;
if(x==u) break;
}
}
}
void ini(int n)
{
for(int i=0;i<=n;i++) G[i].clear();
dfs_clock=scc_cnt=0;
memset(in,0,sizeof(in));
memset(out,0,sizeof(out));
memset(pre,0,sizeof(pre));
memset(sccno,0,sizeof(sccno));
}
int main()
{
scanf("%d",&t);
while(t--)
{
scanf("%d%d",&n,&m);
ini(n);
for(int i=1;i<=m;i++)
{
scanf("%d%d",&x,&y);
G[x].push_back(y);
}
for(int i=1;i<=n;i++) if(!pre[i]) DFS(i);
if(scc_cnt==1) printf("%d\n",0);
else
{
for(int u=1;u<=n;u++)//缩点
{
for(int j=0;j<G[u].size();j++)
{
int v=G[u][j];
if(sccno[u]!=sccno[v])//如果边u->v正好连接两个强联通那就记录下来已经存在的边
{
out[sccno[u]]++;
in[sccno[v]]++;
}
}
}
int innum=0,outnum=0;
for(int i=1;i<=scc_cnt;i++)
{
if(in[i]==0) innum++;
if(out[i]==0) outnum++;
}
printf("%d\n",max(innum,outnum));
}
}
return 0;
}