1 条题解

  • 0
    @ 2025-10-15 22:05:40

    「POI2017 R1」破坏 Sabotage 题解

    问题深入分析

    我们有一个树形组织,根节点为1号(最高领导)。关键规则如下:

    叛乱传播机制

    1. 某个员工作为破坏者率先叛乱(但不强迫下属)
    2. 对于每个员工u:如果u的直接下属中叛乱的比例 > x,则u也会叛乱
    3. u叛乱后,会强迫所有直接和间接下属跟随叛乱
    4. 叛乱只向上传播,不会向下或横向传播

    目标:找到最小的x,使得任意员工作为破坏者时,总叛乱人数 ≤ k

    关键性质与观察

    1. 单调性分析

    • x ↗ → 叛乱传播条件更严格 → 最大叛乱人数 ↘
    • x ↘ → 叛乱传播条件更宽松 → 最大叛乱人数 ↗
    • 因此可以使用二分答案

    2. 叛乱传播的数学表达

    对于节点u,设:

    • children[u]:u的直接下属集合
    • rebel_children:u的直接下属中叛乱的数量

    叛乱条件:rebel_children / |children[u]| > x

    即:rebel_children > x * |children[u]|

    由于人数是整数,等价于:rebel_children ≥ ⌊x * |children[u]|⌋ + 1

    3. 问题转化

    对于给定的x,我们需要检查:是否存在某个破坏者v,使得引发的总叛乱人数 > k

    算法设计

    方法一:树形DP + 二分答案(推荐)

    状态定义

    • size[u]:以u为根的子树大小
    • dp[u]:如果u叛乱,最少需要多少个直接下属已经叛乱

    状态转移

    对于叶子节点:dp[u] = 1(没有下属,条件自动满足)

    对于非叶子节点:

    dp[u] = ⌈x * |children[u]|⌉
    

    即:需要至少dp[u]个直接下属叛乱,u才会叛乱

    检查函数设计

    bool check(double x) {
        // 计算每个节点的阈值
        for (int u = n; u >= 1; u--) {
            if (children[u].empty()) {
                dp[u] = 1;  // 叶子节点
            } else {
                dp[u] = ceil(x * children[u].size());
            }
        }
        
        // 检查每个可能的破坏者
        for (int v = 1; v <= n; v++) {
            int total = count_rebellion(v, x);
            if (total > k) return false;
        }
        return true;
    }
    

    叛乱人数计算

    从破坏者v开始:

    1. v率先叛乱
    2. 向上遍历祖先,检查每个祖先u:
      • 如果u的已叛乱直接下属数量 ≥ dp[u],则u叛乱
      • 继续向上检查
    3. 统计所有叛乱节点

    方法二:优化算法

    观察到:对于破坏者v,叛乱传播路径是从v到根的链上的连续一段。

    我们可以预处理:

    • up[u]:从u开始向上,最多能传播到哪个祖先
    • 然后O(1)计算每个破坏者的影响范围

    复杂度分析

    • 二分答案:O(log(1e8)) ≈ 27次
    • 每次检查:O(n) 计算dp值 + O(n) 检查每个破坏者
    • 总复杂度:O(n log(1/ε)),满足n ≤ 5e5

    实现细节

    1. 树结构存储

    vector<int> children[MAXN];
    int parent[MAXN], size[MAXN];
    

    2. 预处理

    void dfs(int u) {
        size[u] = 1;
        for (int v : children[u]) {
            dfs(v);
            size[u] += size[v];
        }
    }
    

    3. 二分精度控制

    double l = 0, r = 1;
    for (int iter = 0; iter < 50; iter++) {
        double mid = (l + r) / 2;
        if (check(mid)) r = mid;
        else l = mid;
    }
    

    样例详细分析

    输入:

    9 3
    1
    1
    2
    2
    2
    3
    7
    3
    

    树结构:

        1
       / \
      2   3
     /|\  |\
    4 5 6 7 9
          |
          8
    

    当x = 0.666...时:

    • 节点3有2个直接下属(7,9),阈值 = ceil(0.666×2) = 2
    • 节点7有1个直接下属(8),阈值 = ceil(0.666×1) = 1

    破坏者8号:

    • 8叛乱 → 7有1/1=100%下属叛乱 > 0.666 → 7叛乱
    • 7叛乱 → 3有1/2=50%下属叛乱 ≤ 0.666 → 停止传播
    • 总叛乱:3,7,8,9 → 4人 > k=3

    因此需要x ≥ 2/3。

    边界情况处理

    1. k = 1:x = 1.0(只有破坏者本人叛乱)
    2. k = n:x = 0.0(允许所有人叛乱)
    3. 链状树:传播更容易,需要更大的x
    4. 星形树:根节点需要很多下属叛乱才会加入

    总结

    本题的关键在于:

    1. 理解叛乱传播的数学条件
    2. 将连续变量x的求解转化为二分答案问题
    3. 设计高效的检查函数,利用树形DP计算传播范围
    4. 注意整数人数与实数比例之间的转换

    通过二分搜索士气值x,并在每次检查中模拟最坏情况的破坏者,我们能够高效地找到满足条件的最小x值。

    • 1

    信息

    ID
    3157
    时间
    1000ms
    内存
    256MiB
    难度
    10
    标签
    递交数
    4
    已通过
    1
    上传者