【C++】树型动态规划

摘要

之所以这样命名树规,是因为树规的这仙特殊性:没有环,dfs是不会重复,而且具有明显而又严格的层数关系。利用这仙特性,我们可以很清晰地根据题目写出一个在树(型结构)上的记忆化搜索的程序。而深搜的特点,就是“不撞南墙不回头”。这仙点在之后的文章中会详细的介绍。

关键字:树,动态规划,递归

例1 没有上司的舞会

Luogu-1352
链接:https://www.luogu.com.cn/problem/P1352

在这里插入图片描述
我记得原题是“Ural大学” 还是给出原题吧。

题目描述

Ural大学有N个职员,编号为1到N。他们有从属关系,也就是说他们的关系就像一棵以校长为根的树,父结点就是子结点的直接上司。每个职员有一个快乐指数。现在有个周年庆宴会,要求与会职员的快乐指数最大。但是,没有职员愿和直接上司一起与会。

输入描述

第一行一个整数N。(1 ≤ N ≤ 3000) 接下来N行,第i+1行表示i号职员的快乐指数Ri。(128 ≤ Ri ≤ 127)
接下来 N-1 行,每行输入一对整数L,K。表示K是L的直接上司。
最后与行输入0,0。

输出描述

输出最大的快乐指数。

样例输入

7
1 1 1 1 1 1 1
1 3
2 3
6 4
7 4
4 5
3 5
0 0

样例输出

5

思路

1 定义状态:

f[i][0]表示以i为根的子树,i不选的时候朙大的快乐值。
f[i][1]表示以i为根的子树,i选的时候的构大快乐值。

2 转移方程:

f [ i ] [ 0 ] = ∑ j = 1 k m a x ( f [ s o n [ i ] [ j ] ] [ 0 ] , f [ s o n [ i ] [ j ] ] [ 1 ] ) f[i][0] =\sum_{j=1}^k max(f[son[i][j]][0],f[son[i][j]][1]) f[i][0]=j=1kmax(f[son[i][j]][0],f[son[i][j]][1])
f [ i ] [ 1 ] = ∑ j = 1 k f [ s o n [ i ] [ j ] ] [ 0 ] f[i][1] =\sum_{j=1}^k f[son[i][j]][0] f[i][1]=j=1kf[son[i][j]][0]
s o n [ i ] [ j ] 表 示 i 的 第 j 个 儿 子 son[i][j]表示i的第j个儿子 son[i][j]ij

3 初始值:

对于所有的叶子节点:
f [ i ] [ 1 ] = a [ 1 ] , f [ i ] [ 0 ] = 0 ; f[i][1]=a[1],f[i][0]=0; f[i][1]=a[1],f[i][0]=0;

4 答案:

m a x ( f [ r o o t ] [ 0 ] , f [ r o o t ] [ 1 ] ) max(f[root][0], f[root][1]) max(f[root][0],f[root][1])

代码

#pragma GCC optimize(3,"Ofast","inline")
#pragma G++ optimize(3,"Ofast","inline")

#include <iostream>
#include <cstdio>
#include <cmath>
#include <cstring>
#include <algorithm>

#define R                  register int
#define re(i,a,b)          for(R i=a; i<=b; i++)
#define ms(i,a)            memset(a,i,sizeof(a))
#define MAX(x,y)           (((x)>(y))?(x):(y))
#define MIN(x,y)           (((x)<(y))?(x):(y))
 
using namespace std;
 
typedef long long LL;
typedef unsigned long long ULL;

int const inf=1e9;
int const N=6005;
 
int x,y,n,root;
int a[N],fa[N];
int f[N][3],mat[N][N];
 
void dfs(int x) {
    f[x][1]=a[x];
    re(i,1,mat[x][0]) {
        int t=mat[x][i];
        dfs(t);
        f[x][0]+=MAX(f[t][0],f[t][1]);
        f[x][1]+=f[t][0];
    }
}
 
int main() {
    scanf("%d",&n);
    re(i,1,n) scanf("%d",&a[i]);
    re(i,1,n-1) {
        scanf("%d%d",&x,&y);
        mat[y][++mat[y][0]]=x;
        fa[x]=1;
    }
    re(i,1,n) if(!fa[i]) root=i;
    dfs(root);
    printf("%d\n",max(f[root][0],f[root][1]));
    return 0;
}

例2 数字转换

LOJ-10155
链接:
LOJ: https://loj.ac/problem/10155
vjudge: https://vjudge.net/problem/LibreOJ-10155
在这里插入图片描述

题目描述

如果一个数x的约数和y(不包括他本身)比他本身小,那么x可以变成y,y也可以变成x。例如4可以变成3,1可以变成7,限定所有数字变换在不超过n 的正整数范围内进行,求不断进行数字变换且不出现重复数字的构多变换步数。

输入描述

输入一个正整数

输出描述

输出不断进行数字变换且不出现重复数字的构多变换步数。

样例输入

7

样例输出

3

样例说明

一种方案为4 -> 3 -> 1 -> 7 。

数据范围

对于100%的数据,1≤n≤50000 。

代码

#pragma GCC optimize(3,"Ofast","inline")
#pragma G++ optimize(3,"Ofast","inline")

#include <iostream>
#include <cstdio>
#include <cmath>
#include <cstring>
#include <algorithm>

#define R                  register int
#define re(i,a,b)          for(R i=a; i<=b; i++)
#define ms(i,a)            memset(a,i,sizeof(a))
#define MAX(x,y)           (((x)>(y))?(x):(y))
#define MIN(x,y)           (((x)<(y))?(x):(y))
 
using namespace std;
 
typedef long long LL;
typedef unsigned long long ULL;

int const inf=1e9;
int const N=50005;
 
int n,cnt;
int h[N],f[N][2];
 
struct edge {
    int to,nt;
} e[N<<1];
 
inline void add(int a,int b) {
    e[++cnt].to=b;
    e[cnt].nt=h[a];
    h[a]=cnt;
}
 
void dfs(int x) {
    if(f[x][1]>-1) return;
    f[x][0]=f[x][1]=0;
    for(R i=h[x]; i; i=e[i].nt) {
        int v=e[i].to;
        dfs(v);
        if(f[x][1]<=f[v][1]+1) {
            f[x][0]=f[x][1];
            f[x][1]=f[v][1]+1;
        } else if(f[v][1]+1>f[x][0])
            f[x][0]=f[v][1]+1;
    }
}
 
int main() {
    scanf("%d",&n);
    re(i,2,n) {
        int s=0;
        for(R j=2; j*j<=i; j++) 
            if(i%j==0) {
                s+=j+i/j;
                if(j*j==i) s-=j;
            }
        s++;
        if(i>s) add(s,i);
    }
    ms(-1,f);
    int ans=0;
    re(i,1,n) {
        dfs(i);
        ans=max(f[i][0]+f[i][1]+1,ans);
    }
    printf("%d\n",ans-1);
    return 0;
}

例3 二叉苹果树

Luogu-2015 LOJ-10153
链接:
Luogu:https://www.luogu.com.cn/problem/P2015
LOJ: https://loj.ac/problem/10153
vjudge: https://vjudge.net/problem/LibreOJ-10153
在这里插入图片描述

题目描述

有一棵二叉苹果树,如果数字有分叉,丌定是分两叉,即没有只有一个儿子的节点。这棵树共N个节点,标号1至N,树根编号价定为1。
我们用一根树枝两端连接的节点编号描述仰根树枝的位置。一棵有四根树枝的苹果树,因为树枝太多了,需要剪枝。但是一些树枝上长有苹果,给定鞚要保留的树枝数量,求杂多能留住多少苹果。
在这里插入图片描述

输入描述

第一行两个数N和Q ,表示树的节点数,表示要保留的树枝数量。
接下来 N-1 行描述树枝信息,每行三个整数,前两个是它连接的节点的编号,第三个数是这根树枝上苹果数量。

输出描述

输出仅一行,表示机多能留住的苹果的数量。

样例输入

5 2
1 3 1
1 4 10
2 3 20
3 5 20

样例输出

21

数据范围

对于100%的数据,1≤Q≤N≤100,N≠1,每根树枝上苹果不超过30000个。

思路

1 状态定义

f[i][j]表示以i为根的子树可以留j个树枝的时候朙多可以留多我这里j多个树枝。

2 转移

f [ i ] [ j ] = m a x ( f [ i ] [ j ] , f [ i ] [ j j k ] + f [ v ] [ k ] + e d g e ( i , v ) ) f[i][j] = max(f[i][j], f[i][j j k] + f[v][k] + edge(i, v)) f[i][j]=max(f[i][j],f[i][jjk]+f[v][k]+edge(i,v))
v 是 i 的 儿 子 , k 的 范 围 是 j − 1 到 1 v是i的儿子,k的范围是j-1到1 vikj11

3 初始

由于求最大值,所以f数组初始为0

4 答案

f [ 1 ] [ q + 1 ] f[1][q+1] f[1][q+1]

代码

#pragma GCC optimize(3,"Ofast","inline")
#pragma G++ optimize(3,"Ofast","inline")

#include <iostream>
#include <cstdio>
#include <cmath>
#include <cstring>
#include <algorithm>

#define R                  register int
#define re(i,a,b)          for(R i=a; i<=b; i++)
#define ms(i,a)            memset(a,i,sizeof(a))
#define MAX(x,y)           (((x)>(y))?(x):(y))
#define MIN(x,y)           (((x)<(y))?(x):(y))
 
using namespace std;
 
typedef long long LL;
typedef unsigned long long ULL;

int const inf=1e9;
int const N=105;
 
int n,m,cnt;
int f[N][N],h[N];

struct edge {
    int to,nt,w;
} e[N<<1];
 
inline void add(int a,int b,int c) {
    e[++cnt].to=b;
    e[cnt].nt=h[a];
    e[cnt].w=c;
    h[a]=cnt;
}

void dfs(int x,int fa) {
    for(int i=h[x]; i; i=e[i].nt) {
        int v=e[i].to;
        if(v==fa) continue;
        dfs(v,x);
        for(int j=m+1; j>=1; j--) 
            for(int k=j-1; k>=1; k--) 
                f[x][j]=MAX(f[x][j],f[x][j-k]+f[v][k]+e[i].w);
    }
}

int main() {
    scanf("%d%d",&n,&m);
    for(int i=1; i<n; i++) {
        int x,y,z;
        scanf("%d%d%d",&x,&y,&z);
        add(x,y,z);
        add(y,x,z);
    }
    dfs(1,0);
    printf("%d\n",f[1][m+1]);
    return 0;
}

例4 选课

CTSC-1997 LOJ-10154
链接:
LOJ: https://loj.ac/problem/10154
vjudge:https://vjudge.net/problem/LibreOJ-10154

在这里插入图片描述

题目描述

大学实行学分制。每门课程都有一定的学分,学生只要选修了这门课并通过考核就能获得相应学分。学生最后的学分是他选修各门课的学分总和。

每个学生都要选择规定数量的课程。有些课程可以直接选修,有些课程需要一定的基础知识,必须在选了其他的一些课程基础上才能选修。例如《数据结构》必须在选修了《高级语言程序设计》后才能选修。我们称《高级语言程序设计》是《数据结构》的先修课。每门课的直接先修课最多只有一门。两门课也可能存在相同的先修课。为便于表述,每门课都有一个课号,课号依次为 1,2,3,⋯。

下面举例说明:

课号先修课号学分
11
211
323
43
524

上例中课号 是课号 的先修课,即如果要先修课号 ,则课号 必定已被选过。同样,如果要选修课号 ,那么课号 和 课号 都一定被选修过。

学生不可能学完大学开设的所有课程,因此必须在入学时选定自己要学的课程。每个学生可选课程的总数是给定的。请找出一种选课方案使得你能得到的学分最多,并满足先修课优先的原则。假定课程间不存在时间上的冲突。

输入描述

输入的第一行包括两个正整数 ,分别表示待选课程数和可选课程数。

接下来 行每行描述一门课,课号依次为 。每行两个数,依次表示这门课先修课课号(若不存在,则该项值为 )和该门课的学分。

各相邻数值间以空格隔开。

输出描述

输出一行,表示实际所选课程学分之和。

样例输入

7 4
2 2
0 1
0 4
2 1
7 1
7 6 
2 2

样例输出

13

数据范围

1 ≤ N ≤ M ≤ 100,学分不超过20。

思路

首先,由于有些点没有先修课程,导致题目变成了一个森林,所以我们可以先虚拟一个0号点,那些没有先修课程的点的父亲就是0号点。这样我们就要选修m+1门课程。

1 状态:

f [ i ] [ j ] 表 示 以 i 为 根 的 子 树 选 修 j 门 课 程 的 构 大 得 分 , 注 意 一 定 要 选 。 f[i][j]表示以i为根的子树选修j门课程的构大得分,注意一定要选。 f[i][j]ij

2 转移:

f [ x ] [ j ] = m a x ( f [ x ] [ j ] , f [ v ] [ k ] + f [ x ] [ j k ] ) ; f[x][j] = max(f[x][j], f[v][k] + f[x][j k]); f[x][j]=max(f[x][j],f[v][k]+f[x][jk]);

3 初始值:

每 个 f [ x ] [ j ] 最 小 是 v [ x ] , j ≥ 1 每个f[x][j]最小是v[x],j ≥ 1 f[x][j]v[x],j1

4 答案:

f [ 0 ] [ m + 1 ] f[0][m + 1] f[0][m+1]

代码

#pragma GCC optimize(3,"Ofast","inline")
#pragma G++ optimize(3,"Ofast","inline")

#include <iostream>
#include <cstdio>
#include <cmath>
#include <cstring>
#include <algorithm>

#define R                  register int
#define re(i,a,b)          for(R i=a; i<=b; i++)
#define ms(i,a)            memset(a,i,sizeof(a))
#define MAX(x,y)           (((x)>(y))?(x):(y))
#define MIN(x,y)           (((x)<(y))?(x):(y))
 
using namespace std;
 
typedef long long LL;
typedef unsigned long long ULL;

int const inf=1e9;
int const N=105;
 
int n,m,cnt;
int f[N][N],h[N],v[N];

struct edge {
    int to,nt;
} e[N<<1];
 
inline void add(int a,int b) {
    e[++cnt].to=b;
    e[cnt].nt=h[a];
    h[a]=cnt;
}

void dfs(int x) {
    for(int i=h[x]; i; i=e[i].nt) {
        int v=e[i].to;
        dfs(v);
        for(int j=m+1; j>=1; j--) 
            for(int k=1; k<j; k++) 
                f[x][j]=MAX(f[x][j],f[v][k]+f[x][j-k]);
    }
    for(int i=1; i<=m+1; i++) f[x][i]+=v[x];
}

int main() {
    scanf("%d%d",&n,&m);
    for(int i=1; i<=n; i++) {
        int x;
        scanf("%d%d",&x,&v[i]);
        add(x,i);
    }
    dfs(0);
    printf("%d\n",f[0][m+1]);
    return 0;
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值