洛谷P2016-战略游戏【树状dp+建图】 难度:***

树状DP解决古城堡布兵问题
本文介绍了一种使用树状动态规划方法解决古城堡布兵问题的算法,详细解析了如何构建数据结构以高效访问每个节点的相邻点,并通过状态转移方程计算最少士兵数。

题意:

Bob喜欢玩电脑游戏,特别是战略游戏。但是他经常无法找到快速玩过游戏的办法。现在他有个问题。

他要建立一个古城堡,城堡中的路形成一棵树。他要在这棵树的结点上放置最少数目的士兵,使得这些士兵能了望到所有的路。

注意,某个士兵在一个结点上时,与该结点相连的所有边将都可以被了望到。

请你编一程序,给定一树,帮Bob计算出他需要放置最少的士兵.

输入格式
第一行 N,表示树中结点的数目。

第二行至第N+1行,每行描述每个结点信息,依次为:该结点标号i,k(后面有k条边与结点I相连)。

接下来k个数,分别是每条边的另一个结点标号r1,r2,…,rk。

对于一个n(0<n<=1500)个结点的树,结点标号在0到n-1之间,在输入数据中每条边只出现一次。

输出格式
输出文件仅包含一个数,为所求的最少的士兵数目。

输入输出样例
输入 #1 复制
4
0 1 1
1 2 2 3
2 0
3 0
输出 #1 复制
1

题解:

在刚拿到这道题的时候,虽然一眼能够看出来是dp,但是对树状dp不熟练的我
对建图很懵,看了很多大佬的题解,都是重在讲解如何dp,而直接跳过了建图的思路,因此我在这里想对建图的过程也作一个小小的补充。

题目分析

任务1:分析dp状态转移方程

给一张无向图,如果在某个结点上放士兵,则与之相连的线会被覆盖。问最少放置多少个士兵可以使途中所有的线都被覆盖。

我们设dp[i][0]表示在第i个结点不放士兵时,它的子树想要全部被覆盖最少需要几个士兵;dp[i][1]表示在第i个结点放置士兵时,它的子树想要全部被覆盖最少需要几个士兵。

我们知道每个线段都有两个端点,如果其中一个端点没有士兵,那么另一个端点一定要有士兵,因此如果在第i个结点不放置士兵,那么与之相连的结点都必须放置士兵。假设与i结点相连的点分别是s1、s2、…sk,那么我们得到:

dp[i][0]=dp[s1][1]+dp[s2][1]+…+dp[sk][1].

但是如果考虑i结点放置士兵的情况呢?很显然,与i相连的结点就“可放可不放”了,那么我们就可以取它们“放与不放”两种情况中,子树士兵数最少的情况。因为对于i这个结点,它的子树们之间不会相互影响,所以我们可以贪心地取每一个子树的士兵最小值,即

dp[i][1]=min(dp[s1][0],dp[s1][1])+min(dp[s2][0],dp[s2][1])+…+min(dp[sk][0],dp[sk][1]).

这样我们就完成了状态转移了。可是这道题不是线性dp啊,虽然方程推出来了,可是这个结构我该怎么建立呢?换句话说,我怎么建立结构,才能快速得到s1~sk这些点呢?

任务2:分析数据结构并建图

上文我们分析了dp的整个流程,只要建立好结构,能够合适地、快速地访问每个结点的相邻点(或边),那么这题就差不多出来了。

那么大家仔细想想,当我们访问到i结点时,s1到sk这些结点有什么共同之处呢?没错,共同之处就是:它们都与i结点之间有一条线。我们的重点就放在这些线上。

对于所有连接在同一个头结点的线,我们通过链表将它们链接起来。具体的连接方式是:

对于每一条线都存放在结构体中,结构中的变量分别为:lastline和nextpoint。顾名思义,lastline表示的是,与这根线连接了同一个头结点的上一根线(的下标)。我们访问完这一根线后,就可以通过lastline来访问到上一根同样与i结点相连的线。而nextpoint更显然了,就是这根线连接的那个点。

这样一来,我们就可以以O(k)的复杂来逐次访问与i结点所相连的边了。这时候你会想问,当我已经找到第一条连接的边后,后面的边都能很快找到,那么第一条边怎么找呢?很显然用一个数组flag[]来记录即可。比如与i连接的最后一条线下表是k,那么flag[i]=k即可。

任务3:整合上述内容

读取边->创建边并与头结点关联->找到存在的子结点作为dp起点->先递归到最深处,把子树的子结点的dp值确定下来->根据子结点的dp值来确定根结点的dp值->输出最根结点的dp值(取最小值)

代码:

#include<stdio.h>
struct node
{
	int nextpoint;      //连接的点 
	int lastline;       //同样连接该头结点的上一根边的下标 
}line[1505];
int cnt=0,vis[1505],dp[1505][2];
int flag[5505];         //记录每一个结点所对应的最后一条边的下标 
int min(int a,int b)
{
	return a<b?a:b;
}
void create(int a,int b)
{
	cnt++;                         //每多一条边,新边的下标就要加一 
	line[cnt].nextpoint=b;         //连接点 
	line[cnt].lastline=flag[a];    //指向头结点的最后一条边 
	flag[a]=cnt;                   //将头结点的最后一条边换成自己 
}
void dfsdp(int node)
{
	dp[node][1]=1;
	for(int i=flag[node];i>0;i=line[i].lastline)
	{
		dfsdp(line[i].nextpoint);
		dp[node][0]+=dp[line[i].nextpoint][1];
		dp[node][1]+=min(dp[line[i].nextpoint][1],dp[line[i].nextpoint][0]);
	}
}
int main()
{
	int n,c,d,m,begin=0;
	while(scanf("%d",&n)!=EOF)
	{
		begin=0,cnt=0;
		for(int i=0;i<1505;i++)
		{
			flag[i]=0;
			vis[i]=false;
			dp[i][0]=0;
			dp[i][1]=0;
		}
		for(int i=0;i<n;i++)
		{
			scanf("%d%d",&c,&m);
			while(m--)
			{
				scanf("%d",&d);
				create(c,d);
				vis[d]=true;
			}
		}
		while(vis[begin]==true)begin++;            //要找一个出现过的子结点作为起点 
		dfsdp(begin);
		printf("%d\n",min(dp[begin][0],dp[begin][1]));
	}
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值