原题: https://www.luogu.org/problemnew/show/P2279
题意:
n个点的树,设一个点为消防局,那么离这个点距离2以内的点会被覆盖。问最少的消防局数量使整棵树被覆盖。
树形DP:
先维护好儿子再推父亲的树形dp。
dp[i][0~4]表示遍历到i点时的5种状态:
- dp[i][0]:已经延到i点,且还可以往上2长度
- dp[i][1]:已经延到i点,且还可以往上1长度
- dp[i][2]:已经延到i点,且还可以往上0长度
- dp[i][3]:还未到达i点,父亲节点需要往下延1长度
- dp[i][4]:还未到达i点,父亲节点需要往下延2长度
分析儿子状态对父亲状态的转移:
- dp[fa][0]:自己放一个消费局,所以儿子是什么状态都不用管了,dp[i][0~4]都可以。
- dp[fa][1]:保证有一个儿子的状态为dp[i][0],这个儿子可以延到其他儿子,但不能延到其他儿子的儿子,所以需要其它儿子的dp[i][0~3]。
- dp[fa][2]:保证有一个儿子的状态为dp[i][1],这个儿子不能延到其他儿子,所以需要其他儿子的dp[i][0~2]。
- dp[fa][3]:不考虑自身,但需要保证fa的儿子、孙子全被覆盖,所以dp[i][0~2]。
- dp[fa][4]:不考虑自身和儿子,需要所有孙子都覆盖,所以dp[i][0~3]。
状态转移方程:
- d p [ f ] [ 0 ] = 1 + ∑ i m i n ( d p [ i ] [ 0...4 ] ) dp[f][0]=1+\sum_imin(dp[i][0...4]) dp[f][0]=1+∑imin(dp[i][0...4])
- d p [ f ] [ 1 ] = m i n ( d p [ i ] [ 0 ] + ∑ j ! = i m i n ( d p [ j ] [ 0...3 ] ) ) dp[f][1]=min(dp[i][0]+\sum_{j!=i}min(dp[j][0...3])) dp[f][1]=min(dp[i][0]+∑j!=imin(dp[j][0...3]))
- d p [ f ] [ 2 ] = m i n ( d p [ i ] [ 1 ] + ∑ j ! = i m i n ( d p [ j ] [ 0...2 ] ) ) dp[f][2]=min(dp[i][1]+\sum_{j!=i}min(dp[j][0...2])) dp[f][2]=min(dp[i][1]+∑j!=imin(dp[j][0...2]))
- d p [ f ] [ 3 ] = ∑ i m i n ( d p [ i ] [ 0..2 ] ) dp[f][3]=\sum_imin(dp[i][0..2]) dp[f][3]=∑imin(dp[i][0..2])
- d p [ f ] [ 4 ] = ∑ i m i n ( d p [ i ] [ 0..3 ] ) dp[f][4]=\sum_imin(dp[i][0..3]) dp[f][4]=∑imin(dp[i][0..3])
d
p
[
i
]
[
1
]
+
∑
j
!
=
i
m
i
n
(
d
p
[
j
]
[
0...2
]
)
dp[i][1]+\sum_{j!=i}min(dp[j][0...2])
dp[i][1]+∑j!=imin(dp[j][0...2])可以转化为:
d
p
[
i
]
[
1
]
−
d
p
[
i
]
[
0...2
]
+
∑
m
i
n
(
d
p
[
j
]
[
0...2
]
)
\quad dp[i][1]-dp[i][0...2]+\sum min(dp[j][0...2])
dp[i][1]−dp[i][0...2]+∑min(dp[j][0...2])
到这里应该已经可以做了,但是因为很多项可以合并以优化时间复杂度。
令 S [ i ] [ j ] = min ( d p [ i ] [ 0... j ] ) S[i][j]=\min(dp[i][0...j]) S[i][j]=min(dp[i][0...j])
- d p [ f ] [ 0 ] = 1 + ∑ i S [ i ] [ 4 ] dp[f][0]=1+\sum_i S[i][4] dp[f][0]=1+∑iS[i][4]
- d p [ f ] [ 3 ] = ∑ i S [ i ] [ 2 ] dp[f][3]=\sum_i S[i][2] dp[f][3]=∑iS[i][2]
- d p [ f ] [ 4 ] = ∑ i S [ i ] [ 3 ] dp[f][4]=\sum_i S[i][3] dp[f][4]=∑iS[i][3]
- d p [ f ] [ 1 ] = ∑ i S [ i ] [ 3 ] + m i n ( d p [ i ] [ 0 ] − S [ i ] [ 3 ] ) dp[f][1]=\sum_i S[i][3] +min(dp[i][0]-S[i][3]) dp[f][1]=∑iS[i][3]+min(dp[i][0]−S[i][3])
- d p [ f ] [ 2 ] = ∑ i S [ i ] [ 2 ] + m i n ( d p [ i ] [ 1 ] − S [ i ] [ 2 ] ) dp[f][2]=\sum_i S[i][2] +min(dp[i][1]-S[i][2]) dp[f][2]=∑iS[i][2]+min(dp[i][1]−S[i][2])
可以发现S只需要234,而dp需要01,可以放在dp上一起使用。
#include<bits/stdc++.h>
using namespace std;
const int N = 1005, M = 2005;
int head[N], nex[M], to[M], now;
void add(int a, int b) {
nex[++now] = head[a];
head[a] = now;
to[now] = b;
}
int dp[N][6];
void dfs(int p, int f) {
int s2 = 0, s3 = 0, s4 = 0, sub03 = 1e9, sub12 = 1e9;
for(int i = head[p]; ~i; i = nex[i]) {
int u = to[i];
if(u == f)
continue;
dfs(u, p);
s2 += dp[u][2];
s3 += dp[u][3];
s4 += dp[u][4];
sub03 = min(sub03, dp[u][0] - dp[u][3]);
sub12 = min(sub12, dp[u][1] - dp[u][2]);
}
dp[p][0] = 1 + s4;
dp[p][1] = s3 + sub03;
dp[p][2] = s2 + sub12;
dp[p][3] = s2;
dp[p][4] = s3;
// dp to S
dp[p][2] = min(min(dp[p][0], dp[p][1]), dp[p][2]);
dp[p][3] = min(dp[p][2], dp[p][3]);
dp[p][4] = min(dp[p][3], dp[p][4]);
return ;
}
int main() {
memset(head, -1, sizeof head);
int n;
scanf("%d", &n);
for(int i = 2; i <= n; i++) {
int tmp;
scanf("%d", &tmp);
add(i, tmp);
add(tmp, i);
}
dfs(1, -1);
printf("%d\n", dp[1][2]);
}
贪心:
更简单、更有效、使用范围更广
对于当前还没有被遍历的点中的最低点 I I I,在 I I I的眼中其他的点只有: I I I的父亲, I I I的爷爷, I I I的兄弟。
显然选择 I I I的爷爷最优,不仅解决了这里的所有点,还可以再往上延两个点。
现在有两个问题:最低点的寻找、更新的维护。
尝试使用数组按照深度顺序存储,从前往后线性遍历。就解决了最低点的问题。
至于更新点的维护,开一个dis数组,代表某个点的子树中,前dis层被覆盖。对于 I I I点,标记其爷爷后,dis[爷爷]=2,dis[父亲]=1,dis[曾爷爷]=1,dis[曾曾爷爷]=0。
那么对于遍历其他点的时候,看看它父亲和爷爷的dis值即可判断是否被覆盖。
#include<bits/stdc++.h>
using namespace std;
const int N = 1005, M = 2005;
int head[N], nex[M], to[M], now;
void add(int a, int b) {
nex[++now] = head[a];
head[a] = now;
to[now] = b;
}
int fa[N], deep[N], dis[N];
void dfs(int p, int f) {
for(int i = head[p]; ~i; i = nex[i]) {
int u = to[i];
if(u == f)
continue;
fa[u] = p;
deep[u] = deep[p] + 1;
dfs(u, p);
}
return ;
}
int tmp[N];
bool cmp(const int a, const int b) {
return deep[a] > deep[b];
}
int main() {
memset(head, -1, sizeof head);
memset(dis, -1, sizeof(dis));
int n;
scanf("%d", &n);
for(int i = 2; i <= n; i++) {
int tmp;
scanf("%d", &tmp);
add(i, tmp);
add(tmp, i);
}
// 考虑超过root的情况
fa[1] = 0;
fa[0] = 0;
deep[1] = 1;
dfs(1, -1);
for(int i = 1; i <= n; i++)
tmp[i] = i;
sort(tmp + 1, tmp + 1 + n, cmp);
int ans = 0;
for(int i = 1; i <= n; i++) {
int p = tmp[i], f = fa[p], ff = fa[f], fff = fa[ff], ffff = fa[fff];
if(dis[p] >= 0 || dis[f] >= 1 || dis[ff] >= 2)
continue;
ans++;
dis[f] = max(dis[f], 1);
dis[ff] = max(dis[ff], 2);
dis[fff] = max(dis[fff], 1);
dis[ffff] = max(dis[ffff], 0);
}
printf("%d\n", ans);
}