最大半连通子图
题目描述
核心思路
根据题目给出的 导出子图、半连通子图、最大半连通子图的定义可知,强连通图一定是半连通子图。由于题目要求的是最大半连通图,又由于强连通分量中任意两个点之间都是可以相互到达的,因此,我们最好选择强连通分量。然后我们可以对强连通分量进行缩点,但是这里要注意,我们要把缩点后的图给真的建立出来。我们在缩点后,会得到一张DAG图,在DAG上找到一个最长链(所谓最长是指链上的点最多),这条链上的点的数目就是最大半连通子图的点的数目。因为是拓扑图,所以可以采用递推的方式求解最大半连通子图中点的数目以及对应的方案数,本质上是一个DP。
下面给出f[]
和g[]
的定义:
f[i]
表示以第 i 个点为终点的最长链节点数量之和g[i]
表示让 f [ i ] f[i] f[i]取到最大值时对应的方案数,也就是说有 g [ i ] g[i] g[i]条最长链,可以使 f [ i ] f[i] f[i]取得最大值
由于这是DAG,没有后效性,因此可以用dp思想进行递推,如果存在一条从 i i i到 j j j的边,则状态转移如下:
- 如果 f [ j ] + c n t [ i ] > f [ i ] f[j]+cnt[i]>f[i] f[j]+cnt[i]>f[i],那么更新 f [ i ] = f [ j ] + c n t [ i ] f[i]=f[j]+cnt[i] f[i]=f[j]+cnt[i],由于只是增加了点数,并没有产生新方案,因此 g [ i ] = g [ j ] g[i]=g[j] g[i]=g[j]
- 如果 f [ j ] + c n t [ i ] = f [ i ] f[j]+cnt[i]=f[i] f[j]+cnt[i]=f[i],虽然相等,但是这是两种不同的方案,因此 g [ i ] = g [ i ] + g [ j ] g[i]=g[i]+g[j] g[i]=g[i]+g[j]
这里求方案数的思想与 背包问题求方案数 是一样的,背包问题求方案数
这里还需要注意,这条最长链不能有分叉,如下图所示:
使用两个表头分别存储原图h[]
和缩点后的新图hs[]
,如下图所示:
这里还需要注意重边,根据最大半连通子图的含义可知,区别不同的最大半连通子图关键在于 点的不同 而不是边的不同,也就是说如果对于两个节点 i , j i,j i,j,它俩之间有很多边,在求解DAG最长链时就会把这些边都当作不同的路径了,所以很产生很多方案。但是由于都是节点 i , j i,j i,j,因此点是相同的,于是这就不能算作是不同的最大半连通子图。如下图所示:
为了消除重边的影响,我们需要对边进行判重,这里使用哈希表进行判重。我们把边都哈希成不同的整数,这里的哈希函数是 a ∗ 1000000 l l + b a* 1000000ll + b a∗1000000ll+b。为什么呢?
因为题目中给出了点数最多是1e6,也就是说点的编号最大是1e6。那么边的编号只能从1e6+1开始了。由于节点 a , b a,b a,b的编号必然不同,因此设置 a ∗ 1000000 l l + b a* 1000000ll + b a∗1000000ll+b就可以让边的编号从1e6+1开始取并且还不相同了。这样就可以消除重边的影响了。
代码
#include<iostream>
#include<cstring>
#include<algorithm>
#include<unordered_set>
using namespace std;
const int N=1e5+10,M=2e6+10;
typedef long long LL;
//h[]原图的表头 hs[]缩点后得到的这张DAG图的表头
int h[N],hs[N],e[M],ne[M],idx;
//表示节点u深度优先遍历的序号(也就是节点u被访问的时间点)
//表示节点u或节点u的子孙能够通过非父子边追溯到的dfn最小的节点序号
//即回到最早的过去(也就是节点u通过有向边可回溯到的最早的时间点)
//num表示时间戳
int dfn[N],low[N],num;
//a=id[x]表示x这个节点属于a这个强连通分量
//cnt[i]=100表示i这个强连通分量有100个节点
//scc表示强连通分量的编号 scc=3表示第3个强连通分量 最终也只有scc个强连通分量
int id[N],cnt[N],scc;
//栈用来存储访问的节点 top是栈顶指针
int stk[N],top;
//din存储每个节点的入度 dout存储每个节点的出度
int din[N],dout[N];
//in_stk[i]=true表示节点i还在栈中
bool in_stk[N];
//f[i]表示以第 i 个点为终点的最长链节点数量之和
//g[i]表示让f[i]取到最大值时对应的方案数
LL f[N],g[N];
int n,m,mod;
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]=++num; //给节点u分配一个时间戳
stk[++top]=u; //将节点u入栈
in_stk[u]=true; //标记节点u在栈中
//遍历节点u的所有邻接点
for(int i=h[u];~i;i=ne[i])
{
int j=e[i]; //u的邻接点j
//根据分析得出:先会执行else if中的更新语句,才会执行if中的回溯过程中的更新语句
//如果节点j还没有被访问过
if(!dfn[j])
{
tarjan(j);//递归访问j
//回溯是更新从节点j往回到节点u这条路径上的所有节点的low值
low[u]=min(low[u],low[j]);
}
//否则说明节点j已经被访问过了 看j是否还在栈中
//如果还在栈中 那么就说明形成了环 即存在强连通分量
//当走到这里时说明j先于u被访问,因此u可以追溯到更早的过去
//于是用更早的dfn[j]来更新low[u]
else if(in_stk[j])
low[u]=min(low[u],dfn[j]);
}
//退无可退 即此时u不能追溯到更早的过去了 那么它就是这个强连通分量的入口
if(dfn[u]==low[u])
{
scc++; //强连通分量的个数+1
int y;
//输出这个强连通分量中所包含的节点
do{
y=stk[top--]; //强连通分量的节点y
in_stk[y]=false; //标记节点y不在栈中
id[y]=scc; //节点y属于scc这个强连通分量
cnt[scc]++; //scc这个强连通分量中的节点个数增加了y这个节点
}while(y!=u);
}
}
int main()
{
memset(h,-1,sizeof h);
memset(hs,-1,sizeof hs);
scanf("%d%d%d",&n,&m,&mod);
//建立原图
while(m--)
{
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<LL>S; //哈希表 对边判重 消除重边
for(int i=1;i<=n;i++)
{
//遍历节点i的所有邻接点
for(int j=h[i];~j;j=ne[j])
{
int k=e[j]; //i的邻接点k
int a=id[i]; //节点i所在的强连通分量 也就是缩点后得到的某个节点a
int b=id[k]; //节点k所在的强连通分量 也就是缩点后得到的某个节点b
//节点a,b之间这条边的编号哈希成>1000000的一个整数值
LL hash=a*1000000ll+b;
//如果a和b都不在同一个强连通分量中并且这条边还没有出现过
if(a!=b&&!S.count(hash))
{
//将缩点后的这张DAG图真的建立出来
add(hs,a,b);
S.insert(hash);
}
}
}
//tarjan求完后,scc的编号递减顺序就是拓扑序列
//预处理出f[]和g[]
for(int i=scc;i>0;i--)
{
if(!f[i]) //如果当前点没有更新,就进行初始化
{
f[i]=cnt[i];
g[i]=1;
}
//遍历节点i的所有邻接点
for(int j=hs[i];~j;j=ne[j])
{
int k=e[j]; //i的邻接点k
//通过i能够更新k 只是让f[k]变多了 并没有产生新的方案
if(f[k]<f[i]+cnt[k])
{
f[k]=f[i]+cnt[k];
g[k]=g[i];
}
//比如f[k]=8 f[i]=5 cnt[k]=3 那么{8}和{5,3}就是两种不同的方案
//使得节点k的f值都是8 于是如果相同的话就会产生新方案
else if(f[k]==f[i]+cnt[k])
g[k]=(g[k]+g[i])%mod;
}
}
//maxf是记录最大的f[] sum是记录最大的g[]
int maxf=0,sum=0;
for(int i=1;i<=scc;i++)
{
if(f[i]>maxf)
{
maxf=f[i];
sum=g[i];
}
else if(f[i]==maxf)
sum=(sum+g[i])%mod;
}
printf("%d\n",maxf);
printf("%d\n",sum);
return 0;
}