这篇博文其实我是不想、也不敢写的,因为自己还是半知半解,但又怕自己看了很久的东西和做题得来的体会以后给忘了,所以,还是写下了。个人水平确实有限,若有错误的地方,欢迎指出!
参考了《算法导论(原书第3版)》和网上的博客。
一、动态规划的原理
1、动态规划的用处:
动态规划与分治法相似,都是通过组合子问题的解来求解原问题。动态规划通常是用来求解最优化问题(optimization problem).这类问题可以有很多个可行解,每个解都有一个值,我们希望寻找最优值(最大值或者最小值)的解。我们称这样的解为问题的一个最优解(oneoptimization solution)而不是(theoptimization solution)
2、设计动态规划的步骤:
1)刻画一个最优解的结构特征;
2)递归地定义最优解的值;
3)计算最优解的值,通常采用自底向上的方法;
4)利用计算出的线性构造一个最优解。
运用动态规划求解问题的两个要素分别是最优子结构和子问题重叠。
(1)最优子结构
如果一个问题的最优解包含其子问题的最优解,我们就称此问题具有最优子结构性质。因此,某个问题是否适合应用动态规划算法它是否具有最优子结构性质是一个很好的线索(当然,具有最优子结构性质也可能意味着适合应用贪心策略)。 在动态规划中,通常是自底向上地使用最优子结构。即首先求得子问题的最优解,然后在其中进行选择,求原问题的最优解。在求解问题的过程中需在涉及的子问题中做出选择,选择出能得到原问题最优解的子问题。
(2)重叠子问题
在求解过程中如果递归算法反复求解相同的子问题,就称最优化问题具有重叠子问题的性质。动态规划算法通常利用重叠子问题性质的做法是对每个子问题求解一次,将解存入一个表中,当再次需要这个子问题时直接查表,每次查表的代表为常量时间。
无后效性:我们要求状态具有下面的性质:如果给定某一阶段的状态,则在这一阶段以后过程的发展不受这阶段以前各段状态的影响,所有各阶段都确定时,整个过程也就确定了。换句话说,过程的每一次实现可以用一个状态序列表示,在前面的例子中每阶段的状态是该线路的始点,确定了这些点的序列,整个线路也就完全确定。从某一阶段以后的线路开始,当这段的始点给定时,不受以前线路(所通过的点)的影响。状态的这个性质意味着过程的历史只能通过当前的状态去影响它的未来的发展,这个性质称为无后效性。
二、个人理解
解决动态规划问题的关键在于如何拆分问题,靠以下两点:
1、状态的定义
要解决一个问题,首先要明白如何去定义该问题,(个人理解)求解某一个状态的,如最长非降序的子数列中,数列长度为N,求出长度i(i<N)时的最长(LIS)。一般的做法是,明白问题求什么后,先求出若干个 长度下的最长非降子序。
2、状态转移方程
状态和状态之间的关系式,就叫做状态转移方程。也就是怎么根据已经求得的,得到未求得。每个阶段的最优状态可以从之前某个阶段的某个或某些状态直接得到,也就是找相应的规律。这个找规律的过程其实在状态定义的过程中就应该了解,这里需要用一定的DP方程给出。还有相应的边界条件也得注意。
这里引用知乎上王勐的回答中的一些动态规划和别的方法的区别:
每个阶段只有一个状态->递推;
每个阶段的最优状态都是由上一个阶段的最优状态得到的->贪心;
每个阶段的最优状态是由之前所有阶段的状态的组合得到的->搜索;
每个阶段的最优状态可以从之前某个阶段的某个或某些状态直接得到而不管之前这个状态是如何得到的->动态规划。每个阶段的最优状态可以从之前某个阶段的某个或某些状态直接得到这个性质叫做最优子结构;
而不管之前这个状态是如何得到的
这个性质叫做无后效性。
三、例子
1、如果我们有面值为1元、3元和5元的硬币若干枚,如何能用最少的硬币凑够N元?
(1)状态初始化:我们要首先明白使用动态规划时,数组dp[ ]表示的意思。
下面我们分别给出N=0、1、2、3时的情况:
dp[ i ]表示,凑够i 元,所需的最少硬币数
(2)转移方程:
可以看出,一般是,当前状态可以由前一个状态加1得到,但是当可以用大硬币的时候,我们还要比较时,我们还是要根据硬币数的多少来更新当前状态。
(3)代码如下:
1 #include<iostream> 2 #include<vector> 3 4 using namespace std; 5 6 int main() 7 { 8 int n; 9 cin >> n; 10 cout << "输入成功" << endl; 11 12 vector<int> dp(n + 1, 0); 13 int value[] = { 1,3,5 }; 14 15 for (int i = 1; i <= n; i++) 16 { 17 for (int j = 0; j < 3; j++) //3为value[]元素个数 18 { 19 dp[i] = dp[i - 1] + 1; 20 if (value[j] <= i&&dp[i - value[j]] + 1 < dp[i]) 21 dp[i] = dp[i - value[j]] + 1; 22 } 23 } 24 25 cout << dp[n] << endl; 26 return 0; 27 }
2、最长非降序子序列----------给定一个数组,求最大的非降子序列的长度。
还是分两步走:
(1)dp[ i ]表示数组中array[0...i)中,最长的非降子序列的长度,并举例给出相应的长度
(2)寻找前一个或多个状态和当前状态的关系:
1 dp[i]=max{1,dp[j]+1}; 其中,j<i,nums[j]<=nums[i]
代码如下:
1 #include<iostream> 2 #include<vector> 3 4 using namespace std; 5 6 int LIS(const vector<int> &nums); 7 8 int main() 9 { 10 vector<int> nums = { 5,3,4,8,6,7 }; 11 cout<<LIS(nums)<<endl; 12 return 0; 13 } 14 15 int LIS(const vector<int> &nums) 16 { 17 if (nums.empty()) return 0; 18 int len = nums.size(); 19 vector<int> dp(len , 1); 20 for (int i = 0; i < len; i++) 21 { 22 for (int j = 0; j < i; j++) 23 { 24 if (nums[j]<=nums[i]&& dp[j] + 1>dp[i]) 25 dp[i] = dp[j] + 1; 26 } 27 } 28 return dp[len-1]; 29 }
值得注意的是非降序子序列的含义-----子序列中元素不一定得连续。
如:{5,3,4,8,6,7},最长的长度是4,对应的序列是:{3,4,6,7}