【杂题题解】洛谷 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;
}

浙公网安备 33010602011771号