(树形 dp)洛谷 P2279 HNOI2003 消防局的设立 题解

题意

给定一棵树,要求所有节点被“消防站”覆盖,“消防站”的覆盖范围是距离本身距离不大于 2 2 2 的所有节点。求设立消防站的最少数量。

节点数 1 ≤ n ≤ 1000 1\le n\le 1000 1n1000

思路

树形 dp。感觉这题无从下手的原因就是不知道怎么设状态。

我们知道,节点要么本身是消防站,要么被消防站覆盖,考虑联系节点 u u u 本身与覆盖自己的消防站 k k k

f u , k f_{u,k} fu,k 表示节点 u u u k k k 覆盖,且 u u u 子树内所有点已经完成了覆盖操作。

鉴于节点数 n n n 比较小,因此 Θ ( n ) \Theta (n) Θ(n) 枚举覆盖自己的消防站 k k k,做到 Θ ( n 2 ) \Theta (n^2) Θ(n2) 的复杂度也是可以过的。

预处理树上各节点距离 d i s dis dis。当 d i s ( u , k ) ≤ 2 dis(u,k)\le 2 dis(u,k)2 时,那么 k k k 可以成为覆盖 u u u 的消防站,初始化 f ( u , k ) = 1 f(u,k)=1 f(u,k)=1,再去研究 u u u 的子树来更新 f ( u , k ) f(u,k) f(u,k)

然后枚举 u u u 的子节点 v v v,如果 u u u v v v 都被 k k k 覆盖,且 v v v k k k 覆盖时最优(此时枚举的 dp 状态下, v v v 必然被 k k k 覆盖),那么可以取 f ( u , k ) ← f ( v , k ) − 1 f(u,k) \leftarrow f(v,k)-1 f(u,k)f(v,k)1,即把一开始初始化的 1 1 1 k k k u u u 而建的消防站拆掉。

如果 v v v k k k 覆盖时并非最优,那么怎么获取 v v v 的最优覆盖信息呢?不妨引入 n u m u num_u numu 表示,在 u u u 子树内节点全部成功覆盖、所需最少的消防站数量。更新 n u m u num_u numu 其实是简单的:
n u m u = min ⁡ k = 1 ,   d i s ( u , k ) ≤ 2 n f ( u , k ) num_u=\min_{k=1,\ dis(u,k)\le 2}^{n}f(u,k) numu=k=1, dis(u,k)2minnf(u,k)

那么最终 f ( u , k ) f(u,k) f(u,k) 的式子也可以写出来了:
f ( u , k ) = ∑ v ∈ s o n u min ⁡ ( n u m u , f ( v , k ) − 1 ) f(u,k)=\sum_{v\in son_u}\min(num_u,f(v,k)-1) f(u,k)=vsonumin(numu,f(v,k)1)

为什么这样就是对的? n u m u num_u numu 不会记下了 v v v k k k 覆盖时最优的情况的吗?因为 n u m u num_u numu 就算记下了 v v v k k k 覆盖时最优的情况,也不会比 min ⁡ \min min 的另一项 f ( v , k ) − 1 f(v,k)-1 f(v,k)1 更优,因而不会算重。

最终答案就是 n u m 1 num_1 num1

均摊下来 Θ ( n 2 ) \Theta (n^2) Θ(n2)

代码

#include<bits/stdc++.h>
using namespace std;
#define ll long long
const ll N=1003,inf=0x3f3f3f3f;
ll n;
ll idx,head[N];
struct edge
{
	ll to,next;
}e[N<<1]; 
void addedge(ll u,ll v)
{
	idx++;
	e[idx].to=v;
	e[idx].next=head[u];
	head[u]=idx;
}
ll dis[N][N],f[N][N],num[N];
//dis(u,v):树上u,v距离
//f(u,k):u及其子树已经被覆盖、u被i覆盖的的最少消防站个数 
//num(u):u子树内点集恰好覆盖完,最少消防站个数 
void dfs(ll u,ll fa,ll rt)
{
	for(int i=head[u];i;i=e[i].next)
	{
		ll v=e[i].to;
		if(v==fa)continue;
		dis[rt][v]=dis[v][rt]=dis[rt][u]+1;
		dfs(v,u,rt);
	}
}
void dp(ll u,ll fa)
{
	for(int i=head[u];i;i=e[i].next)
	{
		ll v=e[i].to;
		if(v==fa)continue;
		dp(v,u);
	}
	for(int k=1;k<=n;k++)
	{
		if(dis[u][k]>2)continue;
		f[u][k]=1;
		for(int i=head[u];i;i=e[i].next)
		{
			ll v=e[i].to;
			if(v==fa)continue;
			f[u][k]+=min(num[v],f[v][k]-1);
			//v已经被k覆盖过,-1 
			//若v最优消防站num(v)为k:f(v,k),并非最小不会计算
			//即会计算最优消防站不为k的情况 
		}
		num[u]=min(num[u],f[u][k]);
	}
}
int main()
{
	scanf("%lld",&n);
	for(int i=2;i<=n;i++)
	{
		ll v;
		scanf("%lld",&v);
		addedge(i,v);
		addedge(v,i);
	}
	for(int i=1;i<=n;i++)
	dfs(i,0,i);
	for(int i=1;i<=n;i++)
	{
		num[i]=inf;
		for(int j=1;j<=n;j++)
		f[i][j]=inf;
	}
	dp(1,0);
	printf("%lld",num[1]);
	return 0;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值