树型动态规划(C++版)
- 1、下载文档前请自行甄别文档内容的完整性,平台不提供额外的编辑、内容补充、找答案等附加服务。
- 2、"仅部分预览"的文档,不可在线预览部分如存在完整性等问题,可反馈申请退款(可完整预览的文档不适用该条件!)。
- 3、如文档侵犯您的权益,请联系客服反馈,我们会尽快为您处理(人工客服工作时间:9:00-18:30)。
树型动态规划
补充二叉树的遍历的相关知识:
在二叉树的应用中,常常要求在树中查找具有某种特征的结点,或者对全部结点逐一进
行某种处理。
这就是二叉树的遍历问题。
所谓二叉树的遍历是指按一定的规律和次序访问树
中的各个结点,而且每个结点仅被访问一次。
“访问”的含义很广,可以是对结点作各种处
理,如输出结点的信息等。
遍历一般按照从左到右的顺序,共有3 种遍历方法,先(根)序遍历,中(根)序遍历,后(根)序遍历。
先序遍历的操作定义如下:
若二叉树为空,则空操作,否则
①访问根结点
②先序遍历左子树
③先序遍历右子树
先序遍历右图结果为:124753689
中序遍历的操作定义如下:
若二叉树为空,则空操作,否则
①中序遍历左子树
②访问根结点
③中序遍历右子树
中序遍历右图结果为:742513869
后序遍历的操作定义如下:
若二叉树为空,则空操作,否则
①后序遍历左子树
②后序遍历右子树
③访问根结点
后序遍历右图结果为:745289631
满二叉树:
一棵深度为h且有 2^h-1个结点的二叉树。
满二叉树一定为完全二叉树,但是完全二叉树不一定为满二叉树。
若设二叉树的深度为h,除第 h 层外,其它各层 (1~h-1) 的结点数都达到最大个数,第 h 层所有的结点都连续集中在最左边,这就是完全二叉树。
满二叉树有如下性质:
如果一颗树深度为h,最大层数为k,且深度与最大层数相同,即k=h;
1、它的叶子数是:2^(h-1)
2、第k层的结点数是:2^(k-1)
3、总结点数是:2^k-1 (2的k次方减一)
4、总节点数一定是奇数。
若设二叉树的深度为h,除第h 层外,其它各层(1~h-1) 的结点数都达到最大个数,第h 层所有的结点都连续集中在最左边,这就是完全二叉树。
1、二叉树的序遍历
题目描述Description
求一棵二叉树的前序遍历,中序遍历和后序遍历
输入描述Input Description
第一行一个整数n,表示这棵树的节点个数。
接下来n行每行2个整数L和R。
第i行的两个整数Li和Ri代表编号为i的节点的左儿子编号和右儿子编号。
输出描述Output Description
输出一共三行,分别为前序遍历,中序遍历和后序遍历。
编号之间用空格隔开。
样例输入Sample Input
5
2 3
4 5
0 0
0 0
0 0
样例输出Sample Output
1 2 4 5 3
4 2
5 1 3
4 5 2 3 1
#include<iostream>
#include<cstdio>
using namespace std;
struct node{
int l;
int r;
};
int i,n,r,l;
node tree[1000];
printf("%d ",x);
if (tree[x].l!=0) work1(tree[x].l);
if (tree[x].r!=0) work1(tree[x].r);
}
void work2(int x)
{
if (tree[x].l!=0) work2(tree[x].l);
printf("%d ",x);
if (tree[x].r!=0) work2(tree[x].r);
}
void work3(int x)
{
if (tree[x].l!=0) work3(tree[x].l);
if (tree[x].r!=0) work3(tree[x].r);
printf("%d ",x);
}
int main()
{
scanf("%d",&n);
for(i=1;i<=n;i++) scanf("%d%d",&tree[i].l,&tree[i].r);
work1(1);
printf("\n");
work2(1);
printf("\n");
work3(1);
printf("\n");
return 0;
}
2、二叉树最大宽度和高度(codevs1501)
题目描述Description
给出一个二叉树,输出它的最大宽度和高度。
输入描述Input Description
第一行一个整数n。
下面n行每行有两个数,对于第i行的两个数,代表编号为i的节点所连接的两个左右儿子的编号。
如果没有某个儿子为空,则为0。
输出描述Output Description
输出共一行,输出二叉树的最大宽度和高度,用一个空格隔开。
样例输入Sample Input
5
2 3
4 5
0 0
0 0
0 0
#include<iostream>
#include<cstdio>
#include<cstring>
using namespace std;
int a[1000][3],s[1000],i,n,x,ans;
void dfs(int i, int k)
{
int t,t1,t2;
t=i;
s[k]+=1;
if (ans<s[k]) ans=s[k];
if (k>x) x=k;
t1=a[t][1];
if (a[t][1]!=0) dfs(t1,k+1);
t2=a[t][2];
if (a[t][2]!=0) dfs(t2,k+1);
}
int main()
{
int n;
memset(a, 0, sizeof(a));
memset(s, 0, sizeof(s));
scanf("%d", &n);
for(int i = 1; i <= n; i++) scanf("%d%d", &a[i][1], &a[i][2]);
x=0;ans=0;
dfs(1, 1);
printf("%d %d\n", ans,x);
return 0;
}
3、愚蠢的矿工(RQNOJ30)
题目:背景
Stupid 家族得知在HYC家的后花园里的中央花坛处,向北走3步,向西走3步,再向北走3步,向东走3步,再向北走6步,向东走3步,向南走12步,再向西走2步( - -||)就能找到宝藏的入口,而且宝藏都是藏在山里的,必须挖出来,于是Stupid家族派狗狗带领矿工队去挖宝藏.(HYC家的宝藏被狗狗挖走后有什么感想?)
描述
这个宝藏的制造者为了掩盖世人耳目,他做的宝藏是没有出口,只有入口,不少建造宝藏的人都死在里面.现在知道宝藏总共有N个分岔口,在分岔口处是有财宝的,每个宝藏点都有一个财富值.狗狗只带了M个人来,而且为了安全起见,在每个分岔口,必须至少留一个人下来,这个留下来的人可以挖宝藏(每个人只能挖一个地方的宝藏),这样才能保证不会迷路,而且这个迷宫有个特点,任意两点间有且只有一条路可通(这一句是关键,由此我们可以判断用二叉树来做).狗狗为了让他的00开心,决定要尽可能地多挖些宝藏回去.现在狗狗的圈叉电脑不在身旁,只能求救于你了,你要知道,狗狗的终身幸福就在你手上了..(狗狗ps:00,你不能就这样抛弃偶……)
输入——第1行:两个正整数N,M .N表示宝藏点的个数,M表示狗狗带去的人数(那是一条懒狗,他自己可不做事)。
(n<=1000,m<=100)
第2行:N个整数,第i个整数表示第i个宝藏的财富值.(保证|wi|<maxint)
第N+2行:两个非负整数A和B,表示A通向B,当A=0,表示A是入口.(保证A,B<=n)
输出——输出狗狗能带回去的宝藏的价值。
样例:
输入1
4 3
5 6 2 4
1 2
0 1
2 3
3 4
输出1
13
输入2
7 4
4 7 1
5 10 12 8 3
0 1
1 2
1 3
1 4
2 5
2 6
4 7
输出2:
38
做法:这道题是一道树形dp,dp倒不是很难,只不过这里他的数据是一个多叉树,需要把多叉树变成二叉树,所以我悲剧地不会了,只好学习了......我先把多叉树转二叉树的示意图弄上来吧
注:树形动态规划。
对于多叉树,我们一般采用多叉树转化为二叉树来简化问题即左儿子右兄弟,这也是dfs过程中为什么必须进行f[x,k]=f[r[x],k]。
f[x,y]=max(f[ 所有x结点的子树,分配i-1个人]+当前节点的宝藏财富值+f[ 剩余所有x结点的兄弟,分配y-i个人]);即方将f[x,y]:=f[tree[x].r,y-i] +tree[x].k+f[tree[x].l,i-1];
参考程序:
#include<cstdio>
#define maxn 1010
using namespace std;
struct node
{
int l;
int r;
int k;
}tree[maxn];
int n,m,i,j,k,v[maxn],f[maxn][100];
void dp(int x,int y)
{
int i,tmp;
if (f[x][y]!=-1) return;
dp(tree[x].r,y);
f[x][y]=f[tree[x].r][y];
for(i=1;i<=y;i++)
{
dp(tree[x].r,y);
dp(tree[x].l,i-1);
f[x][y]=max(f[x][y],f[tree[x].r][y-i]+f[tree[x].l][i-1]+tree[x].k);
}
}
int main()
{
scanf("%d%d",&n,&m);
for(i=1;i<=n;i++)
{
tree[i].l=0;
tree[i].r=0;
}
for(i=1;i<=n;i++) scanf("%d",&tree[i].k);
for(i=1;i<=n;i++)
{
scanf("%d%d",&j,&k);
if(v[j]==0) tree[j].l=k;
else tree[v[j]].r=k;
v[j]=k;
}
for(i=1;i<=n;i++)
for(j=1;j<=m;j++)
f[i][j]=-1;
dp(tree[0].l,m);
printf("%d",f[tree[0].l][m]);
return 0;
5、购物问题(RQNOJ188)
题目描述
由于换季,商场推出优惠活动,以超低价格出售若干种商品。
但是商场为避免过分亏本,规定某些商品不能同时购买,而且每种超低价商品只能买一件。
身为顾客的你想获得最大的实惠,也就是争取节省最多的钱。
经过仔细研究,我们发现商场出售的超低价商品中,不存在以下这种情况:n(n>=3)种商品C1,C2,C3,……,Cn,其中Ci和Ci+1是不能同时购买的(i=1,2,……,n-1),而且C1和Cn也不能同时购买。
请编程计算可以节省的最大金额数。
输入格式
第一行两个整数K,M(1<=K<=1000),其中K表示超低价商品数,K种商品的编号依次为1,2,3,……,K;M表示不能同时购买的商品对数。
接下来K行,第i行有一个整数Xi表示购买编号为i的商品可以节省的金额(1<=Xi<=100)。
再接下来M行,每行两个数A,B,表示A和B不能同时购买,1<=A,B<=K,A≠B。
输出格式
仅一行一个整数,表示能节省的最大金额数。
样例输入
3 1
1
1
1
1 2
样例输出
2
分析:
经典的树型DP, 要注意本题给的不是一棵树,而是个森林,也就多个建图过程吧。
DP方程:
1)f[i,2]=Σ(max{f[j,1],f[j,2]})
2) f[i,1]=∑(f[j,2] +x[i];
(f[i,2]表示第i个物品不取,显然它的孩子可取可不取。
f[i,1]表示第i个物品取,显然只能从它孩子都不取的状态下得到并且要+x[i])
源码程序:
#include<cstdio>
#include<iostream>
using namespace std;
int g[1001][1001],t[1001][1001],f[1001][3],d[1001];
int a,b,k,m,ans;
bool h[1001];
void build_tree(int x)
{
h[x]=true;
for (int i=1;i<=g[x][0];++i)
if (!h[g[x][i]])
{
t[x][++t[x][0]]=g[x][i];
build_tree(g[x][i]);
}
void dp(int x)
{
if (t[x][0]==0)
{
f[x][1]=d[x];
f[x][2]=0;
}
else
{
for (int i=1;i<=t[x][0];++i)
dp(t[x][i]);
for (int i=1;i<=t[x][0];++i)
{
int k=t[x][i];
f[x][1]+=f[k][2];
f[x][2]+=max(f[k][1],f[k][2]);
}
f[x][1]+=d[x];
}
}
int main()
{
scanf("%d%d",&k,&m);
for (int i=1;i<=k;++i) scanf("%d",&d[i]);
for (int i=1;i<=m;++i)
{
scanf("%d%d",&a,&b);
g[a][++g[a][0]]=b;
g[b][++g[b][0]]=a;
}
for (int i=1;i<=k;++i)
if (g[i][0]==0)
{
h[i]=true;
ans+=d[i];
}
else
if (!h[i])
{
build_tree(i);
dp(i);
ans+=max(f[i][1],f[i][2]);
}
printf("%d\n",ans);
}
6、没有上司的舞会(dhoj1073)
【问题描述】
有个公司要举行一场晚会。
为了让到会的每个人不受他的直接上司约束而能玩得开心,公司领导决定:如果邀请了某个人,那么一定不会再邀请他的直接的上司,但该人的上司的上司,上司的上司的上司……都可以邀请。
已知每个人最多有唯一的一个上司。
已知公司的每个人参加晚会都能为晚会增添一些气氛,求一个邀请方案,使气氛值的和最大。
【输入:】
第1行一个整数N(1<=N<=6000)表示公司的人数。
接下来N行每行一个整数。
第i行的书表示第i个人的气氛值x(-128<=x<=127)。
接下来每行两个整数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
【分析】
如上例,上司与小兵之间的关系构成一棵树。
5
| \
3 4
| \ | \
1 2 6 7
解题报告:
题目很明显的为我们设置了一个树结构的问题,并且这个树有唯一的根节点(也就是说只有一棵树,并且这棵树中包含所有的点)。
同时题目中还要求到,任何一个员工不愿意和他的直接上司同时与会,也就是我上面提及到的,选取了父亲节点就不能选取子节点,因此我们向树形DP方向考虑。
分析可得:
1、当我们选取某个点的时候,那么这个点的子节点就全部都不能选取;
2、当我们不选取某个点的时候,那么这个点的子节点可以选也可以不选;
我们用 f[i] 表示选取第i 个节点的时候能够得到的最大快乐程度;g[i] 表示不选取第i 个节点的时候能够得到的最大快乐程度,那么我们可以得到状态转移式:
f[i]:=g[1]+g[2]+……+g[n];g[i]:=max(max(f[1],g[1]),max(f[2],g[2]),……,max(f[n],g[n]))
(1……n 表示 i 的子节点);
我们最终的答案就是max(f[root],g[root]) root表示根节点。
正如上面所说,这道题目我们从根节点开始一层一层搜下去,因此需要采取记忆化搜索,这个就不再赘言了,应该很容易。
还有数据中存在负数,这个一定要注意到,宁愿一个人都不邀请也不能出现负数呀!就像生活开聚会一样,宁缺勿滥!
【题解】
#include <cstdio>
#include <cstring>
#include <algorithm>
using namespace std;
int father[6005],vis[6005],dp[6005][2],t;
void dfs(int node)
{
int i,j;
vis[node] = 1;
for(i = 1;i<=t;i++)
{
if(!vis[i] && father[i] == node)
{
dfs(i);
dp[node][1]+=dp[i][0];
dp[node][0]+=max(dp[i][0],dp[i][1]);
}
}
}
int main()
{
int i,j,l,k,root;
while(~scanf("%d",&t))
{
for(i = 1;i<=t;i++)
scanf("%d",&dp[i][1]);
while(scanf("%d%d",&l,&k),l+k>0)
{
father[l] = k;
root = k;
}
memset(vis,0,sizeof(vis));
dfs(root);
printf("%d\n",max(dp[root][1],dp[root][0]));
}
return 0;
}
7、选课(dhoj1072)
【问题描述】
学校实行学分制。
每门的必修课都有固定的学分,同时还必须获得相应的选修课程学分。
学校开设了N (N<300)门的选修课程,每个学生可选课程的数量M 是给定的。
学生选修了这M 门课并考核通过就能获得相应的学分。
在选修课程中,有些课程可以直接选修,有些课程需要一定的基础知识,必须在选了其它的一些课程的基础上才能选修。
例如《FrontPage》必须在选修了《Windows 操作基础》之后才能选修。
我们称《Windows 操作基础》是《FrontPage》的先修课。
每门课的直接先修课最多只有一门。
两门课也可能存在相同的先修课。
每门课都有一个课号,依次为1,2,3,…。
例如:
表中1是2的先修课,2是3、5的先修课。
如果要选3,那么1和2都一定已被选修过。
你的任务是为自己确定一个选课方案,使得你能得到的学分最多,并且必须满足先修课优先的原则。
假定课
程之间不存在时间上的冲突。
【输入格式】
输入文件的第一行包括两个整数N、M (中间用一个空格隔开)其中1≤N≤300,1≤M≤N。
以下N行每行代表一门课。
课号依次为1,2,…,N。
每行有两个数(用一个空格隔开),第一个数为这门课先修课的课号(若不存在先修课则该项为0),第二个数为这门课的学分。
学分是不超过10的正整数。
【输出格式】
输出文件只有一个数:实际所选课程的学分总数。
【输入样例】
7 4
2 2
0 1
0 4
7 1
7 6
2 2
【输出样例】
13
分析:
首先可以从题目中看出这是一类树形动态规划题。
为了方便储存,可以将多叉树转化为二叉树,常用的表示方法是左孩子右兄弟表示法。
设f[i,j]表示以i为节点选j门课所获得的最大学分,则f[I,J]:=max(f[left[i],k]+f[right[i],j-k]+w[i]);0<=k<=j。
f[right[i].j-k]表示右孩子只能选j-k门课。
以-oo表示空节点,0表示根节点,1~n是n门可选课的节点。
在竞赛中如何规划用递归思想来实现动态规划很重要。
树形DP。
先多叉转二叉。
F[I,J]表示以I为接点取j门课程能得到的最大分数。
f[i,j]:=max{f[t[i].l,k]+f[t[i].r,j-k-1]+c[i]}
#include<cstdio>
#include<iostream>
using namespace std;
int m,n;
struct node{
int l,r;
}e[100010];
int w[310],a[310];
int f[310][310];
void dp(int x,int c)
{
if (x==0 || c<=0) return;
if (f[x][c]>0) return;
dp(e[x].r,c);
f[x][c]=max(f[x][c],f[e[x].r][c]);
for (int i=1;i<=c;i++)
{
dp(e[x].l,i-1);
dp(e[x].r,c-i);
f[x][c]=max(f[x][c],f[e[x].r][c-i]+f[e[x].l][i-1]+w[x]);
}
}
int main()
{
scanf("%d%d",&n,&m);
for (int i=1;i<=n;i++)
{
int x;
scanf("%d%d",&x,&w[i]);
if (a[x]==0) e[x].l=i;
else e[a[x]].r=i;
a[x]=i;
dp(e[0].l,m);
int ans=f[e[0].l][m];
printf("%d\n",ans);
return 0;
}
8、河流(TYVJ1506)
几乎整个Byteland 王国都被森林和河流所覆盖。
小点的河汇聚到一起,形成了稍大点的河。
就这样,所有的河水都汇聚并流进了一条大河,最后这条大河流进了大海。
这条大河的入海口处有一个村庄——Bytetown。
在Byteland国,有n个伐木的村庄,这些村庄都座落在河边。
目前在Bytetown,有一个巨大的伐木场,它处理着全国砍下的所有木料。
木料被砍下后,顺着河流而被运到Bytetown的伐木场。
Byteland 的国王决定,为了减少运输木料的费用,再额外地建造k个伐木场。
这k个伐木场将被建在其他村庄里。
这些伐木场建造后,木料就不用都被送到Bytetown了,它们可以在运输过程中第一个碰到的新伐木场被处理。
显然,如果伐木场座落的那个村子就不用再付运送木料的费用了。
它们可以直接被本村的伐木场处理。
注:所有的河流都不会分叉,形成一棵树,根结点是Bytetown。
国王的大臣计算出了每个村子每年要产多少木料,你的任务是决定在哪些村子建设伐木场能获得最小的运费。
其中运费的计算方法为:每一吨木料每千米1分钱。
编一个程序:
1.从文件读入村子的个数,另外要建设的伐木场的数目,每年每个村子产的木料的块数以及河流的描述。
2.计算最小的运费并输出。
【输入格式】第一行包括两个数n(2<=n<=100),k(1<=k<=50,且k<=n)。
n为村庄数,k为要建的伐木场的数目。
除了Bytetown 外,每个村子依次被命名为1,2,3……n,Bytetown被命名为0。
接下来n行,每行3个整数:
wi——每年i 村子产的木料的块数。
(0<=wi<=10000)
vi——离i 村子下游最近的村子。
(即i 村子的父结点)(0<=vi<=n)
di——vi 到i 的距离(千米)。
(1<=di<=10000)
保证每年所有的木料流到bytetown 的运费不超过2000,000,000分
50%的数据中n不超过20。
【输出格式】输出最小花费,精确到分。
Sample Input
4 2
1 0 1
1 1 10
10 2 5
1 2 3
Sample Output
4
【解析】
此题很明显的是一个树形动态规划题。
定义F[I,J,Fa]代表以I节点为根的子树,新增J个木材处理厂,离它最近的木材处理厂是Fa,,有如下方程:F[I,J,Fa]:=Min{F[Left[I],K,Fa]+F[Right[I],J-K,Fa]+Cost[I]*(Dis[I]-Dis[Fa]),F[Left[I],K,I]+F[Right[I],J-K-1,Fa]}
仍然用左孩子右兄弟来实现多叉转二叉。
需要提前预处理出来一点到其余各点间的距离。
【分析】树形动归。
首先转换为二叉树。
转换后,设f[i,j,k]表示以i为根的子树最近的伐木场在j件k个伐木场的最小耗费总和,则对于每一个顶点有放伐木场与不放两种方式。
f[i,j,k]=min{
放伐木场:f[i左孩子,i,k']+f[i兄弟,j,k-k'-1]
不放伐木场:i的运费+f[i的孩子,j,k']+f[i的兄弟,j,k-k']
}
树背包, 左儿子右兄弟来表示树, dp(x, y, z)表示结点x, x的子树及x的部分兄弟共建y个伐木场, 离x最近的伐木场是z时的最小代价. 时间复杂度O(N^2*K^2)
【源程序】
#include<cstdio>
#include<iostream>
#include<cstring>
using namespace std;
int son[101],snum[101],next[101],s[101],s0[101],id[102],d[101],fa[101];
int f[101][101][51];
int n,m,i,j,k,l,k0,tot,id0;
void build(int p,int d0)
{
int i;
++tot;
id[tot]=p;
d[p]=d0+s[p];
for(i=son[p];i;i=next[i])build(i,d[p]);
}
int main()
{
scanf("%d%d",&n,&k);
for(i=1;i<=n;++i)
{
scanf("%d%d%d",&s0[i],&k0,&s[i]);
fa[i]=k0;
next[i]=son[k0];
son[k0]=i;
}
build(0,0);
fa[0]=-1;
memset(f,0x7f,sizeof(f));
memset(f[0],0,sizeof(f[0]));
for(i=n+1;i>=2;--i)
{
id0=id[i];
for(j=fa[id0];j!=-1;j=fa[j])
{
for(k0=0;k0<=k;++k0)
{
for(l=0;l<=k0;++l)
if(f[son[id0]][j][l]!=0x7f7f7f7f)
{
if(f[next[id0]][j][k0-l]!=0x7f7f7f7f)
if(f[son[id0]][j][l]+f[next[id0]][j][k0-l]+s0[id0]*(d[id0]-d[j])<f[id0][j][k0])
f[id0][j][k0]=f[son[id0]][j][l]+f[next[id0]][j][k0-l]+s0[id0]*(d[id0]-d[j]);
}
else break;
for(l=0;l<=k0-1;++l)
if(f[son[id0]][id0][l]!=0x7f7f7f7f)
{
if(f[next[id0]][j][k0-l-1]!=0x7f7f7f7f)
if(f[son[id0]][id0][l]+f[next[id0]][j][k0-l-1]<f[id0][j][k0])
f[id0][j][k0]=f[son[id0]][id0][l]+f[next[id0]][j][k0-l-1];
}
else break;
}
}
}
printf("%d\n",f[son[0]][0][k]);
}。