动态规划
动态规划(英语:Dynamic programming,简称 DP)是一种在数学、管理科学、计算机科学、经济学和生物信息学中使用的,通过把原问题分解为相对简单的子问题的方式求解复杂问题的方法。
动态规划常常适用于有重叠子问题和最优子结构性质的问题,动态规划方法所耗时间往往远少于朴素解法。
动态规划背后的基本思想非常简单。大致上,若要解一个给定问题,我们需要解其不同部分(即子问题),再根据子问题的解以得出原问题的解。动态规划往往用于优化递归问题,例如斐波那契数列,如果运用递归的方式来求解会重复计算很多相同的子问题,利用动态规划的思想可以减少计算量。
通常许多子问题非常相似,为此动态规划法试图仅仅解决每个子问题一次,具有天然剪枝的功能,从而减少计算量:一旦某个给定子问题的解已经算出,则将其记忆化存储,以便下次需要同一个子问题解之时直接查表。这种做法在重复子问题的数目关于输入的规模呈指数增长时特别有用。
解题步骤
一、确定状态
- 状态在动态规划中的作用属于定海神针
- 简单地说,解动态规划的时候需要开一个数组,数组的每个元素f[i]或f[i][j]含义——类似于解数学题中X,Y,Z代表什么。
- 确定状态需要两个意识:最后一步和子问题
二、转移方程
根据最后一步和子问题得到递推关系,进而得出转移方程。
三、初始条件和边界情况
dp数组的初值和结束边界很重要,很多新手容易在初值设置环节犯错,即赋错值。
四、确定计算顺序
即确定循环计算的方式。
下面我们以LeetCode上的简单动态规划题的顺序开始进行学习
LeetCode53. 最大子序和 && 面试题42
给定一个整数数组 nums
,找到一个具有最大和的连续子数组(子数组最少包含一个元素),返回其最大和。
示例:
输入: [-2,1,-3,4,-1,2,1,-5,4],
输出: 6
解释: 连续子数组 [4,-1,2,1] 的和最大,为 6。
由于求的是最大子序列和,那么在增加最后一个目标元素nums[i]之前,我们需要取记录之前最大序列和的dp[i - 1]加上当前nums[i]的值与当前nums[i]的最大值,即确立状态转移方程dp[i] = max(dp[i - 1] + nums[i], nums[i]),然后更新序列和的最大值。
class Solution {
public:
int maxSubArray(vector<int>& nums) {
int n = nums.size();
vector<int> dp(n, nums[0]);
int res = nums[0];
for(int i = 1; i < n; i++){
dp[i] = max(dp[i - 1] + nums[i], nums[i]);
res = max(dp[i], res);
}
return res;
}
};
LeetCode300. 最长上升子序列
给定一个无序的整数数组,找到其中最长上升子序列的长度。
示例:
输入: [10,9,2,5,3,7,101,18]
输出: 4
解释: 最长的上升子序列是 [2,3,7,101],它的长度是 4。
思路:定义 dp[i] 为考虑前 i 个元素,以第 i 个数字结尾的最长上升子序列的长度,注意 nums[i]必须被选取。
我们从小到大计算 dp[] 数组的值,在计算 dp[i] 之前,我们已经计算出 dp[0…i−1] 的值,则状态转移方程为:
dp[i]=max(dp[j])+1,其中 0≤j<i 且 num[j]<num[i]
即考虑往 dp[0…i−1]中最长的上升子序列后面再加一个 nums[i]。由于 dp[j] 代表 nums[0…j] 中以 nums[j] 结尾的最长上升子序列,所以如果能从 dp[j] 这个状态转移过来,那么 nums[i] 必然要大于 nums[j],才能将 nums[i] 放在 nums[j] 后面以形成更长的上升子序列。
最后,整个数组的最长上升子序列即所有 dp[i] 中的最大值。
LISlength=max(dp[i]),其中 0≤i<n
class Solution {
public:
int lengthOfLIS(vector<int>& nums) {
if (nums.size() == 0) return 0;
vector<int> dp(nums.size(), 0);
for(int i = 0; i < nums.size(); i++){
dp[i] = 1;
for(int j = 0; j < i; j++){
if(nums[i] > nums[j]) dp[i] = max(dp[i], dp[j] + 1);
}
}
return *max_element(dp.begin(), dp.end());
}
};
LeetCode392. 判断子序列
给定字符串 s 和 t ,判断 s 是否为 t 的子序列。
你可以认为 s 和 t 中仅包含英文小写字母。字符串 t 可能会很长(长度 ~= 500,000),而 s 是个短字符串(长度 <=100)。
字符串的一个子序列是原始字符串删除一些(也可以不删除)字符而不改变剩余字符相对位置形成的新字符串。(例如,"ace"是"abcde"的一个子序列,而"aec"不是)。
示例 1:
s = "abc", t = "ahbgdc"
返回 true.
示例 2:
s = "axc", t = "ahbgdc"
返回 false.
此题可以不用动态规划来写,当字母匹配时,使用index来记录匹配个数,直到index达到子序列s的长度,则匹配成功。
class Solution {
public:
bool isSubsequence(string s, string t) {
//普通方法
int index = 0;
if(s.empty()) return true;
for(int i = 0; i < t.size(); i++){
if(t[i] == s[index]){
index++;
if(index >= s.size())
return true;
}
}
return false;
}
};
此题若写成动态规划,由于是两个字符串的问题,一般都是二维dp:
状态:s的前i个元素,t的前j个元素位置
dp数组含义:dp[i][j]代表s的前i个元素是否为t的前j个元素的子序列,是则true,否则false。然后是状态转移方程:
- 当s[i] == t[j]时,这两个元素就不重要了,取决于dp[i-1][j-1]是什么,即dp[i][j] = dp[i - 1][j - 1];
- 当s[i] != t[j]时,那么t[j]就不重要了,取决于dp[i][j-1]是什么,即dp[i][j] = dp[i][j - 1]
容易想到:
- dp[i][j]=false,i>j 这一条可以直接在二重循环中体现: int j = i
- dp[0][j]=true,j∈[0,n]
class Solution {
public:
bool isSubsequence(string s, string t) {
//动态规划
//dp[i][j] = dp[i - 1][j - 1];匹配时
//dp[i][j] = dp[i][j - 1] 不匹配,缩短长序列
int m = s.length(), n = t.length();
vector<vector<bool>> dp(m + 1, vector<bool>(n + 1, false));
for(int i = 0; i <= n; i++) {
dp[0][i] = true;
}
for(int i = 1; i <= m; i++){
for(int j = i; j <= n; j++){
if(s[i - 1] == t[j - 1]){ //i-1相当于第i个元素
dp[i][j] = dp[i - 1][j - 1];
}
else{
dp[i][j] = dp[i][j - 1];
}
if(i == m && dp[i][j] == true) return true;//剪枝
}
}
return dp[m][n];
}
};
LeetCode303. 区域和检索 - 数组不可变
给定一个整数数组 nums,求出数组从索引 i 到 j (i ≤ j) 范围内元素的总和,包含 i, j 两点。
示例:
给定 nums = [-2, 0, 3, -5, 2, -1],求和函数为 sumRange()
sumRange(0, 2) -> 1
sumRange(2, 5) -> -1
sumRange(0, 5) -> -3
- 我们不直接存储结果,我们存储从第一个元素到第i个元素的和dp[i] = sum(nums[0]~nums[i - 1]);
- 动态转移方程:
- dp[i] = dp[i - 1] + nums[i];
- res = dp[j] - dp[i - 1];
- 初始条件
- dp[0] = nums[0]
- i=0时,即从初始位置开始则应return dp[j],当i不为0时,用0~j区域的和减去0~i区域的和及res = dp[j] - dp[i - 1]。
class NumArray {
public:
NumArray(vector<int>& nums) {
if(nums.size() != 0){
dp.push_back(nums[0]);
for(int i = 1; i < nums.size(); i++)
dp.push_back(dp[i - 1] + nums[i]);
}
}
int sumRange(int i, int j) {
if(dp.size() == 0) return 0;
return i == 0 ? dp[j] : (dp[j] - dp[i - 1] );
}
private:
vector<int> dp;
};