什么是动态规划,我们要如何描述它?
动态规划算法通常基于一个递推公式及一个或多个初始状态。 当前子问题的解将由上一次子问题的解推出。使用动态规划来解题只需要多项式时间复杂度, 因此它比回溯法、暴力法等要快许多。
现在让我们通过一个例子来了解一下DP的基本原理。
首先,我们要找到某个状态的最优解,然后在它的帮助下,找到下一个状态的最优解。
能用动态规划求解的问题一般要具有3个性质:
(1)、最优化原理:如果问题的最优解所包含的子问题的解也是最优的,就称为该问题的最优子结构,即满足最优化原理。
(2)、无后效性:即某阶段状态一旦确定,就不受这个状态的以后的决策影响,也就是说,某状态以后的过程不会影响以前的状态,只与当前状态有关。
(3)有重叠子问题:即子问题之间是不独立的,一个子问题在一阶段的决策中肯能多次被用到。(该性质并不是动态规划所必需的条件,但是如果没有该性质,动态规划算法较其他算法没有优势)。
例子1:最少硬币问题
如果我们有面值为1元、3元和5元的硬币若干枚,如何用最少的硬币凑够11元? (表面上这道题可以用贪心算法,但贪心算法无法保证可以求出解,比如1元换成2元的时候)
首先我们思考一个问题,如何用最少的硬币凑够i元(i<11)?
1.当我们遇到一个大问题时,总是习惯把问题的规模变小。 2.这个规模变小后的问题和原来的问题是同质的(规模变小后的问题其实是原问题的子问题)。
于是我们已经得到了d(0)=0, 表示凑够0元最小需要0个硬币。
d(0)=0。
d(1)=d(1-1)+1=d(0)+1=0+1=1。
当i=2时, d(2)=d(2-1)+1=d(1)+1=1+1=2。
d(3)=d(3-1)+1=d(2)+1=2+1=3。
所以d(3)=min{d(3-1)+1, d(3-3)+1}。
我们用d(i)表示状态,那么状态转移方程自然包含d(i), 上文中包含状态d(i)的方程是:d(3)=min{d(3-1)+1, d(3-3)+1}。 它就是状态转移方程,我们要对它抽象一下,
d(i)=min{ d(i-vj)+1 },其中i-vj >=0,vj表示第j个硬币的面值;
代码如下://values[]存放零钱数组,valuekinds零钱种类个数,money待凑钱数,coinsused[]待凑钱的最少零钱个数数组
int makechange(int values[],int valuekinds,int money,int coinsused[])例子2:求出最长非降子序列的长度
一个序列有N个数:A[1],A[2],…,A[N],求出最长非降子序列的长度。 (讲DP基本都会讲到的一个问题LIS:longest increasing subsequence)
正如上面我们讲的,面对这样一个问题,我们首先要定义一个“状态”来代表它的子问题, 并且找到它的解。注意,大部分情况下,某个状态只与它前面出现的状态有关, 而独立于后面的状态。
让我们沿用“入门”一节里那道简单题的思路来一步步找到“状态”和“状态转移方程”。 假如我们考虑求A[1],A[2],…,A[i]的最长非降子序列的长度,其中i
为了方便理解我们是如何找到状态转移方程的,我先把下面的例子提到前面来讲。 如果我们要求的这N个数的序列是:
5,3,4,8,6,7
根据上面找到的状态,我们可以得到:(下文的最长非降子序列都用LIS表示)
OK,分析到这,我觉得状态转移方程已经很明显了,如果我们已经求出了d(1)到d(i-1), 那么d(i)可以用下面的状态转移方程得到:
d(i) = max{1, d(j)+1},其中j,
想要求d(i),就把i前面的各个子序列中, 最后一个数不大于A[i]的序列长度加1,然后取出最大的长度即为d(i)。 当然了,有可能i前面的各个子序列中最后一个数都大于A[i],那么d(i)=1, 即它自身成为一个长度为1的子序列。
#include
using namespace std;
int lis(int A[], int n){
int *d = new int[n];
int len = 1;
for(int i=0; i
for(int j=0; j if(A[j]<=A[i] && d[j]+1>d[i])
d[i] = d[j] + 1;
if(d[i]>len) len = d[i];
}
delete[] d;
return len;
}
int main(){
int A[] = {
5, 3, 4, 8, 6, 7
};
cout<
}
例子3:开心的小明
描述 小明今天很开心,家里购置的新房就要领钥匙了,新房里有一间他自己专用的很宽敞的房间。更让他高兴的是,妈妈昨天对他说:“你的房间需要购买哪些物品,怎么布置,你说了算,只要不超过N 元钱就行”。今天一早小明就开始做预算,但是他想买的东西太多了,肯定会超过妈妈限定的N 元。于是,他把每件物品规定了一个重要度,分为5 等:用整数1~5 表示,第5 等最重要。他还从因特网上查到了每件物品的价格(都是整数元)。他希望在不超过N 元(可以等于N 元)的前提下,使每件物品的价格与重要度的乘积的总和最大。设第j 件物品的价格为v[j],重要度为w[j],共选中了k 件物品,编号依次为j1...jk,则所求的总和为:v[j1]*w[j1]+..+v[jk]*w[jk]请你帮助金明设计一个满足要求的购物单.
例子4:01背包问题
01背包问题,是用来介绍动态规划算法最经典的例子,用子问题定义状态:即c[i][m]表示前i件物品恰放入一个重量为m的背包可以获得的最大价值。则其状态转移方程便是:
c[i][m]=max{c[i-1][m],c[i-1][m-w[i]]+p[i]}
有必要将它详细解释一下:“将前i件物品放入重量为m的背包中”这个子问题,若只考虑第i件物品的策略(放或不放),那么就可以转化为一个只牵扯前i-1件物品的问题。如果不放第i件物品,那么问题就转化为“前i-1件物品放入容量为v的背包中”,价值为c[i-1][m];如果放第i件物品,那么问题就转化为“前i-1件物品放入剩下的重量为m-w[i]的背包中”,此时能获得的最大价值就是c[i-1][m-w[i]]再加上通过放入第i件物品获得的价值p[i]。题目描述:
有编号分别为a,b,c,d,e的五件物品,它们的重量分别是2,2,6,5,4,它们的价值分别是6,3,5,4,6,现在给你个承重为10的背包,如何让背包里装入的物品具有最大的价值总和?
name | weight | value | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 |
a | 2 | 6 | 0 | 6 | 6 | 9 | 9 | 12 | 12 | 15 | 15 | 15 |
b | 2 | 3 | 0 | 3 | 3 | 6 | 6 | 9 | 9 | 9 | 10 | 11 |
c | 6 | 5 | 0 | 0 | 0 | 6 | 6 | 6 | 6 | 6 | 10 | 11 |
d | 5 | 4 | 0 | 0 | 0 | 6 | 6 | 6 | 6 | 6 | 10 | 10 |
e | 4 | 6 | 0 | 0 | 0 | 6 | 6 | 6 | 6 | 6 | 6 | 6 |
int [][] pack01(int m,int n,int w[],int p[]){
//c[i][v]表示前i件物品恰放入一个重量为m的背包可以获得的最大价值
int c[][]= new int[n+1][m+1];
for(int i = 0;i
for(int j = 0;j
//
for(int i = 1;i
//(1)物品i不放入背包中,所以c[i][j]为c[i-1][j]的值
//(2)物品i放入背包中,则背包剩余重量为j-w[i-1],所以c[i][j]为c[i-1][j-w[i-1]]的值加上当前物品i的价值
if(w[i-1]<=j){
if(c[i-1][j]<(c[i-1][j-w[i-1]]+p[i-1]))
c[i][j] = c[i-1][j-w[i-1]]+p[i-1];
else
c[i][j] = c[i-1][j];
}else
c[i][j] = c[i-1][j];
}
}
return c;
}