一、基本概念
动态规划过程是:每次决策依赖于当前状态,又随即引起状态的转移。一个决策序列就是在变化的状态中产生出来的,所以,这种多阶段最优化决策解决问题的过程就称为动态规划。
二、基本思想与策略
基本思想与分治法类似,也是将待求解的问题分解为若干个子问题(阶段),按顺序求解子阶段,前一子问题的解,为后一子问题的求解提供了有用的信息。在求解任一子问题时,列出各种可能的局部解,通过决策保留那些有可能达到最优的局部解,丢弃其他局部解。依次解决各子问题,最后一个子问题就是初始问题的解。
由于动态规划解决的问题多数有重叠子问题这个特点,为减少重复计算,对每一个子问题只解一次,将其不同阶段的不同状态保存在一个二维数组中。
与分治法最大的差别是:适合于用动态规划法求解的问题,经分解后得到的子问题往往不是互相独立的(即下一个子阶段的求解是建立在上一个子阶段的解的基础上,进行进一步的求解)。
三、适用的情况
能采用动态规划求解的问题的一般要具有3个性质:
(1) 最优化原理:如果问题的最优解所包含的子问题的解也是最优的,就称该问题具有最优子结构,即满足最优化原理。
(2) 无后效性:即某阶段状态一旦确定,就不受这个状态以后决策的影响。也就是说,某状态以后的过程不会影响以前的状态,只与当前状态有关。
(3)有重叠子问题:即子问题之间是不独立的,一个子问题在下一阶段决策中可能被多次使用到。(该性质并不是动态规划适用的必要条件,但是如果没有这条性质,动态规划算法同其他算法相比就不具备优势)
四、求解的基本步骤
动态规划所处理的问题是一个多阶段决策问题,一般由初始状态开始,通过对中间阶段决策的选择,达到结束状态。这些决策形成了一个决策序列,同时确定了完成整个过程的一条活动路线(通常是求最优的活动路线)。如图所示。动态规划的设计都有着一定的模式,一般要经历以下几个步骤。
初始状态→│决策1│→│决策2│→…→│决策n│→结束状态
图1 动态规划决策过程示意图
(1)划分阶段:按照问题的时间或空间特征,把问题分为若干个阶段。在划分阶段时,注意划分后的阶段一定要是有序的或者是可排序的,否则问题就无法求解。
(2)确定状态和状态变量:将问题发展到各个阶段时所处于的各种客观情况用不同的状态表示出来。当然,状态的选择要满足无后效性。
(3)确定决策并写出状态转移方程:因为决策和状态转移有着天然的联系,状态转移就是根据上一阶段的状态和决策来导出本阶段的状态。所以如果确定了决策,状态转移方程也就可写出。但事实上常常是反过来做,根据相邻两个阶段的状态之间的关系来确定决策方法和状态转移方程。
(4)寻找边界条件:给出的状态转移方程是一个递推式,需要一个递推的终止条件或边界条件。
一般,只要解决问题的阶段、状态和状态转移决策确定了,就可以写出状态转移方程(包括边界条件)。
实际应用中可以按以下几个简化的步骤进行设计:
(1)分析最优解的性质,并刻画其结构特征。
(2)递归的定义最优解。
(3)以自底向上或自顶向下的记忆化方式(备忘录法)计算出最优值
(4)根据计算最优值时得到的信息,构造问题的最优解
五、算法实现的说明
动态规划的主要难点在于理论上的设计,也就是上面4个步骤的确定,一旦设计完成,实现部分就会非常简单。
使用动态规划求解问题,最重要的就是确定动态规划三要素:
(1)问题的阶段 (2)每个阶段的状态
(3)从前一个阶段转化到后一个阶段之间的递推关系。
递推关系必须是从次小的问题开始到较大的问题之间的转化,从这个角度来说,动态规划往往可以用递归程序来实现,不过因为递推可以充分利用前面保存的子问题的解来减少重复计算,所以对于大规模问题来说,有递归不可比拟的优势,这也是动态规划算法的核心之处。
确定了动态规划的这三要素,整个求解过程就可以用一个最优决策表来描述,最优决策表是一个二维表,其中行表示决策的阶段,列表示问题状态,表格需要填写的数据一般对应此问题的在某个阶段某个状态下的最优值(如最短路径,最长公共子序列,最大价值等),填表的过程就是根据递推关系,从1行1列开始,以行或者列优先的顺序,依次填写表格,最后根据整个表格的数据通过简单的取舍或者运算求得问题的最优解。
f(n,m)=max{f(n-1,m), f(n-1,m-w[n])+P(n,m)}
六、动态规划算法基本框架
几个经典的动态规划算法
https://blog.csdn.net/a20180825/article/details/76718417
一、动态规划基本思想
一般来说,只要问题可以划分成规模更小的子问题,并且原问题的最优解中包含了子问题的最优解,则可以考虑用动态规划解决。动态规划的实质是分治思想和解决冗余,因此,动态规划是一种将问题实例分解为更小的、相似的子问题,并存储子问题的解而避免计算重复的子问题,以解决最优化问题的算法策略。由此可知,动态规划法与分治法和贪心法类似,它们都是将问题实例归纳为更小的、相似的子问题,并通过求解子问题产生一个全局最优解。其中贪心法的当前选择可能要依赖已经作出的所有选择,但不依赖于有待于做出的选择和子问题。因此贪心法自顶向下,一步一步地作出贪心选择;而分治法中的各个子问题是独立的 (即不包含公共的子子问题),因此一旦递归地求出各子问题的解后,便可自下而上地将子问题的解合并成问题的解。但不足的是,如果当前选择可能要依赖子问题的解时,则难以通过局部的贪心策略达到全局最优解;如果各子问题是不独立的,则分治法要做许多不必要的工作,重复地解公共的子问题。解决上述问题的办法是利用动态规划。该方法主要应用于最优化问题,这类问题会有多种可能的解,每个解都有一个值,而动态规划找出其中最优(最大或最小)值的解。若存在若干个取最优值的解的话,它只取其中的一个。在求解过程中,该方法也是通过求解局部子问题的解达到全局最优解,但与分治法和贪心法不同的是,动态规划允许这些子问题不独立,也允许其通过自身子问题的解作出选择,该方法对每一个子问题只解一次,并将结果保存起来,避免每次碰到时都要重复计算。因此,动态规划法所针对的问题有一个显著的特征,即它所对应的子问题树中的子问题呈现大量的重复。动态规划法的关键就在于,对于重复出现的子问题,只在第一次遇到时加以求解,并把答案保存起来,让以后再遇到时直接引用,不必重新求解。
中的子问题呈现大量的重复。
动态规划法的关键就在于,
对于重复出现的子问题,
只在第一次遇到时加以求解,
并把答案保存起来,
让以后再遇到时直接引用,
不
必重新求解。
中的子问题呈现大量的重复。
动态规划法的关键就在于,
对于重复出现的子问题,
只在第一次遇到时加以求解,
并把答案保存起来,
让以后再遇到时直接引用,
不
必重新求解。
中的子问题呈现大量的重复。
动态规划法的关键就在于,
对于重复出现的子问题,
只在第一次遇到时加以求解,
并把答案保存起来,
让以后再遇到时直接引用,
不
必重新求解。
二、动态规划实例
1、0-1背包问题
问题描述
假设我们有n件物品,分别编号为1,2,…n。其中编号为i的物品价值Vi,它的重量为Wi。为了简化问题,假设价值和重量都是整数。现在假设我们有一个背包,它能够承载的重量是W。现在,我们希望王背包里装这些物品,使得包里装的物品价值最大化,那么我们该如何选择装的物品?问题结构如下图所示:
初步分析
对于这个问题,一开始确实有点不太好入手。一堆的物品,每一个都有一定的质量和价值,我们能够装入的总重量有限制,该怎么来装使得价值最大呢?对于这n个物品,每个物品我们可能会选,也可能不选,那么我们总共就可能有2^n种组合选择方式。如果我们采用这种办法来硬算的话,则整体的时间复杂度就达到指数级别的,肯定不可行。现在我们换一种思路。既然每一种物品都有价格和重量,我们优先挑选那些单位价格最高的是否可行呢?比如在下图中,我们有3种物品,他们的重量和价格分别是10, 20, 30 kg和60, 100, 120
那么按照单位价格来算的话,我们最先应该挑选的是价格为60的元素,选择它之后,背包还剩下50 - 10 = 40kg。再继续前面的选择,我们应该挑选价格为100的元素,这样背包里的总价值为60 + 100 = 160。所占用的重量为30, 剩下20kg。因为后面需要挑选的物品为30kg已经超出背包的容量了。我们按照这种思路能选择到的最多就是前面两个物品。如下图:
按照我们前面的期望,这样选择得到的价值应该是最大的。可是由于有一个背包重量的限制,这里只用了30kg,还有剩下20kg浪费了。这会是最优的选择吗?我们看看所有的选择情况:
很遗憾,在这几种选择情况中,我们前面的选择反而是带来价值最低的。而选择重量分别为20kg和30kg的物品带来了最大的价值。看来,我们刚才这种选择最佳单位价格的方式也行不通。
动态规划解决问题
既然前面两种办法都不可行,我们再来看看有没有别的方法。我们再来看这个问题。我们需要选择n个元素中的若干个来形成最优解,假定为k个。那么对于这k个元素a1, a2, …ak来说,它们组成的物品组合必然满足总重量<=背包重量限制,而且它们的价值必然是最大的。因为它们是我们假定的最优选择嘛,肯定价值应该是最大的。假定ak是我们按照前面顺序放入的最后一个物品。它的重量为wk,它的价值为vk。既然我们前面选择的这k个元素构成了最优选择,如果我们把这个ak物品拿走,对应于k-1个物品来说,它们所涵盖的重量范围为0-(W-wk)。假定W为背包允许承重的量。假定最终的价值是V,剩下的物品所构成的价值为V-vk。这剩下的k-1个元素是不是构成了一个这种W-wk的最优解呢?
我们可以用反证法来推导。假定拿走ak这个物品后,剩下的这些物品没有构成W-wk重量范围的最佳价值选择。那么我们肯定有另外k-1个元素,他们在W-wk重量范围内构成的价值更大。如果这样的话,我们用这k-1个物品再加上第k个,他们构成的最终W重量范围内的价值就是最优的。这岂不是和我们前面假设的k个元素构成最佳矛盾了吗?所以我们可以肯定,在这k个元素里拿掉最后那个元素,前面剩下的元素依然构成一个最佳解。
现在我们经过前面的推理已经得到了一个基本的递推关系,就是一个最优解的子解集也是最优的。可是,我们该怎么来求得这个最优解呢?我们这样来看。假定我们定义一个函数c[i, w]表示到第i个元素为止,在限制总重量为w的情况下我们所能选择到的最优解。那么这个最优解要么包含有i这个物品,要么不包含,肯定是这两种情况中的一种。如果我们选择了第i个物品,那么实际上这个最优解是c[i - 1, w-wi] + vi。而如果我们没有选择第i个物品,这个最优解是c[i-1, w]。这样,实际上对于到底要不要取第i个物品,我们只要比较这两种情况,哪个的结果值更大不就是最优的么?
在前面讨论的关系里,还有一个情况我们需要考虑的就是,我们这个最优解是基于选择物品i时总重量还是在w范围内的,如果超出了呢?我们肯定不能选择它,这就和c[i-1, w]一样。
另外,对于初始的情况呢?很明显c[0, w]里不管w是多少,肯定为0。因为它表示我们一个物品都不选择的情况。c[i, 0]也一样,当我们总重量限制为0时,肯定价值为0。
这样,基于我们前面讨论的这3个部分,我们可以得到一个如下的递推公式:
有了这个关系,我们可以更进一步的来考虑代码实现了。我们有这么一个递归的关系,其中,后面的函数结果其实是依赖于前面的结果的。我们只要按照前面求出来最基础的最优条件,然后往后面一步步递推,就可以找到结果了。
我们再来考虑一下具体实现的细节。这一组物品分别有价值和重量,我们可以定义两个数组int[] v, int[] w。v[i]表示第i个物品的价值,w[i]表示第i个物品的重量。为了表示c[i, w],我们可以使用一个int[i][w]的矩阵。其中i的最大值为物品的数量,而W表示最大的重量限制。按照前面的递推关系,c[i][0]和c[0][w]都是0。而我们所要求的最终结果是c[n][w]。所以我们实际中创建的矩阵是(n + 1) x (W + 1)的规格。
具体数据:物品个数n = 5,物品重量w[n] = {0,2,2,6,5,4},物品价值v[n] = {0,6,3,5,4,6},W = 10。
代码实现
- #include <iostream>
- #define max(a,b)((a)>(b)?a:b)
- using namespace std;
- int Weight[] = {0,2,2,6,5,4}; //物品的重量
- int Value[] = {0,6,3,5,4,6}; //物品的价值
- int table[6][11]; //存放表示到第i个元素为止,在限制总重量为w的情况下我们所能选择到的最优解
- bool Frist_Flag = true; //主要是初始化table[][] = Value[1]
- int main(void)
- {
- for (int i = 1;i <= 5;i++)
- {
- for (int j = 1;j <=10;j++)
- {
- if (Frist_Flag == true)
- {
- if (Weight[1] <= j)
- {
- table[i][j] = Value[1];
- Frist_Flag = false;
- }
- }
- else
- {
- if (Weight[i] > j)
- {
- table[i][j] = table[i-1][j];
- }
- else
- {
- table[i][j] = max(table[i-1][j-Weight[i]]+Value[i],table[i-1][j]);
- }
- }
- }
- }
- cout << ”MaxValue = ”<< table[5][10] << endl;
- return 0;
- }
#include <iostream>
- 1
- 2
- 3
- 4
- 5
- 6
2、最大子数组和问题
问题
一个有N个整数元素的一维数组(A[0],A[1],…A[N-1]),这个数组有很多子数组,求子数组和的最大值?注意:子数组必须是连续的、不需要返回子数组的具体位置、数组中包含:正、负、零整数、子数组不能空。
例如:
int A[5] = {-1,2,3,-4,2};
符合条件的子数组为2,3,即答案为5;
穷举法:
- int MaxSubStringSum(int *A,int n)
- {
- int MaxSum = A[0];
- int sum = 0;
- for (int i = 0;i < n;i++)
- {
- sum = 0;
- for (int j = i;j < n;j++)
- {
- sum += A[j];
- MaxSum = max(MaxSum,sum);
- }
- }
- return MaxSum;
- }
int MaxSubStringSum(int *A,int n)
{
int MaxSum = A[0];
int sum = 0;
for (int i = 0;i < n;i++)
{
sum = 0;
for (int j = i;j < n;j++)
{
sum += A[j];
MaxSum = max(MaxSum,sum);
}
}
return MaxSum;
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
穷取法最为直接,当然耗时也较多,时间复杂度为O(n^2);
进一步分析
我们利用穷举法虽然简单易懂,但是其时间复杂度很大,我们试着优化。现在考虑数组的第一个元素A[0],与和最大的子数组(A[i],……A[j])之间的关系,有以下三种关系:
1) i = j = 0;A[0]本身构成和最大的子数组
2) j > i = 0;和最大的子数组以A[0]开头
3) i > 0;A[0]与和最大子数组没有关系
从上面3中情况可以看出,可以将一个大问题(N个元素的数组)转化为一个较小的问题(N - 1个元素的数组)。假设已经知道(A[1],……A[N-1])中和最大的子数组和为MaxSum[1],并且知道,(A[1],……A[N-1])中包含A[1]的和最大的子数组为TempMaxSum[1]。我们就可以把(A[0],……A[N-1])求和最大子数组问题转换为,MaxSum[0] = max{A[0],A[0]+TempMaxSum[1],MaxSum[1]}。
代码实现:
- int MaxSubStringSum(int *A,int n)
- {
- int MaxSum = A[0];
- int TempMaxSum = A[0];
- for(int i = 1;i < n;i++)
- {
- TempMaxSum = max(A[i],TempMaxSum + A[i]);
- MaxSum = max(MaxSum,TempMaxSum);
- }
- return MaxSum;
- }
int MaxSubStringSum(int *A,int n)
{
int MaxSum = A[0];
int TempMaxSum = A[0];
for(int i = 1;i < n;i++)
{
TempMaxSum = max(A[i],TempMaxSum + A[i]);
MaxSum = max(MaxSum,TempMaxSum);
}
return MaxSum;
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11