解题报告:luogu P2272 [ZJOI2007]最大半连通子图(tarjan缩点、递推DP、hash、set判重)

在这里插入图片描述

这时yxc上课时讲解的截图。
解题报告:luogu P2272 [ZJOI2007]最大半连通子图(tarjan缩点、递推DP、hash、set判重)_第1张图片
一般用到tarjan算法的题目步骤都非常相似:

  1. tarjan算法
  2. 缩点,建图(这里要判重)
  3. 按照拓扑序递推(这里缩点以后逆向就已经是拓扑序了)/ 循环遍历新图求解答案。

导出子图:点是原图的子集,边一定包含与子图中所有点有关的边。
半联通子图:所有的两个点,我要么可以直接一条路过去,要么你可以直接过来。
显然,对于任意一个强连通分量 S ∈ G S\in G SG,它一定一个半连通子图,于是我们可以考虑先缩点。缩点之后,原图变成了一个有向无环图,显然,对于任意一条DAG中的一条单向到达的链,它都是一个半连通子图。

所以答案K就是DAG中最大链的顶点个数,答案C就是DAG中不同的最大链有多少种。
由于整个DAG按顺序已经是一个拓扑序了,所以我们可以直接递推DP线性 O ( n ) O(n) O(n)求答案。 设size(u)表示顶点u代表的强连通分量
S中的顶点个数,f(u)表示从顶点u开始延伸的最大链的顶点个数,g(u)表示从顶点u开始延伸的不同的最大链有多少个,则有:
f ( u ) = s i z e ( u ) + m a x ( f ( v ) )      ( u → v ) f(u)=size(u)+max(f(v))\ \ \ \ (u\to v) f(u)=size(u)+max(f(v))    (uv)

g ( u ) = ∑ g ( v ) ( u → v , s i z e ( u ) + f ( v ) = f ( u ) ) g(u)=∑g(v)(u→v,size(u)+f(v)=f(u)) g(u)=g(v)(uv,size(u)+f(v)=f(u))每次从一个没有被访问过的顶点开始做深度优先搜索,回溯时计算每个顶点的f,g值。K,C也可以按照相同的方式进行更新。

本题需要判重去重,因为第二问是总的方案数,如果边是一摸一样的那么方案应该是同一种,所以如果不判重就会影响最后的答案。我们直接用一个set判重,顺便用到一个巧妙的hash,因为我们一共就只有100000个点,所以我们直接把所有的点都映射到100000+v即可,不会产生冲突。
需要注意的是建完的图是逆拓扑序,按照拓扑序递推DP,所以必须倒着来循环DP。

#include
#include
#include
#include
#include

using namespace std;

const int N = 100007, M = 2000007, INF = 0x3f3f3f3f;

typedef long long ll;
int mod;
int n, m;
int dfn[N], low[N], num;
int ver[M], edge[M], head[N], nex[M], tot;
int h[N];

int Size[N], scc_cnt, scc_id[N];
int stk[N], top, ins[N];

int f[N], g[N];//顶点个数和方案数

void add(int h[],int x,int y){
    ver[tot] = y;
    nex[tot] = h[x];
    h[x] = tot ++ ;
}

void tarjan(int x){
    dfn[x] = low[x] = ++ num;
    stk[ ++ top] = x;
    ins[x] = true;

    for(int i = head[x];~i;i = nex[i]){
        int y = ver[i];
        if(!dfn[y]){
            tarjan(y);
            low[x] = min(low[x],low[y]);
        }
        else if(ins[y]){
            low[x] = min(low[x], dfn[y]);
        }
    }

    if(dfn[x] == low[x]){
        int y;
        ++ scc_cnt;
        do{
            y = stk[top -- ];
            ins[y] = false;
            scc_id[y] = scc_cnt;
            Size[scc_cnt] ++ ;
        }while(x != y);
    }
}

int main()
{
    memset(head,-1,sizeof head);
    memset(h,-1,sizeof h);
    scanf("%d%d%d",&n,&m,&mod);
    while(m -- ){
        int x,y;
        scanf("%d%d",&x,&y);
        add(head,x,y);
    }

    for(int i = 1;i <= n;++i)
        if(!dfn[i])
        tarjan(i);

    unordered_set<ll>st;
    //(u,v) -> u * 1000000 + v;
    //因为一共只有100000个点
    for(int i = 1;i <= n;++i){
        for(int j = head[i];~j;j = nex[j]){
            int k = ver[j];
            int a = scc_id[i], b = scc_id[k];
            ll hash = a * 100000ll + b;
            //建新图并判重
            if(a != b && !st.count(hash)){
                add(h,a,b);
                st.insert(hash);
            }
        }
    }

    for(int i = scc_cnt;i >= 1;-- i){//建完的图是逆拓扑序,按照拓扑序递推,所以必须倒着来
        if(!f[i]){
            f[i] = Size[i];
            g[i] = 1;
        }
        for(int j = h[i];~j;j = nex[j]){
            int k = ver[j];
            if(f[k] < f[i] + Size[k]){
                f[k] = f[i] + Size[k];
                g[k] = g[i];
            }
            else if(f[k] == f[i] + Size[k]){
                g[k] = (g[k] + g[i]) % mod;
            }
        }
    }

    ll maxx = 0,sum = 0;
    for(int i = 1;i <= n;++i){
        if(f[i] > maxx){
            maxx = f[i];
            sum = g[i];
        }
        else if(f[i] == maxx){
            sum = (sum + g[i] ) % mod;
        }
    }
    printf("%lld\n%lld\n",maxx, sum);
    return 0;
}

你可能感兴趣的:(#,tarjan算法与连通图,#,强连通分量,缩点)