题意:
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]));
}
}