DP(动态规划)学习心得
Posted smilke
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了DP(动态规划)学习心得相关的知识,希望对你有一定的参考价值。
动态规划学习心得
说实话吧,动态规划(DP)确实是一个比较难的知识点,对于初学者来说,是一个难过的坎(笔者的脸呢?开玩笑。)。动态规划就是我从初学开始遇到的最神奇的解法,它不同于暴力搜索,也不同于一般的贪心,能够以出乎人意料的时间复杂度(近似于O(n^2))解决一些难题,算法远远优于一般的深搜(O(2^n))。不过,动态规划的思维性比较强,必须会设好状态,正确写出状态转移方程,并且能够准确判断有无最优子结构。
其实有点像贪心,但是它有局部最优解推导向整体最优解的过程,形象一点说,动态规划的“眼光”比贪心更长远,有一个更新最优解的过程,发现问题了可以“反悔”。它还有一点分治的味道,通过对问题划分各个阶段,对各个阶段分别求解,最后推向整体的过程。与分治法不同的是,适合于用动态规划求解的问题,经分解得到子问题往往不是互相独立的。若用分治法来解这类问题,则分解得到的子问题数目太多,有些子问题被重复计算了很多次。如果我们能够保存已解决的子问题的答案,而在需要时再找出已求得的答案,这样就可以避免大量的重复计算,节省时间。我们可以用一个表来记录所有已解的子问题的答案。不管该子问题以后是否被用到,只要它被计算过,就将其结果填入表中。这就是动态规划法的基本思路。具体的动态规划算法多种多样,但它们具有相同的填表格式。
学习动态规划是一个比较漫长的过程,需要慢慢领悟,去体会动态规划的奥义。显然,多做题,多思考是必需的,坚持下去,慢慢就能学会了。
下面详细地描述一下:
一.动态规划的表示方法:
一般地,动态规划有两种表示方法,分别是:1.递推 2.记忆化搜索。
这两种方法各有优缺点,递推的效率更高,可以降维节省空间,能使用滚动数组,但思维性强,难度高。而记忆化搜索更好写,更便于理解,不容易出错,但容易超空间。有时候状态数目多,记忆化搜索就不行了,会超空间。但是递推是绝对没有问题的,只要会滚动数组或者降维。所以,我比较推荐递推的方法,更能够锻炼我们的算法能力。所以我们一般用递推的方法解决动态规划的问题。
例如:下面的代码就是一种递推:
f[i][j]=max(f[i+1][j],f[i+1][j+1])+a[i][j];
至于记忆化搜索,也上一波代码:
int dfs(int k,int j) { if(f[k][j]) return f[k][j]; if(k == n) return a[k][j]; temp1 = dfs(k+1 , j); temp2 = dfs(k+1 , j+1); f[k][j] = max(temp1 , temp2)+a[k][j]; }
(以上代码以题目数字三角形为例)。
二.动态规划的条件:
动态规划有两个必要条件:
1.无后效性.
2.最优子结构.
无后效性:
标准定义是这样的:将各阶段按照一定的次序排列好之后,对于某个给定的阶段状态,它以前各阶段的状态无法直接影响它未来的决策,而只能通过当前的这个状态。换句话说,每个状态都是过去历史的一个完整总结。这就是无后向性,又称为无后效性。
最优子结构:
标准定义是这样的:一个最优化策略具有这样的性质,不论过去状态和决策如何,对前面的决策所形成的状态而言,余下的诸决策必须构成最优策略。简而言之,一个最优化策略的子策略总是最优的。一个问题满足最优化原理又称其具有最优子结构性质。
三.动态规划的分类及解决过程:
分类:
动态规划分为:
1.基本线性动规: 比较基础的DP入门
2.背包动规 背包问题,见某大佬著作《背包九讲》
3.区间动规 区间型的动态规划
4.双进程动规 分为两个进程,一般只需要增加一个维度表示状态就可以了。
5.树状动规 在树上做动态规划,较为高级。
6.各种优化...............等等
解决过程:
第一步:读懂题意,看看题目是否可以满足动态规划的条件(及是否可以用动态规划解决)。
第二步:根据题目所给的条件划分阶段,可以是题目给定的顺序,或者是贪心的顺序,或者是特殊的顺序。
第三步:根据阶段设置状态,一般用f数组表示,最基本规则:求什么设什么,必须满足无后效性 。当感觉是dp,但是当前状态不满足必要条件的时候,状态+维。
第四步:推出状态转移方程式,能够表示当前最优值和前面最优值的关系。
第五步:代码实现,检查前面的步骤是否正确。
四.动态规划经典例题详解:
1.基本线性动规:
hloj#402护卫队
见链接:https://www.cnblogs.com/smilke/p/10502784.html(本蒟蒻的一篇博客题解,如有不当之处欢迎指出)
2.背包动规:
【背包】采药
题目描述
宁智贤是个天资聪颖的孩子,他的梦想是成为世界上最伟大的医师。为此,他想拜附近最有威望的医师为师。
医师为了判断他的资质,给他出了一个难题。
医师把他带到一个到处都是草药的山洞里对他说:“孩子,这个山洞里有一些不同的草药,采每一株都需要一些时间,每一株也有它自身的价值。我会给你一段时间,在这段时间里,你可以采到一些草药。
如果你是一个聪明的孩子,你应该可以让采到的草药的总价值最大。”
如果你是宁智贤,你能完成这个任务吗?
输入格式
输入的第一行有两个整数T(1 <= T <= 1000)和M(1 <= M <= 100),用一个空格隔开,T代表总共能够用来采药的时间,M代表山洞里的草药的数目。接下来的M行每行包括两个在1到100之间(包括1和100)的整数,分别表示采摘某株草药的时间和这株草药的价值。
输出格式
输出包括一行,这一行只包含一个整数,表示在规定的时间内,可以采到的草药的最大总价值。
样例数据
input
70 3
71 100
69 1
1 2
output
3
数据规模与约定
时间限制:1s
空间限制:256MB
----------------------------------------我是美美的分割线-------------------------------------------------------
这道题就是最基本的01背包,对于每件物品,我们有取和不取两种选择.
首先定义状态f[i][j]以j为容量为放入前i个物品(按i从小到大的顺序)的最大价值,那么i=1的时候,放入的是物品1,这时候肯定是最优的.
由此,我们推出状态转移方程:f[i][j] = max(f[i-1][j-w[i]])+v[i],f[i-1][j]);
其实,这道题还有一个滚动数组优化,可以优化第一维的空间。
优化后的状态转移方程:f[j]=max(f[j-w[i]]+v[i],f[j]);
下面是代码:
#include<bits/stdc++.h> using namespace std; int t,m; int w[100010],v[100010]; int f[100010]; int main() { freopen("input.in","r",stdin); freopen("output.out","w",stdout); cin>>m>>t; for(int i=1;i<=t;i++) cin>>w[i]>>v[i]; for(int i=1;i<=m;i++) for(int j=m;j>=w[i];j--) { f[j]=max(f[j],f[j-w[i]]+v[i]); } cout<<f[m]; return 0; }
3.区间动规:
【区间动规】石子合并
题目描述
在操场上沿一直线排列着n堆石子。现要将石子有次序地合并成一堆。
规定每次只能选相邻的两堆石子合并成新的一堆,并将新的一堆石子数计为该次合并的得分。
我们希望这n−1次合并后得到的得分总和最小。
输入格式
第一行有一个正整数n(n<=300),表示石子的堆数; 第二行有n个正整数,表示每一堆石子的石子数,每两个数之间用一个空格隔开。它们都不大于10000。
输出格式
一行,一个整数,表示答案。
样例数据
input
3
1 2 9
output
15
数据规模与约定
区间dp第一题
时间限制:1s
空间限制:256MB
--------------------------------------------我是华丽的分割线-----------------------------------------------------
这样我们可以定义状态f[i][j],表示i到j合并后的最大得分。其中1<=i<=j<=N。
既然这样,我们就需要将这一圈石子分割。很显然,我们需要枚举一个k,来作为这一圈石子的分割线。
这样我们就能得到状态转移方程:
f[i][j] = max(f[i][k] + f[k+1][j] + d(i,j));
其中,1<=i<=<=k<j<=N。d(i,j)表示从i到j石子个数的和。
下面是代码:
#include<bits/stdc++.h> #define din(a) (scanf("%d",&a)); #define dout(a) (printf("%d ",a)); #define ll long long using namespace std; int m,k; int n; int a[101000]; int f[1001][1001]; int sumn[1001]; int cost[1001][1001]; void work_cost()//计算合并的代价/得分. { for(int i=1;i<=n;i++) for(int j=i;j<=n;j++) cost[i][j]=sumn[j]-sumn[i-1]; } void init() { din(n);//初始化 memset(f,0,sizeof(f)); memset(sumn,0,sizeof(sumn)); sumn[0]=0;//计算石子总数,方便累加得分. for(int i=1;i<=n;i++){ din(a[i]); sumn[i]=sumn[i-1]+a[i]; } work_cost(); } void work() //区间动规 { for(int p=1;p<=n;p++) for(int i=1;i<=n;i++){ int j=i+p-1; if(j>n) break; for(int k=i;k<j;k++) if((f[i][j]>f[i][k]+f[k+1][j]+cost[i][j]||(f[i][j]==0))) f[i][j]=f[i][k]+f[k+1][j]+cost[i][j]; } } int main() { freopen("Stone.in","r",stdin); freopen("Stone.out","w",stdout); init(); work(); dout(f[1][n]); return 0; }
4.双进程DP
构建双塔
题目描述
2001年9月11日,一场突发的灾难将纽约世界贸易中心大厦夷为平地,Mr. F曾亲眼目睹了这次灾难。为了纪念“911”事件,Mr. F决定自己用水晶来搭建一座双塔。
Mr. F有N块水晶,每块水晶有一个高度,他想用这N块水晶搭建两座有同样高度的塔,使他们成为一座双塔,Mr. F可以从这N块水晶中任取M(1≤M≤N)块来搭建。但是他不知道能否使两座塔有同样的高度,也不知道如果能搭建成一座双塔,这座双塔的最大高度是多少。所以他来请你帮忙。
给定水晶的数量NN(1≤N≤100)和每块水晶的高度Hi(N块水晶高度的总和不超过2000),你的任务是判断Mr. F能否用这些水晶搭建成一座双塔(两座塔有同样的高度),如果能,则输出所能搭建的双塔的最大高度,否则输出“ImpossibleImpossible”。
输入格式
输入的第一行为一个数N,表示水晶的数量。
第二行为N个数,第i个数表示第i个水晶的高度。
输出格式
输出仅包含一行,如果能搭成一座双塔,则输出双塔的最大高度,否则输出一个字符串“Impossible”。
样例数据
input
5
1 3 4 5 2
output
7
数据规模与约定
时间限制:1s
空间限制:256MB
-------------------我还是华丽的分割线-------------------------------
水晶放置在任意一座塔上都会对另一座塔产生影响,故属于双进程问题。
f[i][j]表示取前i块水晶、两塔差为j时较高塔的最大高度。
注意,这里的f[i][j]都是从上一阶段推得的。我们在面对第i块水晶时,它可能是从以下四种决策得来的:
f[i][j]=max(f[i−1][j])f[i][j]=max(f[i−1][j]) . 这块水晶被丢掉了。
f[i][j]=max(f[i−1][j+h[i]])f[i][j]=max(f[i−1][j+h[i]]) . 这块水晶被给了上一个状态中较低的那座塔,且它未超过较高的塔,由图可知较高塔的最大高度是不变的。
f[i[][j]=max(f[i−1][j−h[i]]+h[i])f[i[][j]=max(f[i−1][j−h[i]]+h[i]) .这块水晶被给了上一个状态中较高的塔,由图可知,较高塔的值增加了h[i]h[i]。
当然,此时我们要保证j>h[i]j>h[i]。f[i][j]=max(f[i−1][h[i]−j]+j)f[i][j]=max(f[i−1][h[i]−j]+j) .这块水晶被给了上一阶段较低的塔,且它超过了较高塔。由图可知,较高塔的值增加了jj。
(感谢hh大佬提供思路)。
以下为代码:
#include<bits/stdc++.h> using namespace std; int h[10010]; int f[1010][1010]; int n,sum=0; int main() { freopen("input.in","r",stdin); freopen("output.out","w",stdout); memset(f,-10,sizeof(f)); f[0][0]=0; scanf("%d",&n); for(int i=1;i<=n;i++) { scanf("%d",&h[i]); sum+=h[i]; } for(int i=1;i<=n;i++) for(int j=0;j<=sum;j++)//f[i][j]表示前i个水晶选择完后,落差为j时的最优值 { f[i][j]=max(f[i-1][j],f[i-1][j+h[i]]);//不要水晶和要水晶的最优。 if(j>=h[i]) f[i][j]=max(f[i-1][j-h[i]]+h[i],f[i][j]); else f[i][j]=max(f[i-1][h[i]-j]+j,f[i][j]);//状态转移 } if(f[n][0]) printf("%d ",f[n][0]); else printf("Impossible"); return 0; }
5.树形动规
树形DP例题1
题目描述
给定一棵n个点的无权树,问树中每个节点的深度和每个子树的大小? (以1号点为根节且深度为0)
输入格式
第1行:n。
第2~n行:每行两个数x,y,表示x,y之间有一条边。
输出格式
n行,每行输出格式为:#节点编号 deep:深度 count:子树节点数(详见样例)
样例数据
input
7
1 2
2 3
1 4
3 5
1 6
3 7
output
#1 deep:0 count:7
#2 deep:1 count:4
#3 deep:2 count:3
#4 deep:1 count:1
#5 deep:3 count:1
#6 deep:1 count:1
#7 deep:3 count:1
数据规模与约定
15% n<=10;
40% n<=1000;
100% n<=100000;
------------------------------我又是美美的分割线-------------------------------
基本的树形,建立一个领接表就OK了。
话不多说,直接上代码:
#include<bits/stdc++.h> using namespace std; int n,p; int head[1001000],size[1001000],dep[1001000]; int cnt=0; int x,y; struct node { int to,next; }e[1001000]; void add(int x,int y) { cnt++; e[cnt].to=y; e[cnt].next=head[x]; head[x]=cnt; } void dfs(int x,int fa,int depth) { size[x]=1; dep[x]=depth; for(int i=head[x];i;i=e[i].next) { int v=e[i].to; if(v==fa) continue; dfs(v,x,depth+1); size[x]+=size[v]; } } int main() { freopen("tree.in","r",stdin); freopen("tree.out","w",stdout); memset(dep,0,sizeof(dep)); scanf("%d",&n); for(int i=1;i<=n-1;i++) { scanf("%d%d",&x,&y); add(x,y); add(y,x); } dfs(1,0,0); for(int i=1;i<=n;i++) { printf("#%d deep:%d count:%d ",i,dep[i],size[i]); } return 0; }
五.动态规划的意义:
动态规划程序设计是对解最优化问题的一种途径、一种方法,而不是一种特殊算法。不像搜索或数值计算那样,具有一个标准的数学表达式和明确清晰的解题方法。动态规划程序设计往往是针对一种最优化问题,由于各种问题的性质不同,确定最优解的条件也互不相同,因而动态规划的设计方法对不同的问题,有各具特色的解题方法,而不存在一种万能的动态规划算法,可以解决各类最优化问题。因此读者在学习时,除了要对基本概念和方法正确理解外,必须具体问题具体分析处理,以丰富的想象力去建立模型,用创造性的技巧去求解。我们也可以通过对若干有代表性的问题的动态规划算法进行分析、讨论,逐渐学会并掌握这一设计方法。
好好理解动态规划吧!
!--?XML:NAMESPACE>!--?XML:NAMESPACE>!--?XML:NAMESPACE>
以上是关于DP(动态规划)学习心得的主要内容,如果未能解决你的问题,请参考以下文章