首页 > 其他分享 >P8867 [noip2022]建造军营 题解

P8867 [noip2022]建造军营 题解

时间:2022-11-27 17:55:44浏览次数:83  
标签:连通 fa int 题解 P8867 cdot noip2022 dp mod

题意:

给定一张图,选择一些点和一些边,使得割断任意没有选的边,被选中的点仍连通。对 \(10^9+7\) 取膜。

\(n \leq 5 \cdot 10^5\)

这篇题解会略讲缩边,详讲自己推 dp 状态与转移式的过程。

前置知识:桥(割边)、缩边。

感觉有些地方的逻辑绕了点,强制分了小段,希望能让阅读者看得更明白。


看到图和连通,立刻想到 tarjan。

割断一条没有选的边,选中的点仍连通,割非桥,整张图仍然连通。

把桥删掉,则整张图分成若干连通子图。可以把一张子图看成一个点,这样形成的新图上没有环,因为在环上的边都不是割边,那它就是一棵树。

设一个新点 \(i\) 中共有 \(cntp_{i}\) 个原图中的节点,\(cnte_{i}\) 条原图中的边。后面的点边均在新树上。

问题转化为:给定一棵树,每个点有 \((2^{cntp_i}-1) \cdot 2^{cnte_i}\) 种染色方式,每条边可以染色或不染色,问有几种染色方法,使得割断任意非染色边,所有染色点仍连通。

树上的问题相较图更好解决,因为转移方向固定为从儿子到父亲。那么显然,现在应该进行 dp。后面说的子树默认是根在 \(1\) 的情况。

什么在转移时会造成影响呢?考虑 \(u\) 和其儿子 \(v\)。当 \(v\) 子树中有点被染色时,这个点需要与 \(v\) 连通,且 \(v\) 需要与 \(u\) 连通,这是为了让 \(v\) 中的点与整棵树其余部分连通。否则,\(u\) 与 \(v\) 间的边属于可有可无。

因此,设出 \(dp_{u,0/1}\),\(dp_{u,0}\) 强制 \(u\) 及其子树不选任意一个点(空);\(dp_{u,1}\) 强制 \(u\) 及其子树选至少一个点,且所有被选的点与 \(u\) 连通(不空)。

现在来考虑如何计算答案。

常用的做法是把一个点集放到它们的 lca 处计算,但是,\(dp_{u,1}\) 只考虑了连通性,而没有在意是否恰好是 lca。容斥应该可行。但是另一个想法是强制一个算入答案的状态不被另一个状态包含。

为了说明,先对树做一遍前缀和,设 \(sum_i\) 为 \(i\) 及其子树中的边数和,包括被缩掉的边。容易看出 \(dp_{u,1} \cdot 2^{sum_1 - sum_u}\) 表示了所有点均位于 \(u\) 子树,且所有点与 \(u\) 连通时的情况数。

现在让一个情况不被另一个情况包含。注意到能统计到一个点集的点均在根到 lca 的链上,则统计时强制断掉 \(u\) 与其父亲间的连边,就不会出现这些点都与 lca 的某个父亲连通的情况,从而保证了一个情况不会被统计多次。

说人话,找到一个点集的 lca,顺着 lca 往根走,满足每一步走过的边都被染色,则我们将在走到的那个点上统计整个子集的贡献。

\[\left \{ \begin{aligned} ans & \gets dp_{u,1} \cdot 2^{sum_1 - sum_u - 1} \quad & u \not = 1 \\ ans & \gets dp_{u,1} & u = 1 \end{aligned} \right . \]

对答案的贡献说清楚了,接着来说 dp 的求法。

设现在位于 \(u\),要把 \(dp_{v,0/1}\) 合并进 \(u\),设 \(dp'_{u,0/1}\) 为没有合并入 \(v\) 前的情况。

\(dp_{u,0}\) 显然只能从 \(dp'_{u,0}\) 和 \(dp_{v,0}\) 推出,且连接 \(u,v\) 的边可有可无。

\[dp_{u,0} \gets dp'_{u,0} \cdot 2dp_{v,0} \]

\(dp_{u,1}\) 可以从 \(dp'_{u,1}\) 处得到,此时 \(v\) 可以选择空或不空,空时中间边可有可无,否则必须有。

\[dp_{u,1} \gets dp'_{u,1} \cdot (dp_{v,1} + 2dp_{v,0}) \]

\(dp_{u,1}\) 也可以从 \(dp'_{u,0}\) 处得到,此时 \(v\) 必须非空,中间的边必须存在。

\[dp_{u,1} \gets dp'_{u,1} \cdot 2dp_{v,1} \]

初始时该点对应所有边均可选可不选,点看是否要求非空而言。

\[\left \{ \begin{aligned} dp_{u,0} & \gets 2^{cnte_u} \\ dp_{u,1} & \gets 2^{cnte_u} \cdot (2^{cntd_u} - 1) \end{aligned} \right . \]

结束。

AC 后去 infoj 翻了最短代码,深感自己写复杂了,居然 \(4\) 个 dfs……

#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);
}

感觉这是一道口嗨很简单的题,但细节什么的确实需要想清楚。评上位蓝或下位紫很恰当。

虽然我的思路看着很杂乱,但这确实是自己一步步想到的思路,自觉每一步的转化都能从上一步找到端倪,因为这是自己真实的思考过程。

赛时一眼缩边+树形 dp,然而没调完,花了 1h 回忆缩边的过程,回家再花了 30min 才写完。得到的教训是要好好复习学过的东西。

完结撒花 ovo

标签:连通,fa,int,题解,P8867,cdot,noip2022,dp,mod
From: https://www.cnblogs.com/purplevine/p/16930223.html

相关文章

  • Oracle的会话进程解锁及问题解决方法
    首先用dba权限的用户登陆数据库1、select*fromv$locked_object查出被锁定的对象,其中object_id是对象的ID,session_id是被锁定对象有sessionID;2、selectobject_name......
  • NOIP2022 又寄。
    前30min照常写板子。在缺省源末尾加上了以下注释,旨在挂分的时候少挂一点(flag)。/***Author:chenxia25*Pleasegivememorepoints!*/看T1。怎么这么简单?看......
  • 『题解』Codeforces 1758B XOR = Average
    Description构造一个\(a\)序列,使\(a_1\oplusa_2\oplusa_3\oplus\cdots\oplusa_n=\dfrac{sum(a)}{n}\)。Solution第一眼看到这道题,我想到的是分情况讨论。......
  • NOIP2022 游记
    当右下角的时间跳转为13时,当耳边传来周围选手起坐离场的声音时,2022的最后一场赛事终于拉下了帷幕2022.9.18CSP-S初赛考试的前一天vp了SCP的模拟卷,成绩不太理想,......
  • 第十三届蓝桥杯省赛与国赛真题题解大汇总(十四届参赛者必备)
    文章前言  大家好,我是执梗。本文汇总了我写的第十三届蓝桥杯所有省赛真题与国赛真题,会针对每道题打出我自己的难度评星⭐️,也会给出每道题的算法标签,帮助大家更有针对性的去......
  • NOIP2022游记
    恶啊,退役之战。不料竟遇到这种鬼赛。赛前一天打板子,结果最终只打了许多数论、普通的树剖和tarjan,结果它还真考,真是喵了个喵(雾(知识点查全率100%查准率33%结果中午去化......
  • [报错解决](Error Creating bean with name ‘xxx‘)类问题解决思路
    遇到ErrorCreatingbeanwithname’'这类问题的解决思路错误日志关键部分:org.springframework.beans.factory.UnsatisfiedDependencyException:Errorcreatingbeanwi......
  • 游记 NOIP2022 E 类
    day0GDFZ模拟赛爆成20QWQ怎么办我还有救吗/kk我们要调整心态,整心态调,心态调整,态调整心,调整心态,如此我们便调整了心态。RP++!23:15分,睡觉了。day1整个学校的考......
  • 题解 [ABC279F] BOX
    这种合并集合的操作使我们想到并查集,因此我们在并查集算法的基础上进行改造来解决问题。这里使用路径压缩实现的并查集。在记录并查集的父亲数组的同时,我们还需要记录两个......
  • 题解 [ABC279E] Cheating Amidakuji
    曾经总结过一类分治套路,没想到竟然派上用场了。这种每个操作依次缺席的问题可以通过分治来解决。设solve(l,r)表示缺席的操作在\([l,r]\)之间时求出它们的答案。设......