题解 [NOIP2022] 建造军营

树形 \(dp\) 好题。

观察题目发现, 如果B国袭击后,导致A国两个军营不联通,那么B国袭击的一定是一条割边,反之,如果袭击的不是割边,那么不会导致任何影响。

所以先进行边双缩点,变成一棵树,记每个联通块(缩完后)内的点数为 \(wa\),边数为 \(wb\),不妨先考虑对于树的情况如何处理。

将问题进行转化,即在树上选一些点和边,使得所有点可以通过选的边相互到达,询问选点和边的方案数。

首先设 \(f[i][0/1]\) 表示以 \(i\) 为根的子树内没有/有 军营的方案数,那么答案显然是 \(f[1][1]\),但我们发现一种情况很难转移:对于节点 \(i\),有些子树内有军营,有些没有,很难对边进行计数。

出现难以转移的情况,有两种解决办法:

  • 增加 \(dp\) 状态,更加具体地描述每个状态;
  • 不增加状态,但是对状态作出更严格的限制。

经过尝试,即使增加 \(dp\) 状态,仍然难以解决上述问题,所以我们采用第二种方法。

\(f[i][0/1]\),其中 \(f[i][0]\) 表示以 \(i\) 为根的子树内没有选点,\(f[i][1]\) 表示以 \(i\) 为根的子树内选了点,且选出的边满足选出的点与 \(i\) 点连通(注意:\(i\) 不一定要选)。

先来考虑答案是什么,显然不是 \(f[1][1]\),需要对每个点单独考虑,具体地,设整棵树除去 \(i\) 为根的子树的边数为 \(cnt[i]\),那么答案就是:

\[\sum_{i=1}^{siz} f[i][1]\times (2^{cnt[i]}-1) \]

解释一下:依次考虑每个点,记当前点为 \(i\),并且钦定只有 \(i\) 子树内选了点,于是 \(i\) 子树外面的边怎么选就无所谓(反正没有点,乱选边也不会有影响),所以每个点选或不选就是 \(2^{cnt[i]}\),但直接这样求会算重,原因如下图所示:

原因是在考虑 \(i\) 时,在选择 \(i\) 外面的边时选择了 \(i\) 头上的边,这种情况恰好包含在 \(f[fa][1]\) 中,因为正好通过这条边与 \(fa\) 相连。

所以考虑 \(i\) 时强制不选 \(i\)\(fa\) 的边,这样就不会导致算重。也就是上述式子中的 \(2^{cnt[i]}-1\)

那么这种算法能将所有情况统一下来不漏吗?换一个角度考虑,发现任意一种选点方案,会在选的点的 \(\rm LCA\) 及其祖先处进行统计,由于我们规定不选与父亲相连的边,所以在所以祖先处考虑时边集不会重复,且覆盖所有,所以没有问题。

下面来考虑如何 \(dp\)

\[\begin{cases} f[nd][1]=2\times \prod_{x\in son[nd]} (f[x][1]+2\times f[x][0])-\prod_{x\in son[nd]} 2\times f[x][0]\\ f[nd][0]=\prod_{x\in son[nd]} f[x][0]\times 2 \end{cases} \]

解释:\(f[x][0] \times 2\) 代表 \(nd\)\(x\) 相连的边有选/不选两种可能,\(f[nd][1]\) 后面减的东西表示除去子树全部为 \(0\) 的情况,前面的 \(2\times\) 系数表示 \(nd\) 自己选/不选两种可能,如果 \(nd\) 选了,那么子树选不选都无所谓,但如果 \(nd\) 没选,那么子树必须要选。

这样我们就会了单纯是树的情况。

下面来考虑边双缩点成的树的情况,多了 \(wa\)\(wb\) 两个权值。

其实几乎只有边界条件的不同,若选择点 \(i\),那么有 \((2^{wa[i]}-1)\times 2^{wb[i]}\),若不选,就是\(2^{wb[i]}\),然后剩下按照转移方程计算即可。

时间复杂度:\(\rm O(n)\)

代码解释:f 同上,dp 表示子树内的边,wa 表示联通块内点数,wb 表示联通块内边数,p 表示 \(2\) 的次幂。

#include<bits/stdc++.h>
using namespace std;

#define PII pair<int,int>
typedef long long LL;
typedef unsigned long long ULL;
LL read() {
    char c=getchar(); LL sum=0,flag=1;
    while(c<'0'||c>'9') {if(c=='-') flag=-1; c=getchar();}
    while(c>='0'&&c<='9') {sum=sum*10+c-'0'; c=getchar();}
    return sum*flag;
}

const int N=5e5+10,M=1e6+10;
const LL MOD=1e9+7;
int n,m;
int low[N],dfn[N],siz,tmst,id[N];
int wa[N],wb[N];
vector<PII> g[N];
struct Edge {
    int l,r;
}a[M]; 
stack<int> st;
LL f[N][2],p[M],dp[N];

void dfs(int nd,int lst) {
    dfn[nd]=low[nd]=++tmst; st.push(nd);
    for(auto t:g[nd]) {
        int x=t.first,id=t.second;
        if(lst==id) continue;
        if(!dfn[x]) {
            dfs(x,id);
            low[nd]=min(low[nd],low[x]);
        }
        else low[nd]=min(low[nd],dfn[x]);
    }
    if(low[nd]==dfn[nd]) {
        int y=0; ++siz;
        do {
            y=st.top(); st.pop();
            id[y]=siz; wa[siz]++;
        } while(y!=nd);
    }
}

void dfs2(int nd,int fa) {
    f[nd][0]=p[wb[nd]];
    f[nd][1]=(p[wa[nd]]-1)*p[wb[nd]]%MOD;
    dp[nd]=wb[nd];
    if(g[nd].size()==1&&g[nd][0].first==fa) return ;
    LL s1=p[wb[nd]];//自己不选
    LL s2=(p[wa[nd]]-1)*p[wb[nd]]%MOD;//自己选
    LL s0=p[wb[nd]];//计算子树全部不选的数量
    for(auto t:g[nd]) {
        int x=t.first; if(x==fa) continue;
        dfs2(x,nd);
        dp[nd]+=dp[x]+1;
        f[nd][0]*=f[x][0]*2; f[nd][0]%=MOD;
        s1*=f[x][1]+f[x][0]*2; s1%=MOD;
        s2*=f[x][1]+f[x][0]*2; s2%=MOD;
        s0*=f[x][0]*2%MOD; s0%=MOD;
    }
    f[nd][1]=s2+s1-s0; f[nd][1]=(f[nd][1]%MOD+MOD)%MOD;
}

int main(){
    n=read(); m=read();
    p[0]=1; for(int i=1;i<=m;i++) p[i]=p[i-1]*2ll%MOD;
    for(int i=1;i<=m;i++) {
        int x=read(),y=read();
        a[i]={x,y};
        g[x].push_back({y,i});
        g[y].push_back({x,i});
    }
    dfs(1,0);
    for(int i=1;i<=n;i++) {
        g[i].clear();
    }
    for(int i=1;i<=m;i++) {
        int x=id[a[i].l],y=id[a[i].r];
        if(x==y) wb[x]++;
        else {
            g[x].push_back({y,0});
            g[y].push_back({x,0});
        }
    }
    dfs2(1,0);

    LL ans=0;
    for(int i=1;i<=siz;i++) {
        ans=(ans+f[i][1]*p[max(dp[1]-dp[i]-1,0ll)]%MOD)%MOD;
    }
    cout<<ans;

    return 0;
}
/*
*/
posted @ 2024-10-23 09:08  2017BeiJiang  阅读(24)  评论(0编辑  收藏  举报