[HNOI2003] 消防局的设立

经典题,但还是想写一篇完整而又详细美观的题解。

本文含有贪心及 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 分别记录以下信息:

  1. diz \text{diz} diz:是否被距离为 0 0 0 的点覆盖:消防站在 u u u 上。
  2. dio \text{dio} dio:被距离为 1 1 1 的点覆盖:消防站在 v v v u u u 的儿子上。
  3. 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 点染色的情况。

  1. diz[ u,v,w ] \text{diz[\textit{u,v,w}]} diz[u,v,w] 中有一个值为 true \textbf{true} true:意味着 u u u 点已经被覆盖到,没必要对 w w w 染色。
  2. dio[ u,v ] \text{dio[\textit{u,v}]} dio[u,v] 中有一个值为 true \textbf{true} true:意味着 u u u 点已经被覆盖到,没必要对 w w w 染色。
  3. 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](1in,2j2) 表示所有 i i i 的子孙与祖先 u u u 中满足 dep [ u ] ≥ d − j \text{dep}[u]\ge d-j dep[u]dj 的点都能被覆盖到。

注意到对于 − 2 ≤ j ≤ k ≤ 2 -2\le j\le k\le 2 2jk2,覆盖 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+uson(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]=mindp[i][2],mindp[u][2]+vson(i)v=udp[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]=mindp[i][1],dp[i][2],mindp[u][1]+vson(i)v=udp[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][02],uson(i)dp[u][0])dp[i][2]=min(dp[i][12],uson(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 22 变为 0 ∼ 4 0\sim4 04,防止 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


就这些吧。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值