【ACM/ICPC2013】树形动态规划专题
前言:按照计划,昨天应该是完成树形DP7题和二分图、最大流基础专题,但是由于我智商实在拙计,一直在理解树形DP的思想,所以第二个专题只能顺延到今天了。但是昨天把树形DP弄了个5成懂我是很高兴的!下面我把这7题的解题思想和部分代码分享给大家。
题目一:皇宫看守
问题描述:
太平王世子事件后,陆小凤成了皇上特聘的御前一品侍卫。
皇宫以午门为起点,直到后宫嫔妃们的寝宫,呈一棵树的形状;某些宫殿间可以互相望见。大内保卫森严,三步一岗,五步一哨,每个宫殿都要有人全天候看守,在不同的宫殿安排看守所需的费用不同。
可是陆小凤手上的经费不足,无论如何也没法在每个宫殿都安置留守侍卫。
编程任务:
帮助陆小凤布置侍卫,在看守全部宫殿的前提下,使得花费的经费最少。
数据输入:
输入文件中数据表示一棵树,描述如下:
第1行 n,表示树中结点的数目。
第2行至第n+1行,每行描述每个宫殿结点信息,依次为:该宫殿结点标号i(0<i<=n),在该宫殿安置侍卫所需的经费k,该边的儿子数m,接下来m个数,分别是这个节点的m个儿子的标号r1,r2,…,rm。
对于一个n(0 < n<=1500)个结点的树,结点标号在1到n之间,且标号不重复。
数据输出:
输出文件仅包含一个数,为所求的最少的经费。
样例输入:
6
1 30 3 2 3 4
2 16 2 5 6
3 5 0
4 4 0
5 11 0
6 5 0
样例输出:
25
分析:本来这套题里还有一个题目叫“战略游戏”,抽象后的模型跟这题是一样的,遇到那题的时候我没太理解题解是怎么用树形DP做的,于是我就跳过了。后来遇到这题发现不研究的话不行。题目说的很清楚,用最少的点覆盖所有的点,(二分图里有一个模型叫用最少的点覆盖所有的边,以后再议)。如果是一个图的话,它是个NP完全问题,但题目给出的是个树,避免了后效性的问题,所以可以用动态规划来解决。
给出如下定义:
- F[i,0]表示i点不放,且以i为根节点的子树(包括i节点)全部被观察到;
- F[i,1]表示i点不放,且以i为根节点的子树(可以不包括i节点)全部被观察到;
- F[i,2]表示i点放,且以i为根节点的子树全部被观察到;
转移如下:
1、由F[i,0]定义可知,设j为i的儿子节点,至少要有一个i的儿子节点是放置守卫的,其余的儿子节点可放可不放,但由于根节点i不放,所以其余的儿子节点如果不放的话,必须保证能被观察到,即F[j][0];所以我们需要枚举必须放置的儿子节点,下面的转移方程描述的很清楚:
- F[i,0] = min{Sigma(min(F[j][0],F[j,2]))+F[k,2]},其中k为枚举的必放的儿子节点,j为除了k之外的儿子节点
2、由F[i,1]定义可知,i可以被观察到也可以不被观察到,但儿子节点必须都要被观察到,转移如下:
- F[i,1] = Sigma(min(F[j,0],F[j,2])) j是i的儿子节点
3、由F[i,2]定义可知,i点放置了守卫,所以对于每个儿子节点都能被观察到,取F[j,0],F[j,1],F[j,2]最小值即可:
- F[i,2] = min(F[j,0],F[j,1],F[j,2]) j是i的儿子节点
- 对于叶节点i,F[i,0] = F[i,2] = data[i],F[i,1] = 0;
看了题解我是恍然大悟啊,智商不够,这个转移方程还是比较难想的。
参考代码:
//
// 皇宫看守.cpp
// 树形DP
//
// Created by TimmyXu on 13-8-3.
// Copyright (c) 2013年 TimmyXu. All rights reserved.
//
#include <iostream>
#include <algorithm>
#include <cstdio>
#include <cstring>
#include <string>
using namespace std;
const int maxn = +;
int f[maxn][],data[maxn],n,son[maxn][maxn],len[maxn],du[maxn],x,root;
void doit(int x)
{
if (len[x] == )
{
f[x][] = f[x][] = data[x];
f[x][] = ;
return;
}
for (int i = ;i <= len[x];i++)
doit(son[x][i]);
f[x][] = INT_MAX;
for (int i = ;i <= len[x];i++)
{
int tmp = ;
for (int j = ;j <= len[x];j++)
if (i!=j)
tmp += min(f[son[x][j]][],f[son[x][j]][]);
f[x][] = min(f[x][],tmp+f[son[x][i]][]);
}
f[x][] = ;
for (int i = ;i <= len[x];i++)
f[x][] += min(f[son[x][i]][],f[son[x][i]][]);
f[x][] = data[x];
for (int i = ;i <= len[x];i++)
f[x][] += min(f[son[x][i]][],min(f[son[x][i]][],f[son[x][i]][]));
}
int main()
{
scanf("%d",&n);
memset(data,,sizeof(data)); //存每个点放置守卫的代价
memset(f,,sizeof(f)); //dp数组,F[i,j]含义如上述分析
memset(du,,sizeof(du)); //存储每个点的入度,用于找出根节点
memset(son,,sizeof(son)); //存储每个点的儿子节点
memset(len,,sizeof(len)); //存储每个点儿子节点个数
for (int i = ;i <= n;i++)
{
scanf("%d",&x);
scanf("%d%d",&data[x],&len[x]);
for (int j = ;j <= len[x];j++)
{
scanf("%d",&son[x][j]);
du[son[x][j]]++;
}
}
for (int i = ;i <= n;i++)
if (du[i] == )
{
root =i;
break;
}
doit(root);
printf("%d\n",min(f[root][],f[root][]));
}
题目二:选课(Vijos1180)
【问题描述】
大学里实行学分。每门课程都有一定的学分,学生只要选修了这门课并考核通过就能获得相应的学分。学生最后的学分是他选修的各门课的学分的总和。
每个学生都要选择规定数量的课程。其中有些课程可以直接选修,有些课程需要一定的基础知识,必须在选了其它的一些课程的基础上才能选修。例如,《数据结构》必须在选修了《高级语言程序设计》之后才能选修。我们称《高级语言程序设计》是《数据结构》的先修课。每门课的直接先修课最多只有一门。两门课也可能存在相同的先修课。为便于表述每门课都有一个课号,课号依次为1,2,3,……。下面举例说明
课号 先修课号 学分
1 无 1
2 1 1
3 2 3
4 无 3
5 2 4
上例中1是2的先修课,即如果要选修2,则1必定已被选过。同样,如果要选修3,那么1和2都一定已被选修过。
学生不可能学完大学所开设的所有课程,因此必须在入学时选定自己要学的课程。每个学生可选课程的总数是给定的。现在请你找出一种选课方案,使得你能得到学分最多,并且必须满足先修课优先的原则。假定课程之间不存在时间上的冲突。
【输入格式】
输入文件的第一行包括两个正整数M、N(中间用一个空格隔开)其中M表示待选课程总数(1≤M≤1000),N表示学生可以选的课程总数(1≤N≤M)。
以下M行每行代表一门课,课号依次为1,2……M。每行有两个数(用一个空格隔开),第一个数为这门课的先修课的课号(若不存在先修课则该项为0),第二个数为这门课的学分。学分是不超过10的正整数。
【输出格式】
输出文件第一行只有一个数,即实际所选课程的学分总数。以下N行每行有一个数,表示学生所选课程的课号。
【输入样例】choose.in
7 4
2 2
0 1
0 4
2 1
7 1
7 6
2 2
【输出样例】choose.out
13
2
6
7
3
分析:这是一个树形背包,在《背包九讲》里面,这种题型被称为泛化物品。用于求解多叉树上的背包问题。
徐持衡大牛在2009年国家队论文《浅谈几类背包题》一文中给出了一种O(n*C)的算法,我昨天痛苦的理解了一天(仍然是智商不够)估计自己才理解了五成。下面我就说说我的理解。
F[i,m]表示以i为根的子树被分配到m的容量所能获得的最大得分。
对于i的每一个儿子j,先将F[i,0~m-v[j]]全部赋值给F[j,0~m-v[j]],按我的理解,就是留出j的空间(因为一定要放),后剩下的m-v[j]个空间的最优值赋值给以j为根的子树去进行递归计算,递归计算完j后,F[i,m] = max(F[i,m],F[j,m-v[j]]+c[j]),v[j]<=m<=M,按我的理解,计算完j子树后,因为F[j,m]的最优值是包含了F[i,m]的最优值计算的,也就是包含了j之前的i的儿子计算出来的最优值,所以只需要在F[i,m]和F[j,m-v[j]]+c[j](放j结点)中取最大值代替F[i,m]即可。
以上就是我昨天一整天思考的结果。或许我理解的不对,或许在大牛眼里这很easy,不过对我来说已经是个很大的进步啦~
参考代码:
//
// 选课.cpp
// 树形DP
//
// Created by TimmyXu on 13-8-3.
// Copyright (c) 2013年 TimmyXu. All rights reserved.
//
#include <iostream>
#include <algorithm>
#include <cstdio>
#include <cstring>
#include <string>
using namespace std;
const int maxn = +;
int x,n,m,data[maxn],g[maxn][maxn],f[maxn][maxn];
void doit(int root,int res)
{
if (res<=) return;
for (int i = ;i <= g[root][];i++)
{
for (int j = ;j <= res-;j++) f[g[root][i]][j] = f[root][j]+data[g[root][i]]; //先把i子树已经算过的最优值赋值给j子树
doit(g[root][i],res-); //递归计算j子树
for (int j = ;j <= res;j++)
f[root][j] = max(f[root][j],f[g[root][i]][j-]); //用j子树最优值来更新i子树
}
}
int main()
{
scanf("%d%d",&n,&m);
for (int i = ;i <= n;i++)
{
scanf("%d%d",&x,&data[i]);
g[x][]++;
g[x][g[x][]] = i;
}
memset(f,,sizeof(f));
doit(,m);
printf("%d\n",f[][m]);
}
题目三:技能树
【问题描述】
玩过Diablo的人对技能树一定是很熟悉的。一颗技能树的每个结点都是一项技能,要学会这项技能则需要耗费一定的技能点数。只有学会了某一项技能以后,才能继续学习它的后继技能。每项技能又有着不同的级别,级别越高效果越好,而技能的升级也是需要 耗费技能点数的。
有个玩家积攒了一定的技能点数,他想尽可能地利用这些技能点数来达到最好的效果。因此他给所有的级别都打上了分,他认为效果越好的分数也越高。现在他要你帮忙寻找一个分配技能点数的方案,使得分数总和最高。
【输入格式】
第一行是一个整数n(1<=n<=20),表示所有不同技能的总数。接下来依次给出n个不同技能的详细情况。每个技能描述包括5行,第一行是该技能的名称,第2行是该技能在技能树中父技能的名称,为空则表示该技能不需要任何的先修技能便能学习。第3行是一个整数L(1<=L<=20),表示这项技能所能拥有的最高级别。第4行共有L个整数,其中第I个整数表示从地I-1级升到第I级所需要的技能点数(0级表示没有学习过)。第5行包括L个整数,其中第I个整数表示从第I-1级升级到第I级的效果评分,分数不超过20。在技能描述之后,共有两行,第1行是一个整数P,表示目前所拥有的技能点数。接下来1行是N个整数,依次表示角色当前习得的技能级别,0表示还未学习。这里不会出现非法情况。
【输出格式】
S,表示最佳分配方案所得的分数总和。
【输入样例】
3
Freezing Arrow
Ice Arrow
3
3 3 3
15 4 6
Ice Arrow
Cold Arrow
2
4 3
10 17
Cold Arrow
3
3 3 2
15 5 2
10
0 0 1
【输出样例】
42
分析:容许我先乐呵一下。因为这是我第一道独立做出来的多叉树形背包的题目!(智商低就是容易满足。。)
这个题目根上一题的区别在于,每个结点的v[i]和c[i]是可以在一定范围内变化的,也就是说,要枚举不同的v[j],然后下放到j子树,然后加上不同的c[j]进行比较。
每个结点有初始等级,等级为0的不能作为根节点,必须要升级,那么对于等级不为0的儿子j,首先将i所获得的所有容量m全部赋值给儿子j,也就是说先不升级。然后再跟等级为0的儿子结点一样,从初始等级+1到最高等级进行枚举,设两个变量p(累加存储所需技能点数)和q(累加存储所获得的分数),进行赋值、递归、更新的三步计算。
然后我一开始WA了一次,为什么呢,我想了一下才明白,枚举每一个等级,将i结点的最优值赋值给儿子j时,必须是任何升级前的初始值,不然每次用上一级的最优值来计算,会出现重复累加的情况,最后答案会变大,所以枚举到儿子结点j时,先用一个tmp数组存下F[i,0~m]的初始值,就是前面几个儿子的最优值,然后对于不升级(初始等级不为0)或者每一次升级,直接将tmp赋值给F[j,0~m-p]即可。
吼吼,再次庆祝一下第一次做出来的题目。话说现在写题解感觉理解又加深了一步。
参考代码:
//
// 技能树.cpp
// 树形DP
//
// Created by TimmyXu on 13-8-3.
// Copyright (c) 2013年 TimmyXu. All rights reserved.
//
#include <iostream>
#include <algorithm>
#include <cstdio>
#include <cstring>
#include <string>
using namespace std;
const int maxn = +;
string na[maxn],st;
int f[maxn][+],son[maxn][maxn],len[maxn],n,m,l1[maxn],data[maxn],l2[maxn][maxn],sc[maxn][maxn],y,top;
int findst(const string st)
{
for (int i = ;i <= top;i++)
if (st == na[i])
return i;
top++;
na[top] = st;
return top;
}
void doit(int root,int res)
{
int tmp[+];
if (res < ) return;
for (int i = ;i <= son[root][];i++)
{
for (int k = ;k <= res;k++)
tmp[k] = f[root][k]; //先存储初始最优值
if (l1[son[root][i]] != ) //如果初始等级不为0,那先尝试不升级的最优
{
for (int k = ;k <= res;k++)
f[son[root][i]][k] = f[root][k];
doit(son[root][i],res);
for (int k = ;k <= res;k++)
f[root][k] = max(f[root][k],f[son[root][i]][k]);
}
int p = ,q = ;
for (int j = l1[son[root][i]]+;j <= len[son[root][i]];j++) //从当前等级升一级到最高等级进行枚举
{
p += l2[son[root][i]][j]; //累加所需技能点
q += sc[son[root][i]][j]; //累加所得分数
for (int k = ;k <= res-p;k++)
f[son[root][i]][k] = tmp[k];
doit(son[root][i],res-p);
for (int k = p;k <= res;k++)
f[root][k] = max(f[root][k],f[son[root][i]][k-p]+q);
}
}
}
int main()
{
scanf("%d",&n);
top = ;
memset(f,,sizeof(f)); //dp数组
memset(son,,sizeof(son)); //每个结点的儿子结点
memset(len,,sizeof(len)); //每个结点的最高等级
memset(l1,,sizeof(l1)); //每个结点的初始等级
memset(data,,sizeof(data)); //每个节点在字符串序列中的编号
memset(l2,,sizeof(l2)); //每个节点升级所需技能点
memset(sc,,sizeof(sc)); //每个节点升级所获得加分
for (int i = ;i <= n;i++)
{
getline(cin,st);
getline(cin,st);
data[i] = findst(st);
getline(cin,st);
if (st!="") y = findst(st);
else y = ;
son[y][]++;
son[y][son[y][]] = data[i];
scanf("%d",&len[data[i]]);
for (int j = ;j <= len[data[i]];j++)
scanf("%d",&l2[data[i]][j]);
for (int j = ;j <= len[data[i]];j++)
scanf("%d",&sc[data[i]][j]);
scanf("%d",&m);
for (int i = ;i <= n;i++)
scanf("%d",&l1[data[i]]);
doit(,m);
printf("%d\n",f[][m]);
}
题目四:Ural 1018 二叉苹果树
【问题描述】
有一棵苹果树,如果树枝有分叉,一定是分2叉(就是说没有只有1个儿子的结点)
这棵树共有N个结点(叶子点或者树枝分叉点),编号为1-N,树根编号一定是1。
我们用一根树枝两端连接的结点的编号来描述一根树枝的位置。下面是一颗有4个树枝的树
现在这颗树枝条太多了,需要剪枝。但是一些树枝上长有苹果。
给定需要保留的树枝数量,求出最多能留住多少苹果。
【输入格式】
第1行2个数,N和Q(1<=Q<= N,1<N<=100)。N表示树的结点数,Q表示要保留的树枝数量。接下来N-1行描述树枝的信息。每行3个整数,前两个是它连接的结点的编号。第3个数是这根树枝上苹果的数量。每根树枝上的苹果不超过30000个。
【输出格式】
一个数,最多能留住的苹果的数量。
【输入样例】Etree.in
5 2
1 3 1
1 4 10
2 3 20
3 5 20
【输出样例】Etree.out
21
分析:这种二叉树的树型DP还是比较简单的,首先建树,找根,分如下几种情况讨论:
- 1、当然容量为0或为叶结点,则返回
- 2、当前容量为1,则返回到左儿子边权值和到右儿子边权值中的最大值
- 3、当前容量大于1,分三种情况:1)全部给左儿子,2)全部给右儿子,3)左右儿子至少要给一个,剩下的枚举分配求最大值。在三种情况中取一个最大值即可。
参考代码:
//
// 二叉苹果树.cpp
// 树形DP
//
// Created by TimmyXu on 13-8-3.
// Copyright (c) 2013年 TimmyXu. All rights reserved.
//
#include <iostream>
#include <cstdio>
#include <cstring>
#include <string>
#include <algorithm>
using namespace std;
const int maxn = +;
int n,q,g2[maxn][maxn],g[maxn][maxn],l[maxn],r[maxn],root,f[maxn][maxn],x,y,du[maxn];
bool vis[maxn];
void createtree(int x)
{
if (x == ) return;
vis[x] = true;
for (int i = ; i<= g2[x][];i++)
if (!vis[g2[x][i]])
{
if (l[x] == ) l[x] = g2[x][i];
else r[x] = g2[x][i];
}
createtree(l[x]);
createtree(r[x]);
}
void doit(int root,int res)
{
if (res == )
{
f[root][res] = ;
return;
}
if (l[root] == )
{
f[root][res] = ;
return;
}
if (f[root][res]>) return; //记忆化,如果算过则不再计算
if (res == )
{
f[root][res] = max(g[root][l[root]],g[root][r[root]]); //如果只有容量1,则取左右中最大值
return;
}
for (int i = ;i <= res-;i++) //如果容量大于1,则先假设左右都被分配到
{
doit(l[root],i);
doit(r[root],res--i);
f[root][res] = max(f[root][res],f[l[root]][i]+f[r[root]][res-i-]);
}
f[root][res] += g[root][l[root]]+g[root][r[root]];
doit(l[root],res-); //只有左边
f[root][res] = max(f[root][res],f[l[root]][res-]+g[root][l[root]]);
doit(r[root],res-); //只有右边
f[root][res] = max(f[root][res],f[r[root]][res-]+g[root][r[root]]);
return;
}
int main()
{
scanf("%d%d",&n,&q);
memset(g,,sizeof(g));
memset(g2,,sizeof(g2));
memset(f,,sizeof(f));
memset(l,,sizeof(l));
memset(r,,sizeof(r));
memset(vis,false,sizeof(vis));
for (int i = ;i < n;i++)
{
scanf("%d%d",&x,&y);
scanf("%d",&g[x][y]);
g[y][x] = g[x][y];
g2[x][]++;
g2[x][g2[x][]] = y;
g2[y][]++;
g2[y][g2[y][]] = x;
}
root = ;
for (int i = ;i <= n;i++)
if (g2[i][] == )
{
root = i;
break;
}
createtree(root); //建树
doit(root,q);
printf("%d\n",f[root][q]);
}
题目五:将功补过
【问题背景】
作为间谍专家的Elvis Han受窃取X星球军事中心的秘密情报,他已经成功进入军事中心。但是很不幸的是,在他还没有找到任务需要情报的时候就被发现,这时他清楚他不可能完成任务了,不过还有机会将功补过,也就是得到一些不如任务情报有价值的其他情报,如果得到的情报的总价值大于等于任务情报价值,他也不会受到惩罚。很幸运的是他已经得到的军事中心的地图,情报都是隐藏在各个道路上的,但是他只有时间遍历一定数量的路(时间宝贵呀!还要逃跑。。)现在你做为他的助手,给你地图和每个道路情报价值,希望你分析出,看他能不能将功补过。
【问题描述】
军事中心是一个严格的二叉树,也就是说,如果有个点可以分道,一定是分出,也只分出2条道路,现在Elvis Han正处在第一个分道处,也就是说树的根结点处。每条道路上都有一个分数,就是这个道路上的情报价值。但是他只有时间走M条路,他的最终情报价值总和就是他所经过的路的情报价值总和(假设他到过的路一定可以把所有情报得到)希望你给出一个方案使得他可以尽量多地获取情报以便将功补过。
【输入格式】
在输入文件inform.in中,共有N行:
第一行:3个数据:N,M,Q(N表示有多少个路口,包括分道和不分道的路口;M表示他可以有时间走的道路总数;Q表示他的任务情报的价值)
第2~N行:每行3个数据,Xi,Yi,Wi (X,Y表示第I条道路连接的2个路口,W表示这条道路上的情报价值分, 注意,所有数据均在Lonint范围内)
【输出格式】
在输出文件inform.out中,共包含2行:
第一行:输出TRUE/FALSE(注意大小写),表示他是否可以收集够任务情报价值
第二行:输出一个数据:
如果他可以完成任务,就输出他收集的情报总价值超过任务情报价值的部分。(正数)
如果不能完成任务,就输出一个数,表示他不能还差多少分才够任务情报价值。(负数)
【输入样例1】
3 1 10
1 2 10
1 3 8
【输出样例1】
TRUE
0
样例说明:(该部分不必输出)
【输入样例2】
9 3 49
6 2 15
7 2 10
8 7 6
7 9 15
1 3 20
2 1 10
4 3 8
3 5 7
【输出样例2】
FALSE
-4
样例说明:
【数据规模】
对于30%的数据 保证有N<=10
对于50%的数据 保证有N<=40
对于全部的数据 保证有 N<=100
分析:同上题
参考代码:
//
// 将功补过.cpp
// 树形DP
//
// Created by TimmyXu on 13-8-3.
// Copyright (c) 2013年 TimmyXu. All rights reserved.
//
#include <iostream>
#include <cstdio>
#include <cstring>
#include <string>
#include <algorithm>
using namespace std;
const int maxn = +;
int f[maxn][maxn],n,m,q,x,y,w,g[maxn][maxn],g2[maxn][maxn],l[maxn],r[maxn],root;
bool vis[maxn];
void createtree(int x)
{
if (x == ) return;
vis[x] = true;
for (int i = ;i <= g2[x][];i++)
if (!vis[g2[x][i]])
{
if (l[x] == ) l[x] = g2[x][i];
else r[x] = g2[x][i];
}
createtree(l[x]);
createtree(r[x]);
}
void doit(int x,int res)
{
if (res == || l[x] == )
{
f[x][res] = ;
return;
}
if (f[x][res]>) return;
if (res == )
{
f[x][res] = max(g[x][l[x]],g[x][r[x]]);
return;
}
for (int i = ;i <= res-;i++)
{
doit(l[x],i);
doit(r[x],res-i-);
f[x][res] = max(f[x][res],f[l[x]][i]+f[r[x]][res-i-]);
}
f[x][res] += g[x][l[x]]+g[x][r[x]];
doit(l[x],res-);
f[x][res] = max(f[x][res],f[l[x]][res-]+g[x][l[x]]);
doit(r[x],res-);
f[x][res] = max(f[x][res],f[r[x]][res-]+g[x][r[x]]);
}
int main()
{
scanf("%d",&n,&m,&q);
memset(f,,sizeof(f));
memset(g,,sizeof(g));
memset(g2,,sizeof(g2));
memset(l,,sizeof(l));
memset(r,,sizeof(r));
memset(vis,false,sizeof(vis));
for (int i = ;i < n;i++)
{
scanf("%d%d%d",&x,&y,&w);
g[x][y] = g[y][x] = w;
g2[x][]++;
g2[x][g2[x][]] = y;
g2[y][]++;
g2[y][g2[y][]] = x;
}
for (int i = ;i <= n;i++)
if (g2[i][] == )
{
root = i;
break;
}
createtree(root);
doit(root,m);
if (f[root][m] >= q)
printf("TRUE\n%d\n",f[root][m]-q);
else printf("FALSE\n%d\n",f[root][m]-q);
}
题目六:加分二叉树
【问题描述】
设一个n个节点的二叉树tree的中序遍历为(l,2,3,…,n),其中数字1,2,3,…,n为节点编号。每个节点都有一个分数(均为正整数),记第j个节点的分数为di,tree及它的每个子树都有一个加分,任一棵子树subtree(也包含tree本身)的加分计算方法如下:
subtree的左子树的加分× subtree的右子树的加分+subtree的根的分数
若某个子树为空,规定其加分为1,叶子的加分就是叶节点本身的分数。不考虑它的空
子树。试求一棵符合中序遍历为(1,2,3,…,n)且加分最高的二叉树tree。要求输出;
(1)tree的最高加分
(2)tree的前序遍历
【输入格式】
第1行:一个整数n(n<30),为节点个数。
第2行:n个用空格隔开的整数,为每个节点的分数(分数<100)。
【输出格式】
第1行:一个整数,为最高加分(结果不会超过4,000,000,000)。
第2行:n个用空格隔开的整数,为该树的前序遍历。
【输入样例】
5
5 7 1 2 10
【输出样例】
145
3 1 2 4 5
分析:其实这个是一个区间动态规划,不能算是一个严格的树形DP。对于[l,r]范围内的最大值,每次枚举中间点作为根,递归计算左子树和右子树,算出来的值跟最大值比较即可。
参考代码:
//
// 加分二叉树.cpp
// 树形DP
//
// Created by TimmyXu on 13-8-3.
// Copyright (c) 2013年 TimmyXu. All rights reserved.
//
#include <iostream>
#include <string>
#include <cstdio>
#include <cstring>
#include <algorithm>
using namespace std;
const int maxn = +;
long long f[maxn][maxn];
int g[maxn][maxn],n,data[maxn];
void doit(int l,int r)
{
if (f[l][r] > ) return;
if (l == r)
{
f[l][r] = data[l];
g[l][r] = l;
return;
}
if (l > r)
{
f[l][r] = ;
return;
}
for (int i = l;i <= r;i++)
{
doit(l,i-);
doit(i+,r);
if (f[l][i-]*f[i+][r]+data[i] > f[l][r])
{
f[l][r] = f[l][i-]*f[i+][r]+data[i];
g[l][r] = i;
}
}
}
void show(int l,int r)
{
if (l > r) return;
if (l == r)
{
printf("%d ",l);
return;
}
printf("%d ",g[l][r]);
show(l,g[l][r]-);
show(g[l][r]+,r);
}
int main()
{
scanf("%d",&n);
for (int i = ;i <= n;i++)
scanf("%d",&data[i]);
memset(f,,sizeof(f));
memset(g,,sizeof(g));
doit(,n);
printf("%lld\n",f[][n]);
show(,n);
printf("\n");
}
题目七:Ural 1039 没有上司的晚会
【背景】
有个公司要举行一场晚会。为了能玩得开心,公司领导决定:如果邀请了某个人,那么一定不会邀请他的上司(上司的上司,上司的上司的上司……都可以邀请)。
【问题描述】
每个参加晚会的人都能为晚会增添一些气氛,求一个邀请方案,使气氛值的和最大。
【输入格式】
第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
分析:根据题目给出的矛盾关系写出动规方程:
F[i,0]表示不邀请i,以i为根的子树所获得的最大值
F[i,1]表示邀请i,以i为根的子树所获得的最大值
转移如下:
- F[i,0] = Sigma(max(F[j,0],F[j,1])) j是i的儿子 //不邀请i,那么儿子可来可不来,取最大值
- F[i,1] = Sigma(F[j,0]) j是i的儿子 //邀请i,那么儿子不能来,直接求和
参考代码:
//
// 没有上司的晚会.cpp
// 树形DP
//
// Created by TimmyXu on 13-8-3.
// Copyright (c) 2013年 TimmyXu. All rights reserved.
//
#include <iostream>
#include <algorithm>
#include <cstdio>
#include <cstring>
#include <string>
using namespace std;
const int maxn = +;
int n,root,du[maxn],data[maxn],f[maxn][],g[maxn][maxn],x,y;
void doit(int x)
{
f[x][] = ;
f[x][] = data[x];
for (int i = ;i <= g[x][];i++)
{
doit(g[x][i]);
f[x][] += max(f[g[x][i]][],f[g[x][i]][]);
f[x][] += f[g[x][i]][];
}
return;
}
int main()
{
scanf("%d",&n);
for (int i = ;i <= n;i++)
scanf("%d",&data[i]);
memset(f,,sizeof(f));
memset(g,,sizeof(g));
memset(du,,sizeof(du));
for (int i = ;i < n;i++)
{
scanf("%d%d",&x,&y);
du[x]++;
g[y][]++;
g[y][g[y][]]= x;
}
for (int i = ;i <= n;i++)
if (du[i] == )
{
root = i;
break;
}
doit(root);
printf("%d\n",max(f[root][],f[root][]));
}
总结:树形dp的题目,如果单纯给出节点间矛盾关系,或者在二叉树上做背包,都是比较简单的。我目前遇到的题目中,多叉树背包是一个难点,希望自己能继续学习,能拿下。OK,下午二分图最大流搞起!
【ACM/ICPC2013】树形动态规划专题的更多相关文章
- 蓝桥杯 ALGO-4 结点选择 (树形动态规划)
问题描述 有一棵 n 个节点的树,树上每个节点都有一个正整数权值.如果一个点被选择了,那么在树上和它相邻的点都不能被选择.求选出的点的权值和最大是多少? 输入格式 第一行包含一个整数 n . 接下来的 ...
- 树形动态规划(树状DP)小结
树状动态规划定义 之所以这样命名树规,是因为树形DP的这一特殊性:没有环,dfs是不会重复,而且具有明显而又严格的层数关系.利用这一特性,我们可以很清晰地根据题目写出一个在树(型结构)上的记忆化搜索的 ...
- NOIP2018提高组金牌训练营——动态规划专题
NOIP2018提高组金牌训练营——动态规划专题 https://www.51nod.com/Live/LiveDescription.html#!#liveId=19 多重背包 二进制优化转化成01 ...
- 正睿国庆DAY2动态规划专题
正睿国庆DAY2动态规划专题 排列-例题 1~n 的排列个数,每个数要么比旁边两个大,要么比旁边两个小 \(f[i][j]\) 填了前i个数,未填的数有\(j\)个比第\(i\)个小,是波峰 \(g[ ...
- 树形dp专题总结
树形dp专题总结 大力dp的练习与晋升 原题均可以在网址上找到 技巧总结 1.换根大法 2.状态定义应只考虑考虑影响的关系 3.数据结构与dp的合理结合(T11) 4.抽直径解决求最长链的许多类问题( ...
- ACM - 动态规划专题 题目整理
CodeForces 429B Working out 预处理出从四个顶点到某个位置的最大权值,再枚举相遇点,相遇的时候只有两种情况,取最优解即可. #include<iostream> ...
- 【ACM/ICPC2013】二分图匹配专题
前言:居然三天没有更新了..我的效率实在太低,每天都用各种各样的理由拖延,太差了!昨天的contest依旧不能让人满意,解出的三题都是队友A的,我又卖了一次萌..好吧废话不多说,今天我要纪录的是二分图 ...
- 动态规划专题(二)——树形DP
前言 \(DP\)这东西真的是博大精深啊...... 简介 树形\(DP\),顾名思义,就是在树上操作的\(DP\),一般可以用\(f_i\)表示以编号为\(i\)的节点为根的子树中的最优解. 转移的 ...
- 【ACM/ICPC2013】POJ基础图论题简析(一)
前言:昨天contest4的惨败经历让我懂得要想在ACM领域拿到好成绩,必须要真正的下苦功夫,不能再浪了!暑假还有一半,还有时间!今天找了POJ的分类题库,做了简单题目类型中的图论专题,还剩下二分图和 ...
随机推荐
- Hive 自定义函数(转)
Hive是一种构建在Hadoop上的数据仓库,Hive把SQL查询转换为一系列在Hadoop集群中运行的MapReduce作业,是MapReduce更高层次的抽象,不用编写具体的MapReduce方法 ...
- WiFi与WLAN的区别
很多人到了某一个地方首选要找的就是无线网络,有时候还会问周围的人:这里有WiFi吗?或者说:这里有WLAN吗?那WiFi和WLAN有什么区别呢? 简单来讲,WiFi是无线保真(wireless fid ...
- 安全测试常见的10个问题 ZT
1, 问题:没有被验证的输入 测试方法: 数据类型(字符串,整型,实数,等) 允许的字符集 最小和最大的长度 是否允许空输入 参数是否是必须的 重复是否允 ...
- const变量的存储区及修改权限
转自const变量的存储区及修改权限 [cpp] view plaincopy const int a = 1; int *p = const_cast<int*>(&a); *p ...
- Java 8 vs. Scala(二):Stream vs. Collection
[编者按]在之前文章中,我们介绍了 Java 8和Scala的Lambda表达式对比.在本文,将进行 Hussachai Puripunpinyo Java 和 Scala 对比三部曲的第二部分,主要 ...
- 开启CURL扩展,让服务器支持PHP curl函数(远程采集)
关于开启Curl的方法模板天下小编在此给大家简单说一下 curl().file_get_contents().snoopy.class.php这三个远程页面抓取或采集中用到的工具,默迹还是侵向于用sn ...
- ZOJ 3170 Friends
点我看题目 题意 : 就是有n个人,m对关系,每对关系的两个人是好朋友,这个关系是相互的,如果有两个人的共同好朋友超过k个,那这两个人也会是好朋友的,给你m对关系,给你足够长的时间,问你还能增加几对关 ...
- android ExpandableListActivity的使用
package com.example.keKuoZhanLieBiao; import android.app.ExpandableListActivity; import android.os.B ...
- php nl2br() 函数
nl2br() 函数在字符串中的每个新行 (\n) 之前插入 HTML 换行符 (<br />).
- Spring3 +mybatis3 之 MapperScannerConfigurer
之前一直使用"org.mybatis.spring.mapper.MapperFactoryBean"这个类在spring中配置mybatis的dao接口,后来发现如果dao太多话 ...