简单dp总结
简单dp总结
本文是阅读《挑战程序设计第二版》其中关于dp章节所作总结。将简要描述dp的部分知识。
一、dp是什么?
dp在计算机专业学科中全称是动态规划(dynamic programming),指的是我们可以用前面的子状态来推导出后面的状态的一种方式。根据指出的定义,我们便知道,要能使用动态规划要满足几个条件:1、每个子状态都必须是最优的,才能用来推导后面的最优。2、每个状态都不能直接影响后续状态,只能成为后续状态判断的一种依据。3、子问题的重叠性,动态规划将原来具有指数级时间复杂度的搜索算法改进成了具有多项式时间复杂度的算法。
满足的三个条件中,前两个条件都是容易理解的,但是第三个可能有点不具体。下面我举一个例子来解释一下。
给定n个重量和价值为w[i]和v[i]的物品,选出重量总和不超过W的物品,求所能挑选出的最大值。
- 题解:看到题目,我们可以采用最容易想到的方式,挑选出所有选择的可能,然后选择最大值就好。
//i代表现在搜索的是第几个物品,物品顺序受,j代表到目前剩下的能选择的重量。
//返回是在这个重量下能找到的最大值
int rec(int i,int j){
//没有物品了,要结束搜索。
int res=0;
if(i==n){
res=0;
}
//放不下这个物品,继续找下一个
else if(j<a[i){
res=rec(i+1,j);
}
//放得下这个物品,是否要放要考虑一下。
else{
res=max(rec(i+1,j),rec(i+1,j-w[i])+v[i]);
}
return res;
}
- 然后我们来看这个搜索的过程,对于每个物品,都一般而言,会延生放不放两种递归,所以复杂度看起来是O(2^n),但再仔细看一看,rec(i+1,j-w[i])这个搜索过程,如果前面存在不同的物品,使得从起始点到终点(即j1-j2)的过程中,存在长度相同但不同的路径(比如j初始化为7,7-3-1与7-2-2),于是就会导致某一个rec(i,j)的被反复执行调用,浪费了大量资源。
- 出现了反复执行rec(i,j)的过程,这容易让我们联想到,如果我们记录每次rec(i,j)的执行记过,每次递归进入这个函数时,判断这个是否被访问过,如果访问过,就直接返回,就可以节省资源。
int dp[MAX_N+1][MAX_W+1];
//初始化dp(i,j)数组为-1,标记为rec(i,j)没有被访问过。
memset(dp,-1,sizeof(dp));
int rec(int i,int j){
if(dp[i][j]>=0) return dp[i][j];
int res;
if(i==n){
res=0;
}
else if(j<a[i]){
res=rec(i+1,j);
}
else{
res=max(rec(i+1,j),rec(i+1,j-w[i])+v[i]);
}
//获得结果,写入dp数组
dp[i][j]=res;
return res;
}
- 上述这种搜索就叫做记忆化搜索。通过这种搜索,可以大幅度减少重复浪费。不知道你有没有注意到,每个函数返回的结果都记录在dp数组中,我们可以访问dp数组直接得到结果。那有没有一种方法,可以绕过递归,直接求得dp数组的结果呢?
二、dp在解题中的使用
在上面的例子中,我们其实可以再探讨一下:1、作为前面搜索的状态的结果,是否有直接影响到后续状态?2、每次搜索得到的结果是不是最优的?3、是否有重复的子结构?(用来判断dp是否合算。)
根据上面的表述,答案是比较明显的。作为前面的搜索状态,实际上只是提供了给后面搜索状态的一种决策信息,并没有直接影响。其次我们每次返回的都是当前的最优结果。最后,通过上面的举例,我们知道有很多重复性的搜索过程。这就意味着我们可以使用dp来解题,而且是比较合算的。
dp解题的模型:
(1)确定问题的决策对象。 (2)对决策过程划分阶段。 (3)对各阶段确定状态变量。 (4)根据状态变量确定费用函数和目标函数。 (5)建立各阶段状态变量的转移过程,确定状态转移方程。
对于这题,要求的是在不超过W重量时的最大价值,所以决策对象就是重量和价值。又因为每一个物体都是独立的,于是可以将每一次挑选一个物品作为一个阶段。接下来,设i表示所在的阶段(对于本题即是在挑选的第几个物品时),j表示当前对应的重量的总值,其f(i,j)代表在挑选第i个物品时,总重量为j的价值的最大值。
于是根据前面描述的,思考每一次挑选时,只有两个状态,这个物品选不选,于是得到状态方程 dp[i][j]=max(dp[i-1][j],dp[i-1][j-w[i]]+v[i])。写代码时要注意初始化。
int dp[max_N+1][max_W+1];
//初始化
memset(dp,0,sizeof(dp));
void solve(){
for(int i=1;i<=n;i++)
for(int j=0;j<=W;j++){
//放不下第i个物品
if(j<w[i]){
dp[i][j]=dp[i-1][j];
}
else{
dp[i][j]=max(dp[i-1][j]+dp[i-1][j-w[i]]+v[i]);
}
}
}
这题实际上就是dp中著名的0-1背包问题。如果我们将二维数组展开,可以看到我们的依赖关系是上,上左依赖(dp[i+k][j]依赖于dp[i][j]为上依赖,dp[i][j+k]依赖于dp[i][j]成为左依赖),而且只依赖最近的一行数组,可以对程序再进行优化,想象成包含dp[i][j]的左部投影到dp[i+1][j]中,为了维持依赖性,即只有 dp[i][j]更新完后,才能修改dp[i-1][j]和dp[i-1][j-w[i]],所以可以从右往左更新,这样就能保持依赖的正确性了。
int dp[max_W+1];
memset(dp,0,sizeof(dp));
void solve(){
for(int i=1;i<=n;i++)
for(int j=W;j>=w[i];j--){
dp[j]=max(dp[j],dp[j-w[i]]+v[i]);
}
}
实际上,这一题可以看到,只使用最近的两行数组,其他的不用,那么我们可以用滚动数组来实现
dp[i&1][j]=max(dp[(i-1)&1][j]+dp[(i-1)&1][j-w[i]]+v[i]);
三、背包问题
完全背包问题:有n种重量和价值分别为wi和vi的物品,从中挑选总重量不超过W的物品,求出挑选物体价值总和的最大值。在这里,每种物品可以选择任意多件。
这一题,按照dp问题的解题模型,先确定决策对象,是重量和价格之间的对应关系,因为每一种物体之间都是独立的,所以可以将不同阶段对应于每一种挑选的物品,于是可以定义dp[i][j]为选择到第i种物品时,剩余j重量下的最大值。接着可以写出状态转移方程:
dp[i][j]=max(dp[i-1][j-k * w[i]]+k *v[i],k=0,1,2...,j/w[i)。
上面的转移方程可以做以下变形:
dp[i][j]=max(dp[i-1][j],dp[i-1][j-k *w[i]]+k *v[i],k=1,2,...,j/w[i])。
dp[i][j]=max(dp[i-1][j],dp[i-1][j-w[i]-(k-1) * w[i]]+(k-1) * v[i]+v[i],k=1,2,...,j/w[i])
令k-1=p,则dp[i][j]=max(dp[i-1][j],max(dp[i-1][j-w[i]-p *w[i]]+p *v[i]+v[i]),p=0,1,2...,(j-w[i])/w[i])
于是可以化简得到:
dp[i][j]=max(dp[i-1][j],dp[i][j-w[i]]+v[i])。
根据01背包的经验,可以看到我们的方程是上依赖和左依赖,所以可以将“上”的投影下来,又因为依赖于左边,所以可以从左到右更新。
/*
*写dp时很重要的是要注意初始化。
*/
memset(dp,0,sizeof(dp));
int dp[W_max+1];
for(int i=1;i<=n;i++)
for(int j=w[i];j<=W;j++){
dp[i][j]=max(dp[i-1][j],dp[i][j-w[i]]+v[i]);
}
前面讲了01背包和完全背包,接下来再介绍01背包的变形。设有n个重量和价值分别为wi,vi的物品,在这些物品中挑选总重量不大于W的物品,求挑选方案中的最大值。n<=100,wi<=10^7, v[i]<=100,W<=10^9。
这题与前面01背包所不同的是,约束范围变了,如果继续定义dp[i][j]为挑选第i个物品时剩余重量为j时的最大价值,很容易发现,会超时。
其实,可以把背包问题描述成,在n个有两个属性<cost,value>物体中,找出在总cost花费条件限制内,特定的value值,我们可以把cost理解成背包问题中的重量或者价值。
这就可以引出我们对于这题的解法:决策属性依然是重量和价值,但我们可以把问题转变成下面所描述的:
dp[i][j]表示在挑选第i个物品时,总价值为j时的最小重量。于是dp[i][j]=min(dp[i-1][j],dp[i-1][j-v[i]]+w[i])。
/*
*本题在初始化时,显然每一个dp[i]在未放入任何东西时,为INF,即无穷大
*/
int dp[VALUE_max];
for(int i=1;i<=n;i++)
for(int j=sum_value;j>=v[i];j--){
dp[j]=min(dp[j],dp[j-v[i]]+w[i]);
}
四、更多dp问题
最长公共子序列问题:给定两个字符串s1s2...sn和t1t2...tm。求出这两个字符串最长的公共子序列的长度。
对于这道题,决策对象为字符串中的字符,以及长度。可以把每一个字符都看成是独立的,所以,可以按字符来划分阶段,设dp[i][j]表示s1-si,t1-ti时的最长公共子序列。
因为dp[i][j]只能从三个状态转移得到,即,dp[i-1][j-1]、dp[i-1][j]、dp[i][j-1],可以清楚的得到转移方程:
如果si=tj dp[i][j]=dp[i-1][j-1]+1 如果si!=tj dp[i][j]=max(dp[i-1][j],dp[i][j-1])
/*
*很显然在没有挑选任何一个字符时,dp[][0]=0,dp[0][]=0;
*/
int dp[N_max+1][M_max+1];
memset(dp,0,sizeof(dp));
for(int i=1;i<=n;i++)
for(int j=1;j<=m;j++){
if(s[i]==t[j]){
dp[i][j]=dp[i-1][j-1]+1;
}
else{
dp[i][j]=max(dp[i-1][j],dp[i][j-1]);
}
}
最长上升子序列问题:给定数列a1,a2,...,an,求其最长上升子序列。
设dp[i]为长度为i时的子序列的末尾最小值。可以得到转移方程:dp[i]=min(aj,dp[i]);
/*
*lower_bound(dp,dp+n,a[i])返回在有序dp数组中,第一个大于等于a[i]的地址。
*/
int dp[N_max+1];
memset(dp,0,sizeof(dp));
for(int i=1;i<=n;i++){
*lower_bound(dp,dp+n,a[i])=a[i];
}
多重部分和问题:有n种大小不同的数字ai,每种各mi个。判断是否可以从这些数字中选出若干使它们的和为K。
定义dp[i][j]为在挑选前i种物品达到j时ai的剩余量。于是,如果dp[i-1][j]>=0,证明这个数已经达到了,所以dp[i][j]=mi;如果dp[i][j-ai]<=0或者j< ai,证明不可达到,则dp[i][j]=-1;在其他条件下,dp[i][j]=dp[i][j-ai]-1;
显然也可以看到,状态转移方程是上依赖和左依赖,可以压缩成一维。
int dp[K_max+1];
memset(dp,-1,sizeof(dp));
for(int i=1;i<=n;i++)
for(int j=0;j<=K;j++){
if(dp[j]>=0){
dp[j]=m[i];
}
else if(j<a[i]||dp[j-a[i]]<=0){
dp[j]=-1;
}
else{
dp[j]=dp[j-a[i]]-1;
}
}
划分数:有n个无区别的物品,将它们划分成不超过m组,求划分方法数模M的余数。
定义dp[i][j]为前i个物品的j划分的划分数。接下来考察j种划分,如果存在一种划分为0,则为dp[i][j-1],或者每种划分都大于0,如果每种划分都大于0,则将每种划分减1;
于是dp[i][j]=dp[i][j-1]+dp[i-j][j];
int dp[M_max+1][N_maxn+1];
dp[0][0]=1;
for(int i=0;i<=n;i++)
for(int j=1;j<=n;j++){
if(i-j>=0){
dp[i][j]=(dp[i][j-1]+dp[i-j][j])%M;
}
else{
dp[i][j]=dp[i][j-1];
}
}
多重集组合数:有n种物品,第i种物品有ai个。不同种类的物品可以互相区分但相同种类无法区分。从这些物体中取出m个的话,有多少种取法?求方案数模M的余数。
设dp[i][j]为前i个物品取出j个的方法数:dp[i][j]=∑dp[i-1][j-k] 其中k=0,1,...,min(ai,j)
可以仿照前面完全背包的推导方式,考虑是j-1还是ai起作用,可以得到:dp[i][j]=dp[i-1][j]+∑dp[i-1][j-1-p]-dp[i-1][j-1-ai] p=0,1,...,min(j-1,ai)。
所以dp[i][j]=dp[i-1][j]+dp[i][j-1]-dp[i-1][j-1-ai];
int a[MAX_N];
int dp[MAX_N+1][MAX_M+1];
for(int i=0;i<=n;i++)
dp[i][0]=1;
for(int i=1;i<=n;i++)
for(int j=1;j<=m;j++){
if(j-1-a[i]>=0){
dp[i][j]=(dp[i-1][j]+dp[i][j-1]-dp[i-1][j-1-a[i]])%M;
}
else{
dp[i][j]=(dp[i-1][j]+dp[i][j-1])%M;
}
}
五、summary
看完前面的这些基础的题目,可以感受得到,一种好的状态的定义,将能带来好的状态转移方程的书写,以及对复杂度的大量降低。但是还是面临一个问题,如何选择好一个好的状态定义?我记得在以前看过的文章中,写过一句话,其实大部分问题都确实有动态规划的解法,是否能用动态规划解题,取决于你对问题的描述与理解。
最后再说一句,但行好事,莫问前程。
简单dp总结的更多相关文章
- HDU 1087 简单dp,求递增子序列使和最大
Super Jumping! Jumping! Jumping! Time Limit: 2000/1000 MS (Java/Others) Memory Limit: 65536/32768 ...
- Codeforces Round #260 (Div. 1) A. Boredom (简单dp)
题目链接:http://codeforces.com/problemset/problem/455/A 给你n个数,要是其中取一个大小为x的数,那x+1和x-1都不能取了,问你最后取完最大的和是多少. ...
- codeforces Gym 100500H A. Potion of Immortality 简单DP
Problem H. ICPC QuestTime Limit: 20 Sec Memory Limit: 256 MB 题目连接 http://codeforces.com/gym/100500/a ...
- 简单dp --- HDU1248寒冰王座
题目链接 这道题也是简单dp里面的一种经典类型,递推式就是dp[i] = min(dp[i-150], dp[i-200], dp[i-350]) 代码如下: #include<iostream ...
- poj2385 简单DP
J - 简单dp Crawling in process... Crawling failed Time Limit:1000MS Memory Limit:65536KB 64bit ...
- hdu1087 简单DP
I - 简单dp 例题扩展 Crawling in process... Crawling failed Time Limit:1000MS Memory Limit:32768KB ...
- poj 1157 LITTLE SHOP_简单dp
题意:给你n种花,m个盆,花盆是有顺序的,每种花只能插一个花盘i,下一种花的只能插i<j的花盘,现在给出价值,求最大价值 简单dp #include <iostream> #incl ...
- hdu 2471 简单DP
题目链接:http://acm.hdu.edu.cn/showproblem.php?pid=2571 简单dp, dp[n][m] +=( dp[n-1][m],dp[n][m-1],d[i][k ...
- Codeforces 41D Pawn 简单dp
题目链接:点击打开链接 给定n*m 的矩阵 常数k 以下一个n*m的矩阵,每一个位置由 0-9的一个整数表示 问: 从最后一行開始向上走到第一行使得路径上的和 % (k+1) == 0 每一个格子仅仅 ...
- poj1189 简单dp
http://poj.org/problem?id=1189 Description 有一个三角形木板,竖直立放.上面钉着n(n+1)/2颗钉子,还有(n+1)个格子(当n=5时如图1).每颗钉子和周 ...
随机推荐
- c# 任务超时执行组件
最近整理下各类框架,学习一下欠缺的东西.因为前一年开发过java服务端,知道java有很多开源框架,但是毕竟起来也很累. 现在转回头从新审视c#,很基础,没有开源框架,因为以前它不开源,所以少,不用比 ...
- GitHub的搭建,使用
Git是一个分布式的版本控制系统,最初由Linus Torvalds编写,用作Linux内核代码的管理.在推出后,Git在其它项目中也取得了很大成功,尤其是在Ruby社区中.目前,包括Rubinius ...
- [HAOI2007]上升序列(最长上升子序列)
题目描述 对于一个给定的 S=\{a_1,a_2,a_3,…,a_n\}S={a1,a2,a3,…,an} ,若有 P=\{a_{x_1},a_{x_2},a_{x_3},…,a_{x_m}\ ...
- CF练习记录
2018/5/6 Codeforces Round #478 (Div. 2) C http://codeforces.com/contest/975/problem/C Valhalla Siege ...
- js 关于字符串转数字及数字保留位数的控制
1.parseInt()和parseFloat()两个转换函数,将字符串转换成相应的数字. 1.parseInt() parseInt进行转换时,将字符串转成相应的整数.浮点数以后的数字都不要了. p ...
- 【php练习源码】
Something is wrong with the XAMPP installation :-( value[$name]=$sex; } public function getInfomatio ...
- Apache和Nignx基于三种方式搭建web站点并设置用户访问控制达到优化整个站点性能
个人用户主页: 1:Vim /etc/http/con.d/userdir: UserDir disabled //个人用户主页开启 UserDir public_html //指定 ...
- spring-运行时值注入
在项目中经常使用连接数据库的配置,如下所示 <bean id="dataSource" class="org.apache.commons.dbcp.BasicDa ...
- I/O流、文件操作
1)操作文件 Path和Files是在JavaSE7中新添加进来的类,它们封装了在用户机器上处理文件系统所需的所有功能.Path表示的一个目录名序列,其后还可以跟着一个文件名.路径中的第一个参数可以是 ...
- Java小功能大杂烩
生成UUID: import java.util.UUID; public class ProductUUID { // 随机返回前十位的UUID public static String getUU ...