题目描述
2020年,人类在火星上建立了一个庞大的基地群,总共有n个基地。起初为了节约材料,人类只修建了n-1条道路来连接这些基地,并且每两个基地都能够通过道路到达,所以所有的基地形成了一个巨大的树状结构。如果基地A到基地B至少要经过d条道路的话,我们称基地A到基地B的距离为d。
由于火星上非常干燥,经常引发火灾,人类决定在火星上修建若干个消防局。消防局只能修建在基地里,每个消防局有能力扑灭与它距离不超过2的基地的火灾。
你的任务是计算至少要修建多少个消防局才能够确保火星上所有的基地在发生火灾时,消防队有能力及时扑灭火灾。
输入输出格式
输入格式:输入文件的第一行为n (n<=1000),表示火星上基地的数目。接下来的n-1行每行有一个正整数,其中文件第i行的正整数为a[i],表示从编号为i的基地到编号为a[i]的基地之间有一条道路,为了更加简洁的描述树状结构的基地群,有a[i]<i。
输出文件仅有一个正整数,表示至少要设立多少个消防局才有能力及时扑灭任何基地发生的火灾。
输入输出样例
6 1 2 3 4 5
2
解题报告
解法一(贪心)
很容易发现每次想覆盖一个深度最大的未被覆盖的节点,最好的做法都是在它的爷爷节点建立消防局。具体做法为:先遍历求出每个节点的深度及父亲,按深度排序放入优先队列中,每次从队列中取出top(即深度最大的节点),若还未被覆盖,就在该节点的爷爷节点建立消防局(从爷爷节点开始遍历,覆盖爷爷节点及其上下各两层)并且++Ans。
解法二(动态规划)
首先考虑节点的状态,我们用DP[i][State]表示节点i在状态State时其子树的最优解。
State=0:节点i为消防局 (i覆盖)
State=1:节点i的至少一个儿子为消防局 (i覆盖)
State=2:节点i的至少一个孙子为消防局 (i覆盖)
State=3:节点i的所有儿子孙子都被覆盖 (i不一定)
State=4:节点i的所有孙子都被覆盖 (i不一定)
现在考虑转移(建议画一个三层的完全二叉树)(j、k为i的儿子节点,文字说明中的0...X指的是对于i的儿子节点)
DP[i][0]=Σmin{DP[j][0...4]}+1;
由于i为消防局,所以i的所有儿子孙子一定被覆盖,所以0...4状态都可以,直接取最小值求和。
DP[i][1]=min{DP[j][0]+Σ(k!=j)DP[k][0...3]};
当i的一个儿子为消防局,i和i的其它儿子一定被覆盖,现在要满足其它儿子的儿子也被覆盖,所以只能0...3。
DP[i][2]=min{DP[j][1]+Σ(k!=j)DP[k][0...2]};
当i的一个孙子为消防局,那么只能覆盖到i,要满足i的其它儿子及孙子都要被覆盖,所以只能0...2。
DP[i][3]=Σmin{DP[j][0...2]};
i的所有儿子和孙子都被覆盖,0...2满足。
DP[i][4]=Σmin{DP[j][0...3]};
i的所有孙子都被覆盖,即所有儿子的儿子被覆盖,0...3满足。
转移就这样好了,但是会发现每次都要从状态0...X中找最小值,非常耗时,变换一下动规数组的含义,就可以实现优化。
Opt[i][X]表示节点i在状态State=0...X下的子树最优解的最小值(即min{DP[i][0...X]}),转移方程就要变为:
DP[i][0]=ΣOpt[j][4]+1;
直接又原方程转化可得。
DP[i][1]=Dp[i][4]+min{Opt[j][0]-Opt[j][3]};
选一个儿子的话,那么除了这个儿子的儿子以外的所有孙子都要已被覆盖,那么相当于一开始所有孙子都被覆盖(即DP[i][4]),然后选一个儿子将它的状态由儿子全被覆盖(状态0...3)变为它自己为消防局(状态0),找一个差值最小的加上。
DP[i][2]=DP[i][3]+min{Opt[j][1]-Opt[j][2]};
基本同上。
Dp[i][0]=ΣOpt[j][2];
直接由原方程转化可得。
DP[i][0]=ΣOpt[j][3];
直接由原方程转化可得。
方程的话就是这样,在实现的时候,由于a[i]<i,那么节点1一定是根节点,那么直接倒推就行了。实现的时候并不要用DP数组,只要Opt就行了,注意要先算状态0,3,4,因为在转移1,2时要用到3,4,最后再取min。
源代码
贪心
#include<bits/stdc++.h>
using namespace std;
struct Node{
int Loc;
int Next;
}E[2001];
struct Hrz{
int Depth;
int Num;
bool operator < (Hrz x)const{ //优先队列按深度降序排列
return x.Depth>Depth;
}
}A[1001];
priority_queue<Hrz>Q;
int x,n,i,j,Cnt,Head[1001],Ans,Cover[1001],Fa[1001];
inline void Adde(int x,int y){
E[++Cnt].Loc=y;
E[Cnt].Next=Head[x];
Head[x]=Cnt;
}
inline void Dfs(int x,int f,int Dep){ //预处理深度和父亲
Fa[x]=f;
A[x].Depth=Dep;
A[x].Num=x;
Q.push(A[x]);
for(int i=Head[x];i;i=E[i].Next){
int y=E[i].Loc;
if(y==f)continue;
Dfs(y,x,Dep+1);
}
}
inline void CC(int x,int f,int C){ //覆盖距离不大于2的节点
if(C<0)return;
Cover[x]=1;
for(int i=Head[x];i;i=E[i].Next){
int y=E[i].Loc;
if(y==f)continue;
CC(y,x,C-1);
}
}
int main(){
scanf("%d",&n);
for(i=2;i<=n;++i){
scanf("%d",&x);
Adde(x,i);
Adde(i,x);
}
Dfs(1,1,1);
while(!Q.empty()){ //贪心,每次取未覆盖的深度最大的节点的爷爷
Hrz u=Q.top();
Q.pop();
if(Cover[u.Num])continue;
int XX=Fa[Fa[u.Num]];
CC(XX,XX,2);
Ans++;
}
printf("%d\n",Ans);
return 0;
}
动态规划
#include<bits/stdc++.h>
using namespace std;
struct Node{
int Loc;
int Next;
}E[2001]; //Opt[i][x]表示节点i在状态State=0-x下的子树最优解的最小值(用于优化)
int x,n,i,j,Opt[1001][5],Head[1001],Cnt; //State=0:节点i为消防局 (i覆盖)
//State=1:节点i的至少一个儿子为消防局 (i覆盖)
//State=2:节点i的至少一个孙子为消防局 (i覆盖)
//State=3:节点i的所有儿子孙子都被覆盖 (i不一定)
//State=4:节点i的所有孙子都被覆盖 (i不一定)
inline void Adde(int x,int y){
E[++Cnt].Loc=y;
E[Cnt].Next=Head[x];
Head[x]=Cnt;
}
int main(){
scanf("%d",&n);
for(i=2;i<=n;++i){
scanf("%d",&x);
Adde(x,i);
}
for(i=n;i>=1;--i){ //1为根节点,只能倒推
int M1=0x3f3f3f3f,M2=0x3f3f3f3f;
Opt[i][0]=1;
for(j=Head[i];j;j=E[j].Next){
int x=E[j].Loc;
Opt[i][3]+=Opt[x][2];
Opt[i][4]+=Opt[x][3];
Opt[i][0]+=Opt[x][4];
M1=min(M1,Opt[x][0]-Opt[x][3]);
M2=min(M2,Opt[x][1]-Opt[x][2]);
}
Opt[i][1]=Opt[i][4]+M1;
Opt[i][2]=Opt[i][3]+M2;
Opt[i][1]=min(Opt[i][1],Opt[i][0]);
Opt[i][2]=min(Opt[i][2],Opt[i][1]);
Opt[i][3]=min(Opt[i][3],Opt[i][2]);
Opt[i][4]=min(Opt[i][4],Opt[i][3]);
}
printf("%d\n",Opt[1][2]);
return 0;
}
解题心得
动态规划类的题目,最重要的是先对状态进行分类,之后再考虑状态的转移。