P8867 NOIP2022 建造军营

P8867 NOIP2022 建造军营 - 洛谷 | 计算机科学教育新生态 (luogu.com.cn)

给定一个无向联通图 \(G = (V', E')\),求有多少个二元组 \((V, E)\),满足:

  • \(V \subseteq V'\)\(E \subseteq E'\)\(V \ne \varnothing\)
  • \(G\) 上,断开 \(E’ - E\) 中任意一条边后,都有 \(V\) 中所有点在 \(G\) 上仍然联通。

35 pts

枚举 \(2^n - 1\)\(V\) 的情况,再用 \(m(n +m)\) 的复杂度暴力检验将每条边断开后 \(V\) 是否仍然联通,记录【断开该边后 \(V\) 仍然可以联通】的边数 \(M\),答案累加 \(2^{M}\) 即可。

时间复杂度 \(\Theta(m(n+m)2^n)\),期望可以通过前 \(7\) 个数据点(没有实测)。

45 pts

考虑开特殊性质 \(\mathrm{A}\),也就是给定的图是一条链。

我们考虑当 \(V\) 最左面的点为 \(l\),最右面的点为 \(r\) 时,有多少种二元组。

\(l = r\),也就是 \(V\) 中只有一个元素 \(l\) 时,所有边都可以随便选,可以有 \(2^{n-1}\) 种取法。这一部分的答案是 \(n \times 2^{n-1}\)

\(l \ne r\) 时,中间的 \(r-l\) 条边必须选入 \(E\),而两头的边都可以随便选取。此时 \(V\)\(2^{r - l - 1}\) 种取法,\(E\)\(2^{n-1-(r-l)}\) 种取法,惊喜发现这一部分答案就是 \(2^{n-2}\),和 \(l\)\(r\) 无关。

\(l\)\(r\) 的取法为 \(\dbinom{n}{2} = \dfrac{n(n-1)}{2}\),所以这部分的答案是 \(n(n-1)2^{n-3}\),总答案为 \(n \times 2 ^ {n-1} + n(n-1)2^{n-3}\)

到这里都很送。

100 pts

不难发现,对于任意非桥边,删除它后整个图都可联通,因此所有非桥边在任意情况下都可以随便选。

这启发我们对整个图进行边双缩点。边双缩点后形成一棵树,原图非桥边将全部消失(被缩在一个点里),桥边变成新的树边。

因为任何非桥边任意情况下可以任意选,所以最终答案会有一个 \(2^{m - M}\) 的系数,这里 \(M\) 的含义是桥边的数量,也就是缩点后的树边数量。这样以来,我们在讨论选边方案时只讨论 \(M\) 条树边,得到的方案数最后乘上 \(2^{m - M}\) 即可。

当模型变成树后,我们发现,任意两个点之间的路径变得唯一:也就是对于树上的任意两个节点 \(s\)\(t\),如果这两个点均被选入点集,那么对于 \(s \rightsquigarrow t\) 的简单路径上的所有边必须全部选入边集。否则,断开那条没选入边集的边,\(s\)\(t\) 一定不再联通。

为了区分,我们定义 \(s\)\(t\) 是【超联通】的,当且仅当 \(s\)\(t\) 可以仅通过所选的边联通。

计数问题要么是动态规划要么是排列组合,要么两个都占。接下来就不难想到树形 dp 了。我们记 \(T(u)\) 表示 \(u\) 的子树。

考虑设计状态,\(f(u)\) 表示 \(T(u)\) 的答案,也就是在 \(T(u)\) 上,最少选一个点,选任意条边的方案数。我们设 \(e(u)\) 表示 \(T(u)\) 中边的数量。

对于 \(u\) 的子节点 \(v\),如果我们决定不在 \(T(v)\) 上选点,那么 \(T(v)\) 上的边和 \((u, v)\) 这条边都有选和不选两种可能,有 \(2^{e(v) +1}\) 种情况。

若我们在 \(T(v)\) 上选点呢,出现问题:

如果 \(T(v)\) 上存在一个点 \(x\),满足 \(x\)\(v\) 不超联通(注意到这个方案是合法的,因为我们可以不选择点 \(v\))。那么无论我们是否选择 \((u, v)\),都有 \(x\)\(u\) 不超联通。此时点 \(u\),以及 \(v\) 的兄弟的子树上的点一定不可选择,因为无法和 \(x\) 超联通,不合法。

如果 \(T(v)\) 上不存在这样的点 \(x\),也就是 \(T(v)\) 上的点都和 \(v\) 超联通,只要我们选择 \((u, v)\),那么 \(T(v)\) 上选的所有点就能和 \(u\) 超联通。此时,\(u\) 以及 \(v\) 的兄弟的子树上的点就可合理选取。

上面两种情况会造成转移的截然不同,但是由于我们统统放在一个 \(f(v)\) 里,没法转移。所以我们需要分开讨论和转移。

换句话说,我们此时应给状态多加一维:

\(f(u, 1)\) 表示 \(T(u)\) 上至少选了一个点,且选的所有点全部和 \(u\) 超联通的方案数;

\(f(u, 2)\) 表示 \(T(u)\) 上至少选了一个点,且存在一个点不和 \(u\) 超联通的方案数。

从而分开转移。最终答案是 \(f(1, 1) + f(1, 2)\)(令 \(1\) 为根)。

详细的转移方程和代码细节可以参考别的题解,因为在这里我要讲的是一种另外的思路,状态不需要多加一维,更有趣一点。

\(f(u)\) 表示 \(T(u)\) 上至少选了一个点,且选的所有点全部和 \(u\) 超联通的方案数。其实就是上边的 \(f(u, 1)\),换句话说,我只需要 \(f(u, 1)\) 一个状态就可完成本题的全部转移和答案统计。

我们加设 \(c(u)\) 表示节点 \(u\) 代表原图上多少个点(即有多少个点被缩进 \(u\) 了)。对于 \(u\) 上的点,可随便选,所以会有个 \(2^{c(u)}\) 的系数。

接着讨论 \(u\) 的子节点 \(v\) 及其子树 \(T(v)\)

  • \(T(v)\) 上不选点:那么 \(T(v)\) 上的边和 \((u, v)\) 这条边都有选和不选两种可能,有 \(2^{e(v) +1}\) 种情况;
  • \(T(v)\) 上选点:此时 \((u, v)\) 必选,有 \(f(v)\) 种情况。

最后还有个问题:\(f(u)\) 要保证 \(T(u)\) 上必选点。上面所有情况计算完毕,会包含所有不选点构成的 \(2^{e(u)}\) 种情况,最后减去。

得到 \(f(u)\) 的状态转移方程:

\[f(u) = -2^{e(u)} + 2 ^ {c(u)} \prod_{v \in \mathrm{child}(u)}2^{e(v) + 1}+f(v) \]

怎么做答案统计?

答案一定不是 \(f(1)\),因为当然会存在:所选点不全部和树根超联通的情况。

我们再考虑,一种选择方案,它选的所有点全都在 \(T(u)\) 上,还都和 \(u\) 超联通,方案数是多少?是 \(f(u)\) 吗?错,我没说所选边都在 \(T(u)\) 上:事实上,上面这个问题的答案是 \(f(u) \times 2^{M - e(u)}\)

此时我想,答案是否为 \(f(1) +f(2) \times 2^{M - e(2)} + f(3) \times 2^{M- e(3)} + \cdots + f(M + 1) \times 2^{M + 1 - e(M+1)}\)(别忘了 $M +1 $ 是新树的点数,而且 \(M = e(1)\))?然后我构造了两个反例出来:

上面这个选择方案(红点和红边为选择对象,黑色的未不选的对象),会被 \(f(4) \times 2^{M - e(4)}\)\(f(2) \times 2^{M - e(2)}\) 统计两次。

上面这个选择方案,会被 \(f(2) \times 2^{M - e(2)}\)\(f(1)\) 统计两次。

是否有一种办法能让每种方案只被统计一次?

观察上面这两个反例和自己造的一些反例,不难发现,只在所选点集的 LCA 统计答案即可。也就是对于第一个反例,期望在 \(4\) 处统计该情况而不是在 \(2\);对于第二个反例,期望在 \(2\) 处该情况而不是在 \(1\)。如果在上面第二个反例的基础上,再选点 \(3\)\((1, 3)\) 这条边,那就期望在 \(1\) 处统计答案了。

换句话讲,我们的目标:对于节点 \(u\),计算出满足所选点集的 LCA 为点 \(u\) 的方案数。

再换句话讲,就是计算出在 \(f(u)\) 中,要么是 \(u\) 本身被选择,要么是 \(u\) 的两棵及以上子树选了点的方案数,最后乘 \(2^{M - e(u)}\)

再再换句话讲,就是计算出在 \(f(u)\) 中,\(u\) 没被选而且只有一个子树中选了点的方案数,最后再用 \(f(u)\) 减去它,乘上 \(2^{M - e(u)}\) 的系数。

我们枚举 \(u\) 的子节点 \(v\)\(T(v)\) 上有 \(f(v)\) 种方案,\((u, v)\) 必选(否则就不在 \(f(u)\) 里了),\(T(u)\) 中其他边可选可不选,总共是 \(f(v) \times 2^{e(u) - e(v) - 1}\) 种方案。

因此,\(u\) 作为 LCA 对答案的贡献是:

\[(f(u) - \sum_{v \in \mathrm{child}(u)} f(v) \times 2 ^ {e(u) - e(v) - 1}) \times 2^{M - e(u)} \]

时间复杂度 \(\Theta(n+m)\)

/*
 * @Author: crab-in-the-northeast 
 * @Date: 2022-12-07 04:15:55 
 * @Last Modified by: crab-in-the-northeast
 * @Last Modified time: 2022-12-07 05:23:10
 */
#include <bits/stdc++.h>
#define int long long
inline int read() {
    int x = 0;
    bool f = true;
    char ch = getchar();
    for (; !isdigit(ch); ch = getchar())
        if (ch == '-')
            f = false;
    for (; isdigit(ch); ch = getchar())
        x = (x << 1) + (x << 3) + ch - '0';
    return f ? x : (~(x - 1));
}
inline bool gmi(int &a, int b) {
    return b < a ? a = b, true : false;
}

const int maxn = (int)5e5 + 5;
const int maxm = (int)1e6 + 5;
std :: vector <int> G[maxn], T[maxn];

int dfn[maxn], low[maxn], snt = 0, times = 0, sno[maxn];
int c[maxn];
std :: stack <int> s;

void tarjan(int u, int fa) {
    low[u] = dfn[u] = ++times;
    s.push(u);

    for (int v : G[u]) {
        if (!dfn[v]) {
            tarjan(v, u);
            gmi(low[u], low[v]);
        } else if (v != fa)
            gmi(low[u], dfn[v]);
    }

    if (low[u] == dfn[u]) {
        ++snt;
        for (; ;) {
            int x = s.top();
            s.pop();
            sno[x] = snt;
            ++c[snt];
            if (x == u)
                break;
        }
    }
}

const int mod = (int)1e9 + 7;
int f[maxn], ans = 0;
int p[maxm], e[maxn];

void dp(int u, int fa) {
    f[u] = p[c[u]];
    for (int v : T[u]) {
        if (v == fa)
            continue;
        dp(v, u);
        (f[u] *= (p[e[v] + 1] + f[v])) %= mod;
        e[u] += e[v] + 1;
    }
    (f[u] += mod - p[e[u]]) %= mod;

    int now = f[u];
    for (int v : T[u]) {
        if (v == fa)
            continue;
        ((now -= f[v] * p[e[u] - e[v] - 1] % mod) += mod) %= mod;
    }
    (ans += now * p[snt - 1 - e[u]] % mod) %= mod;
}

signed main() {
    int n = read(), m = read();
    for (int _ = 1; _ <= m; ++_) {
        int u = read(), v = read();
        G[u].push_back(v);
        G[v].push_back(u);
    }

    tarjan(1, 0);
    
    for (int u = 1; u <= n; ++u)
        for (int v : G[u])
            if (sno[u] != sno[v])
                T[sno[u]].push_back(sno[v]);
    
    p[0] = 1;
    for (int i = 1; i <= m + 3; ++i)
        p[i] = (p[i - 1] << 1) % mod;

    dp(1, 0);
    printf("%lld\n", ans * p[m - e[1]] % mod);
    return 0;
}

如果觉得这篇题解写得好,请不要忘记点赞,谢谢!

posted @ 2022-12-07 05:27  dbxxx  阅读(515)  评论(0编辑  收藏  举报