题解 P2272 【[ZJOI2007]最大半连通子图】

· · 题解

首先观察到这个图中如果有某个部分是强连通的,那其实可以把这整个部分都看做一个点,这样并不会对结果产生影响。

所以首先对这个图进行tarjan缩点,并记录下每个连通块的大小,和每个点所在的连通块。

缩点之后,会发现这个图变成了一个dag(有向无环图),我们要对这个dag求出最长链的大小和个数。

因为这个图是无环的,所以我们可以按照拓扑序进行DP:f[i]表示到第i个连通块的最长链大小,g[i]表示个数。

很容易就想到了转移的方法:

1.{g[i]=g[i]+g[j]}(条件:f[j]+number[i]==f[i])

2.{g[i]=g[j],f[i]=f[j]+number[i]}(条件:f[j]+number[i]>f[i])。

按照这个转移状态就可以啦。

但是要注意缩点之后会出现大量重边拓扑排序之前要去重。

#include<iostream>
#include<vector>
#include<cstring>
#include<algorithm>
#include<queue>
using namespace std;
struct shittt
{
    int aa;
    int bb;
}x1[1000001];
vector<int>a[100001],s[100001];//vector存边数组
queue<int>rt;//拓扑排序队列
bool v[100001],f[100001];//tanjan用的标记数组
int stack[100001],h,u,cnt,low[100001],dfn[100001],number[100001],b[100001];
//number表示连通块大小,b记录每个点所在的连通块,low和dfn是tanjan用的数组,stack是tarjan用的栈。
int sum[100001],dp[100001],r[100001];//sum表示最长链大小,dp表示个数,r存储连通块的入度
void dfs(int o)//tanjan缩点。
{
    u++;
    low[o]=dfn[o]=u;
    h++;
    stack[h]=o;
    f[o]=true;
    if(!a[o].empty())
        for(int i=0;i<a[o].size();i++)
            if(!v[a[o][i]])
            {
                v[a[o][i]]=true;
                dfs(a[o][i]);
                low[o]=min(low[o],low[a[o][i]]);
            }else if(f[a[o][i]]) low[o]=min(low[o],dfn[a[o][i]]);
    if(low[o]==dfn[o])//找到一个连通块
    {
        cnt++;//新的标号
        while(stack[h]!=o)
        {
            b[stack[h]]=cnt;
            number[cnt]++;//连通块大小++
            f[stack[h]]=false;
            h--;//出栈
        }
        b[stack[h]]=cnt;
        number[cnt]++;
        f[stack[h]]=false;
        h--;
    }
}
bool cmp(shittt a1,shittt b1)//排序条件函数
{
    return((a1.aa<b1.aa)||((a1.aa==b1.aa)&&(a1.bb<b1.bb)));
}
int main()
{
    int n,m,x;
    cin>>n>>m>>x;
    for(int i=1;i<=m;i++)
    {
        int xx,yy;
        cin>>xx>>yy;
        a[xx].push_back(yy);//用vector存边
    }
    memset(v,0,sizeof(v));
    memset(f,0,sizeof(f));
    u=cnt=0;
    for(int i=1;i<=n;i++)
        if(!v[i])//可能原图不是连通的,会有多个块组成。
        {
            v[i]=true;
            dfs(i);
        }
    int re=0;
    for(int i=1;i<=n;i++)
        if(!a[i].empty())
            for(int j=0;j<a[i].size();j++)
                if(b[i]!=b[a[i][j]])
                {
                    x1[++re].aa=b[i];
                    x1[re].bb=b[a[i][j]];

}//记录新的边(缩点之后连通块之间的边)

    sort(x1+1,x1+re,cmp);//排序去除重边。
    memset(r,0,sizeof(r));
    for(int i=1;i<=re;i++)
    {
        if((x1[i].aa==x1[i-1].aa)&&(x1[i].bb==x1[i-1].bb)) continue;
        s[x1[i].aa].push_back(x1[i].bb);
        r[x1[i].bb]++;
    }
    for(int i=1;i<=cnt;i++)//DP初始化
        if(r[i]==0) 
        {
            rt.push(i);
            sum[i]=number[i];
            dp[i]=1;
        }
    while(!rt.empty())//拓扑排序(用队列存入度为零的点)+DP
    {
        int yu=rt.front();
        rt.pop();
        for(int i=0;i<s[yu].size();i++)
        {
            r[s[yu][i]]--;
            if(r[s[yu][i]]==0)
                rt.push(s[yu][i]);
            if(sum[yu]+number[s[yu][i]]==sum[s[yu][i]]) //转移状态
            {
                dp[s[yu][i]]+=dp[yu];
                dp[s[yu][i]]%=x;
            }else
            if(sum[yu]+number[s[yu][i]]>sum[s[yu][i]])
            {
                dp[s[yu][i]]=dp[yu];
                sum[s[yu][i]]=sum[yu]+number[s[yu][i]];
            }
        }
    }
    int yu=0,k=0;
    for(int i=1;i<=cnt;i++)
        if(sum[i]>sum[yu]){yu=i;k=dp[i];}
            else if(sum[i]==sum[yu]){k+=dp[i];k%=x;}//求出最终值
    cout<<sum[yu]<<endl<<k<<endl;
    return 0;
}