【杂题题解】洛谷 P2279 消防局的设立

题目传送门

题目大意

给定一个包含 \(n\) 个结点的树,一个消防站能够覆盖与他距离小于 \(2\) 的点。

要求每个结点都能被覆盖到,问至少需要多少个消防站。

\(1\le n\le 10^3\)

题目分析

由于以前做个一个要求距离为 \(1\) 的版本,因此场上推了 \(60+\) 行,到后来分享做法时老师给的评价:

你这 DP 状态转移方程应该重复了吧,而且有很多冗余,回去再想想吧。

看完题解后深受感悟,因此准备写一篇博客纪念。


下面我们即将展示 ChrisPang 自制 DP 分析法!

  • 状态

    \(f(x,0)\) 表示结点 \(x\) 至少向上覆盖两层需要的消防站数量

    \(f(x,1)\) 表示结点 \(x\) 至少向上覆盖一层需要的消防站数量

    \(f(x,2)\) 表示结点 \(x\) 至少覆盖到当前层需要的消防站数量

    \(f(x,3)\) 表示结点 \(x\) 至少覆盖所有儿子需要的消防站数量

    \(f(x,4)\) 表示结点 \(x\) 至少覆盖所有孙子需要的消防站数量

    注意到“至少”,因此存在 \(f(x,0)\ge f(x,1)\ge f(x,2)\ge f(x,3)\ge f(x,4)\),这个结论很重要,后面要用到。

  • 状态转移方程

    对于 \(f(x,0)\),相当于在 \(x\) 建立一个消防站。此时 \(x\) 结点已经把往下两层(也就是所有孙子)给覆盖到了,儿子结点只要选择 \(f(v,4)\)(此处 \(v\) 代表 \(x\) 的儿子,下同)就可以了。因此 \(f(x,0)=1+\sum v \{f(v,4)\}\)

    对于 \(f(x,1)\),要求必须挑出一个儿子选择 \(f(v,0)\),其它儿子因为那个选择 \(f(v,0)\) 的儿子,所以它们选择 \(f(v,3)\)\(f(v,0)\) 相当于能向上覆盖两个,而兄弟之间距离恰好为 \(2\),因此别的儿子只要管好自己的儿子就行了)。转移方程见代码。

    对于 \(f(x,2)\),要求必须挑出一个儿子选择 \(f(v,1)\),其它儿子选择 \(f(v,2)\)(管好自己就行了),转移方程见代码。

    对于 \(f(x,3)\),要求所有儿子必须都管好自己,因此 \(f(x,3)=\sum v \{f(v,2)\}\)

    对于 \(f(x,4)\),要求所有儿子必须都管好自己的儿子,因此 \(f(x,4)=\sum v \{f(v,3)\}\)

    按照上述操作更新完毕,又考虑到最终 \(f(x,0)\ge f(x,1)\ge f(x,2)\ge f(x,3)\ge f(x,4)\),因此 \(f(x,i)=\min_{k=0}^{i} f(x,k)\)

  • 初始化:对于所有的叶子节点 \(x\),要求 \(f(x,0)=f(x,1)=f(x,2)=1,f(x,3)=f(x,4)=0\)

答案就是 \(f(1,2)\)(根据状态定义得出)

代码实现

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

const int N = 1e3 + 10;
const int inf = 1e9 + 10;

int n, f[N][8];
vector<int> linker[N];

/*
  f[x][0] 表示至少向上覆盖两层
  f[x][1] 表示至少向上覆盖一层
  f[x][2] 表示至少覆盖到当前层
  f[x][3] 表示至少覆盖所有儿子
  f[x][4] 表示至少覆盖所有孙子
*/

void add(int x, int y) {
    linker[x].push_back(y);
}

void dfs(int x, int fa) {
    int sum1 = 0, sum2 = 0;
    // 这里 sum1 与 sum2 分别求出 f[v][3] 与 f[v][2] 的和,为后面更新 f[x][1] 与 f[x][2] 做准备
    f[x][0] = 1; // 在当前点建立一个消防站

    for (int v : linker[x]) {
        if (v == fa) continue;
        dfs(v, x);

        // 正常转移,忘了往上翻
        f[x][0] += f[v][4];
        f[x][3] += f[v][2];
        f[x][4] += f[v][3];

        // 预处理 sum1 与 sum2
        sum1 += f[v][3];
        sum2 += f[v][2];
    }

    // 叶子节点
    if (linker[x].empty()) {
        f[x][0] = f[x][1] = f[x][2] = 1;
        return ;
    }

    f[x][1] = f[x][2] = inf;
    for (int v : linker[x]) {
        if (v == fa) continue;

        // 要求取出一个儿子,这个儿子要选择 f[v][0],其它的儿子选择 f[v][3]
        f[x][1] = min(f[x][1], sum1 - f[v][3] + f[v][0]);

        // 要求取出一个儿子,这个儿子要选择 f[v][1],其它的儿子选择 f[v][2]
        f[x][2] = min(f[x][2], sum2 - f[v][2] + f[v][1]);
    }

    // 由于上述不等式可以推导出来,在这里将所有的 f[x][i] 放在一起更新
    for (int i = 1; i < 5; i++)
        f[x][i] = min(f[x][i], f[x][i - 1]);
}

signed main() {
    cin >> n;

    for (int i = 2, x; i <= n; i++) {
        cin >> x;
        add(i, x);
        add(x, i);
    }

    dfs(1, 0);

    cout << f[1][2] << endl;

    return 0;
}
posted @ 2025-11-13 19:58  chrispang  阅读(1)  评论(0)    收藏  举报