[HNOI2003] 消防局的设立

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

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

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

  1. diz\text{diz}diz:是否被距离为 000 的点覆盖:消防站在 uuu 上。
  2. dio\text{dio}dio:被距离为 111 的点覆盖:消防站在 vvvuuu 的儿子上。
  3. dit\text{dit}dit:被距离为 222 的点覆盖:消防站在 wwwuuu 的兄弟(vvv 的儿子或 www 的孙子)上。

每次遍历时只要寻找一下 u,v,wu,v,wu,v,w 对应的 bool 值是否为 false\textbf{false}false 即可。

以下是不在 www 点染色的情况。

  1. diz[u,v,w]\text{diz[\textit{u,v,w}]}diz[u,v,w] 中有一个值为 true\textbf{true}true:意味着 uuu 点已经被覆盖到,没必要对 www 染色。
  2. dio[u,v]\text{dio[\textit{u,v}]}dio[u,v] 中有一个值为 true\textbf{true}true:意味着 uuu 点已经被覆盖到,没必要对 www 染色。
  3. 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,4dit[1] = true\text{dit[1] = \textbf{true}}dit[1] = true

接下来依次扫描点 5,6,45,6,45,6,4,发现都不符合要求,不进行染色操作。

当扫描到点 222 时,我们发现它的父亲 111dio\text{dio}dio 值为 false\textbf{false}false。(之前标记的是 111dit\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](1in,2j2) 表示所有 iii 的子孙与祖先 uuu 中满足 dep[u]≥d−j\text{dep}[u]\ge d-jdep[u]dj 的点都能被覆盖到。

注意到对于 −2≤j≤k≤2-2\le j\le k\le 22jk2,覆盖 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-22 层,即

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+uson(i)dp[u][2]

要求 dp[i][1]dp[i][1]dp[i][1],我们发现有两种情况:

  • 若覆盖恰好 111 层,则 iii 的儿子中必有一个点 uuu 经过了染色,其他儿子 vvv 都覆盖了至少 −1-11 层。
  • 若覆盖 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]=mindp[i][2],mindp[u][2]+vson(i)v=udp[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]=mindp[i][1],dp[i][2],mindp[u][1]+vson(i)v=udp[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][02],uson(i)dp[u][0])dp[i][2]=min(dp[i][12],uson(i)dp[u][1])

我们得到了上述 5 个转移方程后,即可开始转移。

最后的答案是覆盖节点 111000 层所需的最小染色点数,即 dp[1][0]dp[1][0]dp[1][0]

为了方便,我们在开数组的时候,将第二维统一加上 222,将 −2∼2-2\sim222 变为 0∼40\sim404,防止 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


就这些吧。

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值