刷题记录【树形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个用户的最小总收益
状态转移方程f[i][j]=max(f[i][j−k]+f[to[v]][k]−dis[v])f[i][j]=max(f[i][j-k]+f[to[v]][k]-dis[v])f[i][j]=max(f[i][jk]+f[to[v]][k]dis[v])其中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的乡村的三个参数是aia_iai​,bib_ibi​和cic_ici​。假设 从编号为iii的乡村走到首都一共需要经过x条未翻修的公路与y条未翻修的铁路,那么该乡村 的不便利值为 ci⋅(ai+x)⋅(bi+y)ci⋅(ai+x)⋅(bi+y)ci​⋅(ai​+x)⋅(bi​+y)ci⋅(ai+x)⋅(bi+y)c_i \cdot (a_i + x) \cdot (b_i + y)ci​⋅(ai​+x)⋅(bi​+y)ci(ai+x)(bi+y)ci(ai+x)(bi+y)ci(ai+x)(bi+y)在给定的翻修方案下,每个乡村的不便利值相加的和为该翻修方案的不便利值。 翻修n−1n - 1n−1条道路有很多方案,其中不便利值最小的方案称为最优翻修方案,小 W 自然 希望找到最优翻修方案,请你帮助他求出这个最优翻修方案的不便利值。
输入输出格式
输入格式:
第一行为正整数n。 接下来n−1行,每行描述一个城市。其中第iii行包含两个数si,tis_i,t_isi,ti​。sis_isi​表示通向第iii座城市 的公路的起点,tit_iti​表示通向第i座城市的铁路的起点。如果si&gt;0s_i &gt; 0si>0,那么存在一条从第sis_isi​座城 市通往第iii座城市的公路,否则存在一条从第sis_isi个乡村通往第i座城市的公路;tit_iti​类似地,如 果ti&gt;0t_i &gt; 0ti>0,那么存在一条从第tit_iti​座城市通往第i座城市的铁路,否则存在一条从第tit_iti​个乡村通 往第i座城市的铁路。 接下来n行,每行描述一个乡村。其中第i行包含三个数ai,bi,cia_i,b_i,c_iai,bi,ci,其意义如题面所示。
输出格式:
输出一行一个整数,表示最优翻修方案的不便利值。

一共20组数据,编号为1 ∼ 20。 对于编号≤4≤44的数据,n≤20;n≤20;n20
对于编号为5 ∼ 8的数据,ai,bi,ci≤5,ai,bi,ci≤n≤50;ai,bi,ci≤5,a_i,b_i,c_i \le n≤50;ai,bi,ci5,ai,bi,cin50
对于编号为9 ∼ 12的数据,n≤2000;n≤2000;n2000
对于所有的数据,n≤20000,ai,bi≤60,ci≤109,si,ti​n≤20000, a_i,b_i \le 60,c_i \le 10^9,s_i,t_i​n20000,ai,bi60ci109,si,ti​是$ [−n,−1]∪(i,n−1]$内的整数,任意乡村可以通过不超过40条道路到达首都

这道题的关键点:
1.树为二叉树
2.二叉树的左儿子与右儿子必须选择一个
3.二叉树的深度小于40
由前两个关键点我们可以用左儿子表示铁路,右儿子表示公路
因此我们定义状态f[i][j][k]表示在从首都到节点i的路上有j条未修复铁路,k条未修复公路的状态
由于i的左右儿子必须选择一个,
状态转移方程为f[i][j][k]=min(f[leftson[i]][j+1][k]+f[rightson[i]][j][k+1],f[rightson[i]][j][k+1]+f[leftson[i]][j][k])f[i][j][k]=min(f[leftson[i]][j+1][k]+f[rightson[i]][j][k+1],f[rightson[i]][j][k+1]+f[leftson[i]][j][k])f[i][j][k]=min(f[leftson[i]][j+1][k]+f[rightson[i]][j][k+1],f[rightson[i]][j][k+1]+f[leftson[i]][j][k])最终答案为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不在消防局范围内,但所有孙子均被覆盖
现在讨论状态转移:
f[x][0]=∑min(f[i][0,1,2,3,4])+1f[x][0]=\sum min(f[i][0,1,2,3,4])+1f[x][0]=min(f[i][0,1,2,3,4])+1
f[x][1]=min(∑i!=kmin(f[i][0,1,2,3])+f[k][0])f[x][1]=min(\sum_{i!=k} min(f[i][0,1,2,3])+f[k][0])f[x][1]=min(i!=kmin(f[i][0,1,2,3])+f[k][0])
f[x][2]=min(∑i!=kmin(f[i][0,1,2])+f[k][1])f[x][2]=min(\sum_{i!=k} min(f[i][0,1,2])+f[k][1])f[x][2]=min(i!=kmin(f[i][0,1,2])+f[k][1])
f[x][3]=∑min(f[i][0,1,2])f[x][3]=\sum min(f[i][0,1,2])f[x][3]=min(f[i][0,1,2])
f[x][3]=∑min(f[i][0,1,2,3])f[x][3]=\sum min(f[i][0,1,2,3])f[x][3]=min(f[i][0,1,2,3])
据说可以化简,然而我比较懒

#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]));
}

持续更新中…

转载于:https://www.cnblogs.com/akonoh/p/10216764.html

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
树形动态规划(Tree DP)是一种常用的动态规划算法,用于解决树结构相关的问题。在Python中,可以使用递归或者迭代的方式实现树形DP树形DP的基本思想是,从树的叶子节点开始,逐层向上计算每个节点的状态,并利用已经计算过的节点状态来更新当前节点的状态。这样可以通过自底向上的方式,逐步计算出整个树的最优解。 下面是一个简单的示例,演示如何使用树形DP解决一个二叉树中节点权值之和的最大值问题: ```python class TreeNode: def __init__(self, val=0, left=None, right=None): self.val = val self.left = left self.right = right def max_sum(root): if root is None: return 0 # 递归计算左右子树的最大权值和 left_sum = max_sum(root.left) right_sum = max_sum(root.right) # 当前节点的最大权值和为当前节点值加上左右子树中较大的权值和 return root.val + max(left_sum, right_sum) # 构建一个二叉树 root = TreeNode(1) root.left = TreeNode(2) root.right = TreeNode(3) root.left.left = TreeNode(4) root.left.right = TreeNode(5) # 计算二叉树中节点权值之和的最大值 result = max_sum(root) print(result) ``` 这段代码中,我们定义了一个`TreeNode`类来表示二叉树的节点,其中`val`表示节点的权值,`left`和`right`分别表示左子节点和右子节点。`max_sum`函数使用递归的方式计算二叉树中节点权值之和的最大值,通过比较左右子树的最大权值和来确定当前节点的最大权值和。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值