经典题,但还是想写一篇完整而又详细美观的题解。
本文含有贪心及 dp 的做法,适合所有语言人群阅读。
1 Perface\Large\textbf{1 Perface}1 Perface
本题的 424242 篇题解(指洛谷 P2279),可以说是没有一篇做到了内容与美观程度并存,而这又是一道经典题目,所以打算再次写一篇题解。
2 Solution\Large\textbf{2 Solution}2 Solution
2.1 贪心\large\textbf{2.1 贪心}2.1 贪心
2.1.1 贪心策略\textbf{2.1.1 贪心策略}2.1.1 贪心策略
我们贪心地先选择深度大的节点,由于一个消防站能够覆盖五层的节点,我们找出当前选择节点 uuu 的父亲 vvv 与爷爷 www。
开三个 bool 数组 diz, dio, dit\text{diz, dio, dit}diz, dio, dit 分别记录以下信息:
- diz\text{diz}diz:是否被距离为 000 的点覆盖:消防站在 uuu 上。
- dio\text{dio}dio:被距离为 111 的点覆盖:消防站在 vvv 或 uuu 的儿子上。
- dit\text{dit}dit:被距离为 222 的点覆盖:消防站在 www 或 uuu 的兄弟(vvv 的儿子或 www 的孙子)上。
每次遍历时只要寻找一下 u,v,wu,v,wu,v,w 对应的 bool 值是否为 false\textbf{false}false 即可。
以下是不在 www 点染色的情况。
- diz[u,v,w]\text{diz[\textit{u,v,w}]}diz[u,v,w] 中有一个值为 true\textbf{true}true:意味着 uuu 点已经被覆盖到,没必要对 www 染色。
- dio[u,v]\text{dio[\textit{u,v}]}dio[u,v] 中有一个值为 true\textbf{true}true:意味着 uuu 点已经被覆盖到,没必要对 www 染色。
- dit[u]\text{dit[\textit{u}]}dit[u] 的值为 true\textbf{true}true:意味着 uuu 点已经被覆盖到,没必要对 www 染色。
(上述情况的证明非常容易,留作习题。)
判完这些情况,我们就可以对 www 进行染色,并将 diz[w], dio[fa(w)], dit[fa(fa(u))]\text{diz[\textit w], dio[fa(\textit w)], dit[fa(fa(\textit u))]}diz[w], dio[fa(w)], dit[fa(fa(u))] 标记为 true\textbf{true}true。
不管 u,vu,vu,v 的原因是以后都用不上它们了。
还不明白?那就看看下面的图理解。
2.1.2 贪心实例\textbf{2.1.2 贪心实例}2.1.2 贪心实例
考虑如下的一棵树。

(dep[i] 表示节点 i 的深度) \textbf{(dep[\textit i] 表示节点 \textit i 的深度)} (dep[i] 表示节点 i 的深度)
我们先对 dep\text{dep}dep 进行排序,先扫描到点 777。

经过对爷爷 444 和父亲 666 的查询,我们发现点 444 可染色,于是便标记 1,3,41,3,41,3,4,dit[1] = true\text{dit[1] = \textbf{true}}dit[1] = true。

接下来依次扫描点 5,6,45,6,45,6,4,发现都不符合要求,不进行染色操作。
当扫描到点 222 时,我们发现它的父亲 111 的 dio\text{dio}dio 值为 false\textbf{false}false。(之前标记的是 111 的 dit\text{dit}dit 值),且其他对应的 bool 值都符合要求,于是我们对 222 的爷爷(假设有)进行染色。
答案为 222,完美覆盖了整棵树。
2.1.3 贪心代码\textbf{2.1.3 贪心代码}2.1.3 贪心代码
#include <cstdio>
#include <algorithm>
#define N 2021
using namespace std;
bool diz[N], dio[N], dit[N];
int fa[N], de[N], ind[N];
bool cmp(int a, int b){
return de[a] > de[b];
}
bool check(int u, int op){
if(op == 0) return (!diz[u]) && (!dio[u]) && (!dit[u]);
if(op == 1) return (!diz[u]) && (!dio[u]);
return !diz[u];
}
int n;
int ans = 0;
int main(){
scanf("%d", &n);
ind[1] = 1;
for(int i = 2 ; i <= n ; i++){
scanf("%d", &fa[i]);
de[i] = de[fa[i]] + 1;
ind[i] = i;
}
sort(ind + 1, ind + n + 1, cmp);
for(int i = 1 ; i <= n ; i++){
int u = ind[i], v = fa[u], w = fa[v], x = fa[w], y = fa[x];
if(check(u, 0) && check(v, 1) && check(w, 2))
ans++, diz[w] = 1, dio[x] = 1, dit[y] = 1;
}
printf("%d\n", ans);
return 0;
}
2.2 dp\large\textbf{2.2 dp}2.2 dp
2.2.1 dp 状态设计\textbf{2.2.1 dp 状态设计}2.2.1 dp 状态设计
令 d=dep[i]d=\text{dep}[i]d=dep[i],设 dp[i][j](1≤i≤n,−2≤j≤2)dp[i][j](1\le i\le n,-2\le j\le 2)dp[i][j](1≤i≤n,−2≤j≤2) 表示所有 iii 的子孙与祖先 uuu 中满足 dep[u]≥d−j\text{dep}[u]\ge d-jdep[u]≥d−j 的点都能被覆盖到。
注意到对于 −2≤j≤k≤2-2\le j\le k\le 2−2≤j≤k≤2,覆盖 kkk 层的情况一定包含了覆盖 jjj 层的情况,故有 dp[i][j]≤dp[i][k]dp[i][j]\le dp[i][k]dp[i][j]≤dp[i][k],当下列出现至少覆盖 jjj 层的字眼时,默认选择 dpdpdp 值最小的,即选择 dp[i][j]dp[i][j]dp[i][j]。
2.2.1 dp 转移方程\textbf{2.2.1 dp 转移方程}2.2.1 dp 转移方程
容易发现,若 iii 要覆盖到 222 层,则 iii 本身必须被染色且 iii 的儿子们也必须覆盖到至少 −2-2−2 层,即
dp[i][2]=1+∑u∈son(i)dp[u][−2] dp[i][2]=1+\sum\limits_{u\in son(i)}dp[u][-2] dp[i][2]=1+u∈son(i)∑dp[u][−2]
要求 dp[i][1]dp[i][1]dp[i][1],我们发现有两种情况:
- 若覆盖恰好 111 层,则 iii 的儿子中必有一个点 uuu 经过了染色,其他儿子 vvv 都覆盖了至少 −1-1−1 层。
- 若覆盖 222 层,答案就是 dp[i][2]dp[i][2]dp[i][2]。
于是就有式子
dp[i][1]=min(dp[i][2],min{dp[u][2]+∑v∈son(i)∧v≠udp[u][−1]}) dp[i][1]=\min\left(dp[i][2],\min\left\{dp[u][2]+\sum\limits_{v\in son(i)\land v\ne u}dp[u][-1]\right\}\right) dp[i][1]=min⎝⎛dp[i][2],min⎩⎨⎧dp[u][2]+v∈son(i)∧v=u∑dp[u][−1]⎭⎬⎫⎠⎞
求 dp[i][0]dp[i][0]dp[i][0],有以下两种情况:
- 恰好覆盖 000 层,则 iii 的儿子中至少有一个点 uuu 能覆盖 111 层,其他儿子 vvv 都覆盖了至少 000 层。
- 若覆盖 1,21,21,2 层,则答案就是 min(dp[i][1],dp[1][0])\min(dp[i][1],dp[1][0])min(dp[i][1],dp[1][0])。
于是就有式子:
dp[i][0]=min(dp[i][1],dp[i][2],min{dp[u][1]+∑v∈son(i)∧v≠udp[u][0]}) dp[i][0]=\min\left(dp[i][1],dp[i][2],\min\left\{dp[u][1]+\sum\limits_{v\in son(i)\land v\ne u}dp[u][0]\right\}\right) dp[i][0]=min⎝⎛dp[i][1],dp[i][2],min⎩⎨⎧dp[u][1]+v∈son(i)∧v=u∑dp[u][0]⎭⎬⎫⎠⎞
- 对于求解 dp[i][−1]dp[i][-1]dp[i][−1],我们只要让其所有儿子都被覆盖。
- 对于求解 dp[i][−2]dp[i][-2]dp[i][−2],我们只要让其所有孙子都被覆盖。
于是有式子:
dp[i][−1]=min(dp[i][0∼2],∑u∈son(i)dp[u][0])dp[i][−2]=min(dp[i][−1∼2],∑u∈son(i)dp[u][−1]) \begin{matrix} dp[i][-1]=\min\left(dp[i][0\sim 2],\sum\limits_{u\in son(i)}dp[u][0]\right)\\ dp[i][-2]=\min\left(dp[i][-1\sim 2],\sum\limits_{u\in son(i)}dp[u][-1]\right) \end{matrix} dp[i][−1]=min(dp[i][0∼2],u∈son(i)∑dp[u][0])dp[i][−2]=min(dp[i][−1∼2],u∈son(i)∑dp[u][−1])
我们得到了上述 5 个转移方程后,即可开始转移。
最后的答案是覆盖节点 111 的 000 层所需的最小染色点数,即 dp[1][0]dp[1][0]dp[1][0]。
为了方便,我们在开数组的时候,将第二维统一加上 222,将 −2∼2-2\sim2−2∼2 变为 0∼40\sim40∼4,防止 RE。
2.2.1 dp 代码\textbf{2.2.1 dp 代码}2.2.1 dp 代码
#include <cstdio>
#include <vector>
#include <algorithm>
#define N 2021
#define INF 99999999
using namespace std;
vector <int> t[N];
int dp[N][5];
int n;
void dfs(int u){
dp[u][4] = 1, dp[u][1] = 0, dp[u][0] = 0;
int len = t[u].size();
for(int i = 0 ; i < len ; i++){
int v = t[u][i];
dfs(v);
dp[u][4] += dp[v][0];
dp[u][1] += dp[v][2];
dp[u][0] += dp[v][1];
}
if(!len) dp[u][2] = dp[u][3] = 1;
else{
dp[u][2] = dp[u][3] = INF;
int cnt1 = 0, cnt2 = 0;
// %
for(int i = 0 ; i < len ; i++){
int v = t[u][i];
cnt1 += dp[v][1];
cnt2 += dp[v][2];
}
for(int i = 0 ; i < len ; i++){
int v = t[u][i];
dp[u][3] = min(cnt1 - dp[v][1] + dp[v][4], dp[u][3]);
dp[u][2] = min(cnt2 - dp[v][2] + dp[v][3], dp[u][2]);
}
// %
}
for(int i = 3 ; i >= 0 ; i--)
dp[u][i] = min(dp[u][i + 1], dp[u][i]);
}
int main(){
scanf("%d", &n);
for(int i = 2 ; i <= n ; i++){
int u;
scanf("%d", &u);
t[u].push_back(i);
}
dfs(1);
printf("%d\n", dp[1][2]);
return 0;
}
这里计算 dp[i][0],dp[i][1]dp[i][0],dp[i][1]dp[i][0],dp[i][1] 的部分(注释 %\tt \%% 的部分)运用了一个先计算总和再减去对应部分的 trick,复杂度有所降低。
3 Compare\Large\textbf{3 Compare}3 Compare
如图,

贪心更好 /hanx
就这些吧。
本文针对一道经典算法题目提供详细的贪心与动态规划解决方案。通过深入解析贪心策略及其实例演示,结合DP状态设计与转移方程,为读者提供了全面的技术指导。
448

被折叠的 条评论
为什么被折叠?



