7053 建造军营

提高组   树形DP   图论提高   Tarjan   容斥原理   NOIP2022

描述

A 国与 B 国正在激烈交战中,A 国打算在自己的国土上建造一些军营。

A 国的国土由 n 座城市组成,m 条双向道路连接这些城市,使得任意两座城市均可通过道路直接或间接到达。A 国打算选择一座或多座城市(至少一座),并在这些城市上各建造一座军营。

众所周知,军营之间的联络是十分重要的。然而此时 A 国接到情报,B 国将会于不久后袭击 A 国的一条道路,但具体的袭击目标却无从得知。如果 B 国袭击成功,这条道路将被切断,可能会造成 A 国某两个军营无法互相到达,这是 A 国极力避免的。因此 A 国决定派兵看守若干条道路(可以是一条或多条,也可以一条也不看守),A 国有信心保证被派兵看守的道路能够抵御 B 国的袭击而不被切断。

A 国希望制定一个建造军营和看守道路的方案,使得 B 国袭击的无论是 A 国的哪条道路,都不会造成某两座军营无法互相到达。现在,请你帮 A 国计算一下可能的建造军营和看守道路的方案数共有多少。由于方案数可能会很多,你只需要输出其对 1,000,000,007(109+7) 取模的值即可。两个方案被认为是不同的,当且仅当存在至少一 座城市在一个方案中建造了军营而在另一个方案中没有,或者存在至少一条道路在一个 方案中被派兵看守而在另一个方案中没有。

输入描述

第一行包含两个正整数 n,m,分别表示城市的个数和双向道路的数量。

接下来 m 行,每行包含两个正整数 ui​,vi​,描述一条连接 ui​ 和 vi​ 的双向道路。保证没有重边和自环。

输出描述

输出一行包含一个整数,表示建造军营和看守道路的方案数对 1,000,000,007(109+7) 取模的结果。

样例输入 1 

2 1
1 2

样例输出 1 

5

样例输入 2 

4 4
1 2
2 3
3 1
1 4

样例输出 2 

184

提示

数据规模与提示

对所有数据,保证 1≤n≤5×105,n−1≤m≤106,1≤ui​,vi​≤n,ui​=vi​。

各测试点的信息如下

测试点编号n≤m≤特殊条件
1∼3810
4∼71625
8∼930005000
10∼115×105106特殊性质 A
12∼145×105106m=n−1
15∼165×105106m=n
17∼205×105106

特殊性质 A:保证 m=n−1 且第 i 条道路连接城市 i 与 i+1。

样例 1 解释

A 国有两座城市,一条道路连接他们。所有可能的方案如下:

  • 在城市 1 建军营, 不看守这条道路;
  • 在城市 1 建军营, 看守这条道路;
  • 在城市 2 建军营, 不看守这条道路;
  • 在城市 2 建军营, 看守这条道路;
  • 在城市 1,2 建军营, 看守

代码

#include <cstdio>
#include <iostream>
using namespace std;
const int M = 4e6, mod = 1000000007;
int add(int x, int y) {return ((x += y) >= mod) ? x-mod : x;}
void addn(int &x, int y) {if((x += y) >= mod) x -= mod;}
int mins(int x, int y) {return ((x -= y) < 0) ? x+mod : x;}
struct G{
    struct edge{
        int to, nxt, w;
    } e[M << 1];
    int head[M], cnt1 = 1;
    void link(int u, int v) {
        e[++cnt1] = {v, head[u], 1}; head[u] = cnt1;
    }
}g1, g2;
int fa[M]; 
int dfn[M], low[M], cnt; bool cut[M];
void tarjan(int u, int f) {
    low[u] = dfn[u] = ++cnt; 
    for(int i = g1.head[u]; i; i = g1.e[i].nxt) {
        int v = g1.e[i].to; if(v == f) continue;
        if(!dfn[v]){
            tarjan(v, u);
            low[u] = min(low[u], low[v]);
            if(low[v] > dfn[u]) g1.e[i].w = g1.e[i ^ 1].w = 0; // 0 标记为割边,i^1 用了成对变换的技巧,把反边一起标记
        }
        else low[u] = min(low[u], dfn[v]);
    }

int siz1[M], siz2[M]; bool vis[M];
int bl[M];
int n, m, cnt2;
// 用于划分连通块,siz1 对应行文中的 cntp,点数;siz2 对应行文中的 cnte,边数
void dfs2(int u, int f, int t) {
    bl[u] = t; vis[u] = 1; ++siz1[t];
    for(int i = g1.head[u]; i; i = g1.e[i].nxt) {
        if(g1.e[i].w) ++siz2[t];
        int v = g1.e[i].to;
        if(g1.e[i].w == 0 || v == f) continue;
        if(!vis[v]) dfs2(v, u, t);
    }
}
int sm[M];
// 用于计算文中的 sum 数组,子树中边数和,包括被缩掉者
void dfs4(int u, int fa) {
    sm[u] = siz2[u];
    for(int i = g2.head[u]; i; i = g2.e[i].nxt) {
        int v = g2.e[i].to; if(v == fa) continue;
        dfs4(v, u);
        sm[u] += sm[v] + 1;
    }
}
int dp[M][2], pw[M], ans;
// 统计答案的 dfs,dp 意义如上文所叙
// dp_{u,0} 强制 u 及其子树不选任意一个点(空)
// dp_{u,1} 强制 u 及其子树选至少一个点,且所有被选的点与 u 连通(不空)。
void dfs3(int u, int fa) {
    dp[u][0] = pw[siz2[u]];
    dp[u][1] = 1ll * pw[siz2[u]] * (pw[siz1[u]] - 1) % mod;
    int tot = 0;
    for(int i = g2.head[u]; i; i = g2.e[i].nxt) {
        int v = g2.e[i].to; if(v == fa) continue;
        dfs3(v, u);
        dp[u][1] = add(1ll * dp[u][1] * add(1ll * dp[v][0] * 2 % mod, dp[v][1]) % mod, 1ll * dp[u][0] * dp[v][1] % mod);
        dp[u][0] = 1ll * dp[u][0] * 2 % mod * dp[v][0] % mod;
        addn(tot, dp[v][1]);
    }
    if(u != n+1) addn(ans, 1ll * dp[u][1] * pw[sm[n+1] - sm[u] - 1] % mod);
    else addn(ans, 1ll * dp[u][1] % mod);
}
int find(int u) {return u == fa[u] ? u : fa[u] = find(fa[u]);}
void merge(int u, int v) {if((u = find(u)) != (v = find(v))) fa[u] = v;}
int main(){
    scanf("%d %d", &n, &m);
    pw[0] = 1;
    for(int i = 1; i <= 2 * n + m; i++) pw[i] = 1ll * pw[i-1] * 2 % mod;
    for(int i = 1; i <= m; i++) {
        int u, v; scanf("%d %d", &u, &v);
        g1.link(u, v); g1.link(v, u);
    }
    tarjan(1, 0); cnt2 = n;
    for(int i = 1; i <= n; i++) if(!vis[i]) dfs2(i, 0, ++cnt2);
    for(int i = n+1; i <= 2*n; i++) {
        siz2[i] /= 2; 
    }
    // 缩边
    for(int i = 1; i <= 2*n; i++) fa[i] = i;
    for(int u = 1; u <= n; u++) {
        for(int i = g1.head[u]; i; i = g1.e[i].nxt) {
            int v = g1.e[i].to; 
            if(find(bl[u]) != find(bl[v])) g2.link(bl[u], bl[v]), g2.link(bl[v], bl[u]), merge(bl[u], bl[v]);
        }
    }
    // n+1 是缩完边后树的根
    dfs4(n+1, 0);
    dfs3(n+1, 0);
    printf("%d\n", ans);
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值