经典题,但还是想写一篇完整而又详细美观的题解。
本文含有贪心及 dp 的做法,适合所有语言人群阅读。
1 Perface \Large\textbf{1 Perface} 1 Perface
本题的 42 42 42 篇题解(指洛谷 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 贪心策略
我们贪心地先选择深度大的节点,由于一个消防站能够覆盖五层的节点,我们找出当前选择节点 u u u 的父亲 v v v 与爷爷 w w w。
开三个 bool 数组 diz, dio, dit \text{diz, dio, dit} diz, dio, dit 分别记录以下信息:
- diz \text{diz} diz:是否被距离为 0 0 0 的点覆盖:消防站在 u u u 上。
- dio \text{dio} dio:被距离为 1 1 1 的点覆盖:消防站在 v v v 或 u u u 的儿子上。
- dit \text{dit} dit:被距离为 2 2 2 的点覆盖:消防站在 w w w 或 u u u 的兄弟( v v v 的儿子或 w w w 的孙子)上。
每次遍历时只要寻找一下 u , v , w u,v,w u,v,w 对应的 bool 值是否为 false \textbf{false} false 即可。
以下是不在 w w w 点染色的情况。
- diz[ u,v,w ] \text{diz[\textit{u,v,w}]} diz[u,v,w] 中有一个值为 true \textbf{true} true:意味着 u u u 点已经被覆盖到,没必要对 w w w 染色。
- dio[ u,v ] \text{dio[\textit{u,v}]} dio[u,v] 中有一个值为 true \textbf{true} true:意味着 u u u 点已经被覆盖到,没必要对 w w w 染色。
- dit[ u ] \text{dit[\textit{u}]} dit[u] 的值为 true \textbf{true} true:意味着 u u u 点已经被覆盖到,没必要对 w w w 染色。
(上述情况的证明非常容易,留作习题。)
判完这些情况,我们就可以对 w w w 进行染色,并将 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 , v u,v u,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 进行排序,先扫描到点 7 7 7。
经过对爷爷 4 4 4 和父亲 6 6 6 的查询,我们发现点 4 4 4 可染色,于是便标记 1 , 3 , 4 1,3,4 1,3,4, dit[1] = true \text{dit[1] = \textbf{true}} dit[1] = true。
接下来依次扫描点 5 , 6 , 4 5,6,4 5,6,4,发现都不符合要求,不进行染色操作。
当扫描到点 2 2 2 时,我们发现它的父亲 1 1 1 的 dio \text{dio} dio 值为 false \textbf{false} false。(之前标记的是 1 1 1 的 dit \text{dit} dit 值),且其他对应的 bool 值都符合要求,于是我们对 2 2 2 的爷爷(假设有)进行染色。
答案为 2 2 2,完美覆盖了整棵树。
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],设 d p [ 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) 表示所有 i i i 的子孙与祖先 u u u 中满足 dep [ u ] ≥ d − j \text{dep}[u]\ge d-j dep[u]≥d−j 的点都能被覆盖到。
注意到对于 − 2 ≤ j ≤ k ≤ 2 -2\le j\le k\le 2 −2≤j≤k≤2,覆盖 k k k 层的情况一定包含了覆盖 j j j 层的情况,故有 d p [ i ] [ j ] ≤ d p [ i ] [ k ] dp[i][j]\le dp[i][k] dp[i][j]≤dp[i][k],当下列出现至少覆盖 j j j 层的字眼时,默认选择 d p dp dp 值最小的,即选择 d p [ i ] [ j ] dp[i][j] dp[i][j]。
2.2.1 dp 转移方程 \textbf{2.2.1 dp 转移方程} 2.2.1 dp 转移方程
容易发现,若 i i i 要覆盖到 2 2 2 层,则 i i i 本身必须被染色且 i i i 的儿子们也必须覆盖到至少 − 2 -2 −2 层,即
d p [ i ] [ 2 ] = 1 + ∑ u ∈ s o n ( i ) d p [ 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]
要求 d p [ i ] [ 1 ] dp[i][1] dp[i][1],我们发现有两种情况:
- 若覆盖恰好 1 1 1 层,则 i i i 的儿子中必有一个点 u u u 经过了染色,其他儿子 v v v 都覆盖了至少 − 1 -1 −1 层。
- 若覆盖 2 2 2 层,答案就是 d p [ i ] [ 2 ] dp[i][2] dp[i][2]。
于是就有式子
d p [ i ] [ 1 ] = min ( d p [ i ] [ 2 ] , min { d p [ u ] [ 2 ] + ∑ v ∈ s o n ( i ) ∧ v ≠ u d p [ 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]⎭⎬⎫⎠⎞
求 d p [ i ] [ 0 ] dp[i][0] dp[i][0],有以下两种情况:
- 恰好覆盖 0 0 0 层,则 i i i 的儿子中至少有一个点 u u u 能覆盖 1 1 1 层,其他儿子 v v v 都覆盖了至少 0 0 0 层。
- 若覆盖 1 , 2 1,2 1,2 层,则答案就是 min ( d p [ i ] [ 1 ] , d p [ 1 ] [ 0 ] ) \min(dp[i][1],dp[1][0]) min(dp[i][1],dp[1][0])。
于是就有式子:
d p [ i ] [ 0 ] = min ( d p [ i ] [ 1 ] , d p [ i ] [ 2 ] , min { d p [ u ] [ 1 ] + ∑ v ∈ s o n ( i ) ∧ v ≠ u d p [ 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]⎭⎬⎫⎠⎞
- 对于求解 d p [ i ] [ − 1 ] dp[i][-1] dp[i][−1],我们只要让其所有儿子都被覆盖。
- 对于求解 d p [ i ] [ − 2 ] dp[i][-2] dp[i][−2],我们只要让其所有孙子都被覆盖。
于是有式子:
d p [ i ] [ − 1 ] = min ( d p [ i ] [ 0 ∼ 2 ] , ∑ u ∈ s o n ( i ) d p [ u ] [ 0 ] ) d p [ i ] [ − 2 ] = min ( d p [ i ] [ − 1 ∼ 2 ] , ∑ u ∈ s o n ( i ) d p [ 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 个转移方程后,即可开始转移。
最后的答案是覆盖节点 1 1 1 的 0 0 0 层所需的最小染色点数,即 d p [ 1 ] [ 0 ] dp[1][0] dp[1][0]。
为了方便,我们在开数组的时候,将第二维统一加上 2 2 2,将 − 2 ∼ 2 -2\sim2 −2∼2 变为 0 ∼ 4 0\sim4 0∼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;
}
这里计算 d p [ i ] [ 0 ] , d p [ 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
就这些吧。