1. 动归问题的核心
状态和状态转移方程。状态可以根据子问题定义,状态方程就是描述状态是怎么转移的,它的的本质不在于是递推或是递归,也不需要纠结是不是内存换时间。
2. 相关概念
2.1 递推
2.2 贪心
3 算法选择
每个阶段的最优状态都是由 上一个阶段的最优状态得到的 -> 贪心;
每个阶段的最优状态是由 之前所有阶段的状态的组合得到的 -> 搜索;
每个阶段的最优状态可以从之前某个阶段的某个或某些状态直接得到而 不管之前这个状态是如何得到的 -> 动态规划。
4. 一维DP
(1)最大和子数组O(n):
假设A(0, i)区间存在k,使得[k, i]区间是以i结尾区间的最大值, 定义为dp[i], 在这里,当求取dp[i+1]时,有两种情况:
dp[i+1] = dp[i] + A[i+1], if (dp[i] + A[i+1] >0)
= 0, if(dp[i]+A[i+1] <0),如果和小于零,A[i+1]必为负数,舍弃重新开始
然后从左往右扫描,求取dp数字的最大值即为所求。
int maxSubArr(vector<int> nums) {
if (nums.empty()) return 0;
int sum = nums[0];
// vector<int> dp(nums.size()); //O(n)
int dp = nums[0]; //O(1)
for (int i = 1; i < nums.size(); i++) {
dp = max((dp + nums[i]), nums[i]);
sum = max(sum, dp);
}
return sum;
}
如果时间复杂度低一些O(n)可以采用分治的方法,采用二分。即最大和子数组区间要么在左半边,要么在右半边,要么横跨两部分。前两种情况可以递归的进行求解。第三种情况则需以以mid为中心,向两侧扫描求最大值。
//O(n^2)解法
int maxArr(vector<int> nums, int begin, int end, int &maxV) {
if (begin > end) return INT_MIN;
int mid = begin + (end - begin) /2;
int lmax = maxArr(nums, begin, mid-1, maxV);
int rmax = maxArr(nums, mid+1, end, maxV);
maxV = max(max(maxV, lmax), rmax);
int sum = 0, mlmax = 0;
for (int i = mid-1; i>= begin; i--) {
sum += nums[i];
mlmax = max(sum, mlmax);
}
sum = 0;
int mrmax = 0;
for (int i = mid+1; i <= end; i++) {
sum += nums[i];
mrmax = max(sum, mrmax);
}
maxV = max(maxV, mlmax + mrmax + nums[mid]);
}
int maxSubArr(vector<int> nums) {
if (nums.empty()) return 0;
int maxV = INT_MIN;
return maxArr(nums, 0, nums.size()-1, maxV);
}
(2)最长升序子序列LIS
定义dp[i],表示前i个数中以A[i]结尾(前i个元素)的最长非降子序列的长度.那么dp[i+1]可以利用如下状态转移方程得到
d[i+1] = max{1, d[j]+1},其中j<i,A[j]<=A[i].即如果A[i+1]>A[j],那么第i+1个元素可以接在dp[i]长的子序列的后面构成一个更长的子序列。与此同时,A[i+1]本身就可以构成一个长度为1的子序列。
//O(n^2)的解法
class Solution {
public:
int lengthOfLIS(vector<int>& nums) {
if (nums.empty()) return 0;
vector<int> dp(nums.size(), 1);
for (int i = 0; i < nums.size(); i++) {//对于第i+1个元素,不考虑前i个元素的情况
for (int j = 0; j < i; j++) {
if (nums[i] > nums[j] && (dp[j] + 1 > dp[i]))
dp[i] = dp[j] + 1;
}
}
return *(max_element(dp.begin(), dp.end()));
}
};
<pre name="code" class="cpp">/*O(nlogn)的解法
利用一个数组纪录递增子序列的尾元素,即数组下标为i对应的元素是长度为i的子序列的尾元素
*/
Class Solution() {
public:
int lengthOfLIS(vector<int> &nums) {
vector<int> ret;
for (int i = 0; i < nums.size(); i++) {
auto it = upper_bound(ret.begin(), ret.end(), nums[i]);//返回的是大于某个数的第一个数的位置
if(it == ret.end()) ret.push_back(nums[i]);
else *it = nums[i];
}
return ret.size();
}
};
5. 二维DP
1. Minimum Path Sum:给一个矩阵,从上至下从左至右的走,求路径最小和。
注意初始条件。设dp[i][j]表示走到A[i][j]时,路径中所包含的最小和。状态转移方程dp[i][j]=min(dp[i-1][j], dp[i][i-1])+A[i][j-1]
int minPathSum(vector<vector<int> > &mat) {
if(mat.empty()) return 0;
int rows = mat.size();
int cols = mat[0].size();
vector<vector <int>> dp(rows, vector<int>(cols, 0));
dp[0][0] = mat[0][0];
for (int i = 1; i < rows; i++)
dp[i][0] = dp[i-1][0] + mat[i][0];
for (int j = 1; j < cols; j++)
dp[0][j] = dp[0][j-1] + mat[0][j];
for (int i = 1; i < rows; i++) {
for (int j = 1; j < cols; j++) {
dp[i][j] = (dp[i-1][j], dp[i][j-1]) + mat[i][j];
}
}
return dp[rows-1][cols-1];
}
2. Interleaving String:给定三个字符串s1, s2, s3,判断s3是不是由前两个字符串交替组成的。
设dp[i][j]表示s1取i长度的字符,最后一个字符是s1[i-1],s2取j个长度的字符串,最后一个字符是s2[j-1],能否与s3(i+j)长度的字符串匹配上。
边界条件是,其中一个字符串长度为0,就用另一个字符串去匹配s3
bool table[s1.length()+1][s2.length()+1];
for(int i=0; i<s1.length()+1; i++)
for(int j=0; j< s2.length()+1; j++){
if(i==0 && j==0)
table[i][j] = true;
else if(i == 0)
table[i][j] = ( table[i][j-1] && s2[j-1] == s3[i+j-1]);
else if(j == 0)
table[i][j] = ( table[i-1][j] && s1[i-1] == s3[i+j-1]);
else
table[i][j] = (table[i-1][j] && s1[i-1] == s3[i+j-1] ) || (table[i][j-1] && s2[j-1] == s3[i+j-1] );
}
return table[s1.length()][s2.length()];
}