自己做了动态规划的题目已经有了一个月,但是成效甚微,所以来总结一下动态规划,希望自己能够温故知新。这个博客是关于树形dp的,动态规划的一类题目。

首先从最简单的树形DP入手,树形DP顾名思义就是一棵树和动态规划结合起来,我做了7,8题树形DP,目前为止发现树形DP的代码样式都是差不多,都在dfs树的过程中进行DP。

首先看一道简单的入门题目

题意就是在一棵树中,选取一些结点每个结点都可以监管者连接自己的一条边,问最少选取多少个结点可以让所有边都被监管起来。

思路:1:结点状态可以分为取和不取,所以用二维数组表示结点的状态。2:如果当前结点选取了,子结点可以选取也可以不选取,但是如果当前结点没有选取,那么子节点必须选取。

状态转移方程见代码里

#include <iostream>
#include <algorithm>
#include <string.h>
#include <math.h>
#include <stdlib.h> using namespace std;
#define MAX 1500
int n;
int root;
int tot;
struct Node
{
int value;
int next;
}edge[MAX*2+5];
int head[MAX+5];
int dp[MAX+5][2];
int vis[MAX+5];
void add(int x,int y)
{
edge[tot].value=y;
edge[tot].next=head[x];
head[x]=tot++;
}
void dfs(int root)
{
dp[root][0]=0;
dp[root][1]=1;
vis[root]=1;
for(int i=head[root];i!=-1;i=edge[i].next)
{
int k=edge[i].value;
if(!vis[k])
{
dfs(k);
//状态转移方程
dp[root][0]+=dp[k][1];
dp[root][1]+=min(dp[k][0],dp[k][1]);
}
}
}
int main()
{ int a,b;
int m;
while(scanf("%d",&n)!=EOF)
{
tot=0;
memset(vis,0,sizeof(vis));
memset(head,-1,sizeof(head));
memset(dp,0,sizeof(dp));
for(int i=0;i<n;i++)
{
scanf("%d:(%d)",&a,&m);
if(i==0) root=a;
for(int j=0;j<m;j++)
{
scanf("%d",&b);
add(a,b);
add(b,a);
}
}
dfs(root);
printf("%d\n",min(dp[root][0],dp[root][1]));
}
return 0;
}

这是一道入门题目,那么再看一到和它非常类似的题目,只是稍微变了一下

状态转移方程见代码里

(http://acm.hdu.edu.cn/showproblem.php?pid=1520)

#include <iostream>
#include <string.h>
#include <algorithm>
#include <math.h>
#include <stdlib.h> using namespace std;
#define MAX 6000
struct Node
{
int value;
int next;
}edge[MAX*2+5];
int tag[MAX+5];
int dp[MAX+5][2];
int n;
int root;
int tot;
int rat[MAX+5];
int head[MAX+5];
int vis[MAX+5];
void add(int x,int y)
{
edge[tot].value=y;
edge[tot].next=head[x];
head[x]=tot++;
}
void dfs(int root)
{
dp[root][1]=rat[root];
dp[root][0]=0;
vis[root]=1;
for(int i=head[root];i!=-1;i=edge[i].next)
{
int k=edge[i].value;
if(!vis[k])
{
dfs(k);
//状态转移方程
dp[root][1]+=dp[k][0];
dp[root][0]+=max(dp[k][0],dp[k][1]);
}
} }
int main()
{
int a,b;
while(scanf("%d",&n)!=EOF)
{
tot=0;
memset(head,-1,sizeof(head));
memset(dp,0,sizeof(dp));
memset(vis,0,sizeof(vis));
memset(tag,0,sizeof(tag));
for(int i=1;i<=n;i++)
{
scanf("%d",&rat[i]);
}
scanf("%d%d",&a,&b);
while(a!=0&&b!=0)
{
tag[a]=1;
add(b,a);
add(a,b);
scanf("%d%d",&a,&b);
}
for(int i=1;i<=n;i++)
{
if(tag[i]==0)
{
root=i;
break;
}
}
dfs(1);
printf("%d\n",max(dp[1][0],dp[1][1]));
}
return 0;
}

这两道题目可以说一个类型的,都是在树上选取一些结点,以获得最优值,限制条件就是子节点和父节点的关系,二者不可以都选取,或者不可以都不选取,所以代码几乎都一样。

接下来看一道比前面难一点的题目

(http://acm.hdu.edu.cn/showproblem.php?pid=1561)

题意:有n个城堡形成一棵树,你只能攻打m个城堡,每个城堡里都有相应的宝物,要攻克这些城堡必须先攻克其他某一个特定的城堡,就是先攻克根,才可以往下攻击子树,问可以获得最大的宝物。

思路:这道题目和前面不同的是,有两个限制条件,一个是必学攻克父节点才能去攻打子节点,另一个是只能攻克m个城堡。这里可以看出树形dp和依赖背包是不是有一点差不多。其实解决依赖背包问题,就是用树形dp的思路,先解决子树的最优解,然后才能得到根的最优解。过程是从下往上的,而树的DFS就是先搜索到叶子节点,然后逐层往上,不断更新结点的最优值,最终保证根节点的值是最优的。状态转移方程见代码里

#include <iostream>
#include <string.h>
#include <algorithm>
#include <stdlib.h>
#include <math.h> using namespace std;
#define MAX 200
struct Node
{
int value;
int next;
}edge[MAX+5];
int tot;
int head[MAX+5];
int n,m;
int num[MAX+5];
int dp[MAX+5][MAX+5];
int vis[MAX+5];
void add(int x,int y)
{
edge[tot].value=y;
edge[tot].next=head[x];
head[x]=tot++;
}
void dfs(int root,int tag)
{
vis[root]=1;
dp[root][1]=num[root];
for(int i=head[root];i!=-1;i=edge[i].next)
{
int t=edge[i].value;
if(!vis[t])
{
dfs(t,tag-1);
}
for(int j=tag;j>=1;j--)
{
for(int k=1;k<j;k++ )
{
//状态转移方程
dp[root][j]=max(dp[root][j],dp[root][j-k]+dp[t][k]);
}
}
}
}
int main()
{
int a;
while(scanf("%d%d",&n,&m)!=EOF)
{
if(n==0&&m==0)
break;
tot=0;
memset(head,-1,sizeof(head));
memset(dp,0,sizeof(dp));
memset(vis,0,sizeof(vis));
for(int i=1;i<=n;i++)
{
scanf("%d%d",&a,&num[i]);
add(a,i);
}
dfs(0,m+1);
printf("%d\n",dp[0][m+1]);
}
return 0;
}

代码看懂了也很好理解,接下来看这个问题的升级版(http://acm.hdu.edu.cn/showproblem.php?pid=1011)

题意:和上面一提差不多,变的是每个结点被攻克要求不同士兵数量

//
// main.cpp
// 树形DP2
//
// Created by 陈永康 on 16/1/3.
// Copyright (c) 2016年 陈永康. All rights reserved.
// #include <iostream>
#include <string.h>
#include <algorithm>
#include <math.h>
#include <stdlib.h> using namespace std;
int dp[105][105];//当前节点为i,小兵数为j得到的最大能量值
int n,m;
int bug[105];
int brain[105];
int head[105];
int tot;
int vis[105];
struct Node
{
int value;
int next;
}edge[105*2];
void add(int x,int y)
{
edge[tot].value=y;
edge[tot].next=head[x];
head[x]=tot++;
}
void dfs(int root,int tag)
{
vis[root]=1;
int term=(bug[root]+19)/20; dp[root][term]=brain[root];
for(int i=head[root];i!=-1;i=edge[i].next)
{
int u=edge[i].value;
if(!vis[u])
{
dfs(u,tag-term); for(int j=tag;j>=term;j--)
{
for(int k=1;j+k<=m;k++)
{
if(dp[u][k])
dp[root][j+k]=max(dp[root][j+k],dp[root][j]+dp[u][k]);
}
}
}
}
}
int main()
{
int x,y;
while(scanf("%d%d",&n,&m)!=EOF)
{
if(n==-1&&m==-1)
break;
memset(vis,0,sizeof(vis));
memset(head,-1,sizeof(head));
tot=0;
memset(dp,0,sizeof(dp));
for(int i=1;i<=n;i++)
{
scanf("%d%d",&bug[i],&brain[i]);
}
for(int i=1;i<n;i++)
{
scanf("%d%d",&x,&y);
add(x,y);
add(y,x);
}
dfs(1,m);
if(m==0)
printf("0\n");
else
printf("%d\n",dp[1][m]);
}
}
值得注意的是还有另外一个ac代码,它和上面有一点不同
#include <iostream>
#include <string.h>
#include <algorithm>
#include <math.h>
#include <stdlib.h> using namespace std;
int dp[105][105];//当前节点为i,小兵数为j得到的最大能量值
int n,m;
int bug[105];
int brain[105];
int head[105];
int tot;
int vis[105];
struct Node
{
int value;
int next;
}edge[105*2];
void add(int x,int y)
{
edge[tot].value=y;
edge[tot].next=head[x];
head[x]=tot++;
}
void dfs(int root)
{
vis[root]=1;
int term=(bug[root]+19)/20;
for(int i=term;i<=m;i++)
dp[root][i]=brain[root];
for(int i=head[root];i!=-1;i=edge[i].next)
{
int u=edge[i].value;
if(!vis[u])
{
dfs(u); for(int j=m;j>=term;j--)
{
for(int k=1;j+k<=m;k++)
{
if(dp[u][k])
dp[root][j+k]=max(dp[root][j+k],dp[root][j]+dp[u][k]);
}
}
}
}
}
int main()
{
int x,y;
while(scanf("%d%d",&n,&m)!=EOF)
{
if(n==-1&&m==-1)
break;
memset(vis,0,sizeof(vis));
memset(head,-1,sizeof(head));
tot=0;
memset(dp,0,sizeof(dp));
for(int i=1;i<=n;i++)
{
scanf("%d%d",&bug[i],&brain[i]);
}
for(int i=1;i<n;i++)
{
scanf("%d%d",&x,&y);
add(x,y);
add(y,x);
}
dfs(1);
if(m==0)
printf("0\n");
else
printf("%d\n",dp[1][m]);
}
}

这段代码并没有像前面那样,dfs的时候加上当前结点可以使用的小兵数量,似乎这样写下去并没有体现要攻打子节点必须攻克父节点这个限制条件啊,但这的确是对的。我的猜想是,这是两个不同的规划方式,第一个规划方式,你可以打一个dp二维数组的表,发现它是一个严格的从根节点往下,必须满足父节点被攻克,子节点才可以有最优值。而第二种方式,你可以发现,它的dp二维数组表,是以每一个结点为根节点得到这个结点的最优值。也就是说当我规划到这个节点,不用管它的父节点他就是根。所以回朔到根节点的时候,依然可以保证要攻打子节点必须攻克父节点这个限制条件,最终的值也是正确的。如果问题变成这样,你可以事先挑选某个结点作为根节点,往下去攻打。那么第二种解法就满足了

接下来,还是这个类型的题目但是又要更难一些,

(http://acm.hdu.edu.cn/showproblem.php?pid=4169)

题意:这个题目和前面的题目的区别就是,把要攻打子节点必须攻克父节点这个限制条件去掉了。加上一个,子节点和父节点不能共存。而且结点有1500000,所以你用dp数组表示结点的状态是肯定炸的。关键在于怎么设定dp数组,我感觉要不看博客accept这一题,是要对动态规划十分熟悉的。状态转移方程见代码,我把我的理解都写在注释里了

#include <iostream>
#include <string.h>
#include <algorithm>
#include <math.h>
#include <stdlib.h> using namespace std;
#define MAX 1500000
int v[MAX+5];
int head[MAX+5];
int dp[305];
int n;
int tot;
int m;
struct Node
{
int value;
int next;
}edge[MAX+5];
void add(int x,int y)
{
edge[tot].value=y;
edge[tot].next=head[x];
head[x]=tot++;
}
int dfs(int root)
{
int t=0;
int tag=0;
int cur[305];//这个数组是不断替换的
for(int i=0;i<=m;i++)
dp[i]=cur[i]=-1;
dp[0]=cur[0]=0;
for(int i=head[root];i!=-1;i=edge[i].next)
{
tag=1;
int k=edge[i].value;
//得到子树的状态数
int state=dfs(k);
for(int j=t;j>=0;j--)
{
for(int p=1;p<=state;p++)
{
if(p+j>m)
break;
cur[p+j]=max(cur[p+j],dp[p]+cur[j]);
}
}
t+=state;
}
if(!tag) t++;
cur[1]=max(cur[1],v[root]);
//把当前子树的状态保存到dp数组里,这里的dp数组表示当前这颗树的最优值,cur数组也是
for(int i=0;i<=m;i++)
dp[i]=cur[i];
return t;
}
int main()
{
int a;
int root;
while(scanf("%d%d",&n,&m)!=EOF)
{
memset(head,-1,sizeof(head));
tot=0;
for(int i=1;i<=n;i++)
{
scanf("%d%d",&a,&v[i]);
if(a==0)
root=i;
add(a,i);
}
dfs(root);
if(dp[m]==-1)
printf("impossible\n");
else
printf("%d\n",dp[m]); }
return 0;
}

这道题目定义了两个状态数组,cur[],dp[]。dp[]里面保存的是最终的答案,cur[]是一个临时的数组表示子树的状态。

看另一道不同风格的题目吧

(http://acm.hdu.edu.cn/showproblem.php?pid=2412)

这道题目和前面一道非常相似,但是有一点不同,它要询问这个最优值是不是只有一种组合方式。这道题目的解决方法是,再开一个状态数组,表示当前的最优值是不是唯一,如果能想到这里,问题就好解决了

#include <iostream>
#include <string.h>
#include <math.h>
#include <stdlib.h>
#include <algorithm>
#include <map>
#include <string> using namespace std;
#define MAX 200
map<string,int>m;
int n;
string a,b;
int tot;
int head[MAX+5];
int dp[MAX+5][2];
int dup[MAX+5][2];
bool tag;
struct Node
{
int value;
int next;
}edge[MAX*2+5];
void add(int x,int y)
{
edge[tot].value=y;
edge[tot].next=head[x];
head[x]=tot++;
}
void dfs(int root)
{
dp[root][1]=1;
dp[root][0]=0;
dup[root][1]=1;
dup[root][0]=1;
for(int i=head[root];i!=-1;i=edge[i].next)
{
int k=edge[i].value;
dfs(k);
dp[root][1]+=dp[k][0];
dp[root][0]+=max(dp[k][0],dp[k][1]);
if(dp[k][1]<dp[k][0]&&dup[k][0]==0)
dup[root][0]=0;
else if(dp[k][1]>dp[k][0]&&dup[k][1]==0)
dup[root][0]=0;
else if(dp[k][1]==dp[k][0])
dup[root][0]=0;
if(dup[k][0]==0)
dup[root][1]=0; }
}
int main()
{
while(scanf("%d",&n)!=EOF)
{
if(n==0)
break;
memset(head,-1,sizeof(head));
m.clear();
cin>>a;
m[a]=1;
int cnt=1;
tot=0;
for(int i=1;i<n;i++)
{
cin>>a>>b;
if(!m.count(a))
m[a]=++cnt;
if(!m.count(b))
m[b]=++cnt;
add(m[b],m[a]);
}
memset(dp,0,sizeof(dp));
memset(dup,0,sizeof(dup));
dfs(1);
if(dp[1][1]>dp[1][0]&&dup[1][1]==1)
printf("%d Yes\n",dp[1][1]);
else if(dp[1][0]>dp[1][1]&&dup[1][0]==1)
printf("%d Yes\n",dp[1][0]);
else
printf("%d No\n",max(dp[1][1],dp[1][0]));
}
return 0; }

前面说的都是在节点的值上面进行动态规划,下面看如果要在边上吗进行规划呢?

HDU-3452 Bonsai

题意就是给你一棵树,每个边都有权值,问你剪去那些边,可以让叶子节点不能达到根节点,且剪去边的权值和最小

思路:为了达到让叶子节点不能达到根节点的目的,你有两种选择要么剪去根节点和下一个子节点的边,要么不剪这条边,直接到子树里面取剪。状态转移方程也就清晰了,见代码里

#include <iostream>
#include <string.h>
#include <algorithm>
#include <math.h>
#include <stdlib.h> using namespace std;
#define MAX 1000
struct Node
{
int value;
int next;
int weight;
}edge[MAX*2+5];
int tot;
int n,r;
int a,b,c;
int head[MAX+5];
int vis[MAX+5];
int dp[MAX+5];
void add(int x,int y,int w)
{
edge[tot].weight=w;
edge[tot].value=y;
edge[tot].next=head[x];
head[x]=tot++;
}
void dfs(int root)
{ vis[root]=1;
int tag=0;
int sum;
for(int i=head[root];i!=-1;i=edge[i].next)
{
int k=edge[i].value;
sum=edge[i].weight;
if(!vis[k])
{
tag=1;
dfs(k);
dp[root]+=min(sum,dp[k]);
} }
if(tag==0)
dp[root]=sum;
}
int main()
{ while(scanf("%d%d",&n,&r)!=EOF)
{ if(n==0&&r==0)
break;
tot=0;
memset(head,-1,sizeof(head));
memset(dp,0,sizeof(dp));
memset(vis,0,sizeof(vis));
for(int i=0;i<n-1;i++)
{
scanf("%d%d%d",&a,&b,&c);
add(a,b,c);
add(b,a,c);
}
if(n==1)
printf("0\n");
else
{
dfs(r);
printf("%d\n",dp[r]);
} }
return 0;
}

再看一道这道题目的升级版,题意一样,不同的就是问你的是最小限制,就是你选取剪去的边里面权值最大的。这道题目的思路就是树形dp加二分,状态转移方程稍微变一下,加上一个限制。二分其实就是枚举这个限制,二分更快一点而已。

题目要问的是最小的最大限制,必然二分答案。

HDU-3586 Information Disturbing

#include <iostream>
#include <string.h>
#include <algorithm>
#include <stdlib.h>
#include <math.h> using namespace std;
#define MAX 1000
int dp[MAX+5];
int head[MAX+5];
int vis[MAX+5];
int n,m;
int tot;
int maxin;
struct Node
{
int value;
int weight;
int next;
}edge[MAX*2+5];
void add(int x,int y,int weight)
{
edge[tot].weight=weight;
edge[tot].value=y;
edge[tot].next=head[x];
head[x]=tot++;
}
void dfs(int root,int limit)
{ vis[root]=1;
bool tag=false;
int sum=0;
for(int i=head[root];i!=-1;i=edge[i].next)
{ int k=edge[i].value;
sum=edge[i].weight;
if(!vis[k])
{
tag=true;
dfs(k,limit);
if(sum>limit)
dp[root]+=dp[k];
else
dp[root]+=min(dp[k],sum); }
}
if(!tag)
dp[root]=1000010;
}
int main()
{
int x,y,z;
while(scanf("%d%d",&n,&m)!=EOF)
{
if(n==0&&m==0)
break;
memset(head,-1,sizeof(head));
tot=0;maxin=0;
for(int i=1;i<n;i++)
{
scanf("%d%d%d",&x,&y,&z);
maxin=max(maxin,z);
add(x,y,z);
add(y,x,z);
}
int l=1;int r=maxin;int ans=-1;
while(l<=r)
{
int mid=(l+r)/2;
memset(vis,0,sizeof(vis));
memset(dp,0,sizeof(dp));
dfs(1,mid);
if(dp[1]<=m)
{
ans=mid;
r=mid-1;
}
else
l=mid+1;
}
printf("%d\n",ans); }
return 0;
}

树形DP总结,持续更新的更多相关文章

  1. 【转】【DP_树形DP专辑】【9月9最新更新】【from zeroclock's blog】

    树,一种十分优美的数据结构,因为它本身就具有的递归性,所以它和子树见能相互传递很多信息,还因为它作为被限制的图在上面可进行的操作更多,所以各种用于不同地方的树都出现了,二叉树.三叉树.静态搜索树.AV ...

  2. 状压dp(状态压缩&&dp结合)学习笔记(持续更新)

    嗯,作为一只蒟蒻,今天再次学习了状压dp(学习借鉴的博客) 但是,依旧懵逼·································· 这篇学习笔记是我个人对于状压dp的理解,如果有什么不对的 ...

  3. DP刷题记录(持续更新)

    DP刷题记录 (本文例题目前大多数都选自算法竞赛进阶指南) TYVJ1071 求两个序列的最长公共上升子序列 设\(f_{i,j}\)表示a中的\(1-i\)与b中色\(1-j\)匹配时所能构成的以\ ...

  4. 理解DP(持续更新)

    理解DP author: thy from buaa 初见 dynamic programming(可以理解为动态刷表法 其实这里的programming并不是编程而是规划.设计表格的意思) 关于动态 ...

  5. 提升——树形DP

    这里讲提高一点的内容,所以没有树形DP基础的,先看一下基础部分: 浅说——树形DP 闲言不表,看第一题. 这道题是典型的树上最长链问题.(就是一个模板题) 给定一棵树,树上共有N个节点(N<=5 ...

  6. 【BZOJ-3572】世界树 虚树 + 树形DP

    3572: [Hnoi2014]世界树 Time Limit: 20 Sec  Memory Limit: 512 MBSubmit: 1084  Solved: 611[Submit][Status ...

  7. 【BZOJ-3631】松鼠的新家 树形DP?+ 倍增LCA + 打标记

    3631: [JLOI2014]松鼠的新家 Time Limit: 10 Sec  Memory Limit: 128 MBSubmit: 1231  Solved: 620[Submit][Stat ...

  8. 洛谷P2014 选课 (树形dp)

    10月1日更新.题目:在大学里每个学生,为了达到一定的学分,必须从很多课程里选择一些课程来学习,在课程里有些课程必须在某些课程之前学习,如高等数学总是在其它课程之前学习.现在有N门功课,每门课有个学分 ...

  9. POJ3162 Walking Race(树形DP+尺取法+单调队列)

    题目大概是给一棵n个结点边带权的树,记结点i到其他结点最远距离为d[i],问d数组构成的这个序列中满足其中最大值与最小值的差不超过m的连续子序列最长是多长. 各个结点到其他结点的最远距离可以用树形DP ...

随机推荐

  1. 几个实用的sublime text 2的快捷键

    Sublime text快捷键 Sublime text 2是一款轻量.简洁.高效.跨平台的编辑器,他适合编写javascript,html,css,php,paython等等, 作为程序员,编码效率 ...

  2. 【笔试面试】神马搜索C++程序猿电话面试

    面试时间:2015.07.15 预约时间:2015.07.14.电话面试前一天,会电话咨询你方面电话面试的时间. 面试环节: 无自我介绍(这是我面试这么多家公司碰到的第一次),直接面试内容. 问题1: ...

  3. Redis Cluster 简单安装配置

    1 新建目录 “/app/redis”,输入命令 mkdir -p /app/redis 2 先安装ruby-2.3.1.tar.gz 3 测试ruby是否安装成功,输入命令:gem,如果显示以下信息 ...

  4. SpringMVC由浅入深day01_13springmvc和struts2的区别_14问题

    13 springmvc和struts2的区别 1.springmvc是基于方法开发(一个url对应一个方法),请求参数传递到方法的形参,可以设计为单例或多例(建议单例),struts2是基于类开发, ...

  5. Git Step by Step – (8) Git的merge和rebase

    前面一篇文章中提到了"git pull"等价于"git fetch"加上"git merge",然后还提到了pull命令支持rebase模式 ...

  6. java中类相关注意事项

    下面default类就是默认修饰符的类 1.Java中调用类中属性或方法(不管是否静态属性或方法)都要在类的方法中调用,虽然这个太基础,但今天想在类中调用静态类的静态变量,不能调用: 2.Java调用 ...

  7. RF实现多次失败重跑结果合并的基础方法和优化方法

    实现思路:通过分次执行失败案例重跑,然后通过结果文件合并命令实现多次失败重跑结果文件的合并,并输出合并后的log和report文件: 说明:具体失败案例重跑命令和结果文件合并命令请参考本博客其他相关章 ...

  8. 【RF库Collections测试】Log Dictionary 【同log list】

    Name:Log DictionarySource:Collections <test library>Arguments:[ dictionary | level=INFO ]Logs ...

  9. CentOS6.5下安装Oracle11g

    一.安装前系统准备 1. 修改主机名 #sed -i "s/HOSTNAME=localhost.localdomain/HOSTNAME=oracledb.01/" /etc/s ...

  10. 在taro中跳转页面的时候执行两遍componentDidMount周期的原因和解决方法

    在做taro跳转的时候,发现在跳转后的页面会走两遍componentDidMount周期,查看了github上的issues,发现是跳转路由带参为中文引起的,只要把中文参数进行urlencode解决 ...