刷题记录【树形DP
完全不按顺序的复习
由于本人是菜鸡,高中时没有系统学习过dp,所以按照查漏补缺的原则,优先学习没有接触过的算法
本文将持续更新
动态规划本质上是对搜索过程的记忆化优化
由于dfs搜索的过程近似于树上结构,因此树上DP是常常考察的DP类型
下面是最近做的三道典型题
洛谷P1273有线电视网
链接https://www.luogu.org/problemnew/show/P1273
题目描述
某收费有线电视网计划转播一场重要的足球比赛。他们的转播网和用户终端构成一棵树状结构,这棵树的根结点位于足球比赛的现场,树叶为各个用户终端,其他中转站为该树的内部节点。
从转播站到转播站以及从转播站到所有用户终端的信号传输费用都是已知的,一场转播的总费用等于传输信号的费用总和。
现在每个用户都准备了一笔费用想观看这场精彩的足球比赛,有线电视网有权决定给哪些用户提供信号而不给哪些用户提供信号。
写一个程序找出一个方案使得有线电视网在不亏本的情况下使观看转播的用户尽可能多。
输入输出格式
输入格式:
输入文件的第一行包含两个用空格隔开的整数N和M,其中2≤N≤3000,1≤M≤N-1,N为整个有线电视网的结点总数,M为用户终端的数量。
第一个转播站即树的根结点编号为1,其他的转播站编号为2到N-M,用户终端编号为N-M+1到N。
接下来的N-M行每行表示—个转播站的数据,第i+1行表示第i个转播站的数据,其格式如下:
K A1 C1 A2 C2 … Ak Ck
K表示该转播站下接K个结点(转播站或用户),每个结点对应一对整数A与C,A表示结点编号,C表示从当前转播站传输信号到结点A的费用。最后一行依次表示所有用户为观看比赛而准备支付的钱数。
输出格式:
输出文件仅一行,包含一个整数,表示上述问题所要求的最大用户数。
输入输出样例
输入样例#1:
5 3
2 2 2 5 3
2 3 2 4 3
3 4 2
输出样例#1:
2
首先我们来定义状态
类似于普通DP的分组背包问题,我们把一个节点的所有儿子当作一个组,从组中枚举每一个数量
我们用f[i][j]表示节点i服务j个用户的最小总收益
状态转移方程其中v表示I的一条连边的编号
#include <iostream>
#include <cstdio>
#include <cstring>
#include <cstdlib>
#include <algorithm>
using namespace std;
#define N 3005
#define next nico
int f[N][N],val[N],n,m;
int head[N],next[N],to[N],dis[N],tot,buf[N];
void add(int a,int b,int c)
{
to[++tot]=b;
dis[tot]=c;
next[tot]=head[a];
head[a]=tot;
}
bool ad[N];
int dfs(int x)
{
if(val[x]!=0)
{
f[x][1]=val[x];
return 1;
}
int sum = 0;
for(int i = head[x];i;i=next[i])
{
int t = dfs(to[i]);
for(int j = sum ;j >=0 ;j--)
for(int k = t; k>=0 ; k --)
f[x][j+k]=max(f[x][j+k],f[x][j]+f[to[i]][k]-dis[i]);
sum+=t;
}
return sum;
}
int main()
{
scanf("%d%d",&n,&m);
for(int i = 1; i<= n-m; i ++)
{
int x,y,z;
scanf("%d",&x);
while(x--)
{
scanf("%d%d",&y,&z);
add(i,y,z);
}
}
for(int i = n-m+1; i <=n;i++)
{
scanf("%d",&val[i]);
}
memset(f,~0x3f,sizeof(f));
for(int i = 1; i <= n; i++)f[i][0]=0;
dfs(1);
for(int i = m; i >=0; i --)
{
if(f[1][i]>=0)
{
printf("%d",i);
return 0;
}
}
}
[HNOI/AHOI2018]道路
题目描述
W 国的交通呈一棵树的形状。W 国一共有n−1个城市和n个乡村,其中城市从1到n−1 编号,乡村从1到n编号,且1号城市是首都。道路都是单向的,本题中我们只考虑从乡村通往首都的道路网络。对于每一个城市,恰有一条公路和一条铁路通向这座城市。对于城市i, 通向该城市的道路(公路或铁路)的起点,要么是一个乡村,要么是一个编号比iii大的城市。 没有道路通向任何乡村。除了首都以外,从任何城市或乡村出发只有一条道路;首都没有往 外的道路。从任何乡村出发,沿着唯一往外的道路走,总可以到达首都。W 国的国王小 W 获得了一笔资金,他决定用这笔资金来改善交通。由于资金有限,小 W 只能翻修n−1条道路。小 W 决定对每个城市翻修恰好一条通向它的道路,即从公路和铁 路中选择一条并进行翻修。小 W 希望从乡村通向城市可以尽可能地便利,于是根据人口调 查的数据,小 W 对每个乡村制定了三个参数,编号为iii的乡村的三个参数是,和。假设 从编号为iii的乡村走到首都一共需要经过x条未翻修的公路与y条未翻修的铁路,那么该乡村 的不便利值为 在给定的翻修方案下,每个乡村的不便利值相加的和为该翻修方案的不便利值。 翻修n−1n - 1n−1条道路有很多方案,其中不便利值最小的方案称为最优翻修方案,小 W 自然 希望找到最优翻修方案,请你帮助他求出这个最优翻修方案的不便利值。
输入输出格式
输入格式:
第一行为正整数n。 接下来n−1行,每行描述一个城市。其中第iii行包含两个数。表示通向第iii座城市 的公路的起点,表示通向第i座城市的铁路的起点。如果,那么存在一条从第座城 市通往第iii座城市的公路,否则存在一条从第个乡村通往第i座城市的公路;类似地,如 果,那么存在一条从第座城市通往第i座城市的铁路,否则存在一条从第个乡村通 往第i座城市的铁路。 接下来n行,每行描述一个乡村。其中第i行包含三个数,其意义如题面所示。
输出格式:
输出一行一个整数,表示最优翻修方案的不便利值。
一共20组数据,编号为1 ∼ 20。 对于编号的数据,
对于编号为5 ∼ 8的数据,
对于编号为9 ∼ 12的数据,
对于所有的数据,是$ [−n,−1]∪(i,n−1]$内的整数,任意乡村可以通过不超过40条道路到达首都
这道题的关键点:
1.树为二叉树
2.二叉树的左儿子与右儿子必须选择一个
3.二叉树的深度小于40
由前两个关键点我们可以用左儿子表示铁路,右儿子表示公路
因此我们定义状态f[i][j][k]表示在从首都到节点i的路上有j条未修复铁路,k条未修复公路的状态
由于i的左右儿子必须选择一个,
状态转移方程为最终答案为f[1][0][0]
#include <iostream>
#include <algorithm>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#define lval a
#define rval b
#define N 50002
//left 铁路 right 公路
using namespace std;
struct nod
{
int l,r,a,b,c,lnum,rnum;
}node[N*2];
long long f[N][45][45];
int n;
int numl,numr;
void dfs(int i)
{
node[i].lnum=numl;
node[i].rnum=numr;
if(i>n)
{return;}
numl++;
dfs(node[i].l);
numl--;
numr++;
dfs(node[i].r);
numr--;
}
void dfs1(int p)
{
if(p>n)
{
for(int i = 0; i <= node[p].lnum;i++)
for(int j = 0; j <= node[p].rnum;j++)
{
f[p][i][j]=(long long)node[p].c*(node[p].a+i)*(node[p].b+j);
}
}
else
{
dfs1(node[p].l);dfs1(node[p].r);
for(register int i = 0; i <= node[p].lnum;i++)
for(register int j = 0; j <= node[p].rnum;j++)
{
f[p][i][j]=min(f[node[p].l][i][j]+f[node[p].r][i][j+1],f[node[p].l][i+1][j]+f[node[p].r][i][j]);
}
}
}
int main()
{
scanf("%d",&n);
for(int i = 1 ;i <= n-1 ; i++)
{
int x,y;
scanf("%d%d",&x,&y);
if(x<0)x=-x+n;
if(y<0)y=-y+n;
node[i].l=x;
node[i].r=y;
}
for(int i = 1 ; i <= n ; i ++)
{
int a,b,c;
scanf("%d%d%d",&a,&b,&c);
node[n+i].a=a;
node[n+i].b=b;
node[n+i].c=c;
}
dfs(1);
dfs1(1);
cout<<f[1][0][0];
}
[HNOI2003]消防局的设立
链接https://www.luogu.org/problemnew/show/P2279
题目描述
2020年,人类在火星上建立了一个庞大的基地群,总共有n个基地。起初为了节约材料,人类只修建了n-1条道路来连接这些基地,并且每两个基地都能够通过道路到达,所以所有的基地形成了一个巨大的树状结构。如果基地A到基地B至少要经过d条道路的话,我们称基地A到基地B的距离为d。
由于火星上非常干燥,经常引发火灾,人类决定在火星上修建若干个消防局。消防局只能修建在基地里,每个消防局有能力扑灭与它距离不超过2的基地的火灾。
你的任务是计算至少要修建多少个消防局才能够确保火星上所有的基地在发生火灾时,消防队有能力及时扑灭火灾。
输入输出格式
输入格式:
输入文件名为input.txt。
输入文件的第一行为n (n<=1000),表示火星上基地的数目。接下来的n-1行每行有一个正整数,其中文件第i行的正整数为a[i],表示从编号为i的基地到编号为a[i]的基地之间有一条道路,为了更加简洁的描述树状结构的基地群,有a[i]<i。
输出格式:
输出文件名为output.txt
输出文件仅有一个正整数,表示至少要设立多少个消防局才有能力及时扑灭任何基地发生的火灾
这是一类特殊的树形动态规划题:给定点的覆盖范围,求最少覆盖整颗树所需点数问题
本题有贪心做法,但是本文不讨论
定义状态:
f[x][v]表示f[x]在v状态下的最小点数
v=0:x处有消防局
v=1:x至少一个儿子处有消防局,且x的所有儿孙均被覆盖
v=2:x至少一个孙子处有消防局,且x的所有儿孙均被覆盖
v=3:x不在消防局范围内,但所有儿子均被消防局覆盖
v=4:x不在消防局范围内,但所有孙子均被覆盖
现在讨论状态转移:
据说可以化简,然而我比较懒
#include <iostream>
#include <cstdio>
#include <cstdlib>
#include <algorithm>
using namespace std;
#define N 1005
#define next nico
const int inf = 0x3f7f7f7f;
int f[N][5],head[N],next[N<<1],to[N<<1],fa[N],tot,n;
//1 unbuilt dis = 1; 2 unbuilt dis = 2; 3 unbuilt dis = 3; 4 unbuilt dis = 4;
void add(int x, int y )
{
to[++tot]=y;
next[tot]=head[x];
head[x]=tot;
}
void dfs(int x)
{
f[x][0]=1;
f[x][1]=f[x][2]=inf;
for(int i = head[x]; i ; i =next[i] )
if(to[i]!=fa[x])
{
fa[to[i]]=x;
dfs(to[i]);
f[x][0]+=min(f[to[i]][0],min(f[to[i]][1],min(f[to[i]][2],min(f[to[i]][3],f[to[i]][4]))));
}
int min1 = 0,k;
for(k=head[x];k;k=next[k])
{
min1 = 0;
for(int i = head[x]; i ; i =next[i] )
if(to[i]!=fa[x]&&i!=k)
{
min1+=min(f[to[i]][0],min(f[to[i]][1],min(f[to[i]][2],f[to[i]][3])));
}
f[x][1]=min(min1+f[to[k]][0],f[x][1]);
min1 = 0;
for(int i = head[x]; i ; i =next[i] )
if(to[i]!=fa[x]&&to[i]!=k)
{
min1+=min(f[to[i]][1],min(f[to[i]][2],f[to[i]][0]));
}
f[x][2]=min(f[x][2],min1+f[to[k]][1]);
}
for(int i = head[x]; i ; i =next[i] )
if(to[i]!=fa[x])
{
f[x][3]+=min(f[to[i]][2],min(f[to[i]][1],f[to[i]][0]));
}
for(int i = head[x]; i ; i =next[i] )
if(to[i]!=fa[x])
{
f[x][4]+=min(f[to[i]][0],min(f[to[i]][1],min(f[to[i]][2],f[to[i]][3])));
}
}
int main()
{
int x,y;
scanf("%d",&n);
for(int i = 2 ;i <= n ; i ++)
{
scanf("%d",&y);
add(i,y);add(y,i);
}
dfs(1);
cout<<min(f[1][0],min(f[1][1],f[1][2]));
}