第一步骤:定义数组元素的含义,上面说了,我们会用一个数组,来保存历史数组,假设用一维数组 dp[] 吧。这个时候有一个非常非常重要的点,就是规定你这个数组元素的含义,例如你的 dp[i] 是代表什么意思?
第二步骤:找出数组元素之间的关系式,我觉得动态规划,还是有一点类似于我们高中学习时的归纳法的,当我们要计算 dp[n] 时,是可以利用 dp[n-1],dp[n-2]……dp[1],来推出 dp[n] 的,也就是可以利用历史数据来推出新的元素值,所以我们要找出数组元素之间的关系式,例如 dp[n] = dp[n-1] + dp[n-2],这个就是他们的关系式了。而这一步,也是最难的一步,后面我会讲几种类型的题来说。
第三步骤:找出初始值。学过数学归纳法的都知道,虽然我们知道了数组元素之间的关系式,例如 dp[n] = dp[n-1] + dp[n-2],我们可以通过 dp[n-1] 和 dp[n-2] 来计算 dp[n],但是,我们得知道初始值啊,例如一直推下去的话,会由 dp[3] = dp[2] + dp[1]。而 dp[2] 和 dp[1] 是不能再分解的了,所以我们必须要能够直接获得 dp[2] 和 dp[1] 的值,而这,就是所谓的初始值。
42. 接雨水(两次遍历+两组一维动规)
雨水量=min(左边高度,右边高度)-当前高度
先从左边扫一遍每个点左边的最大高度,再从右边扫一遍得到每个点右边的最大高度
求这两个数组的最小值,再减去每个位置的高度求和就是雨水总量
class Solution {
public:
int trap(vector<int>& height) {
int n = height.size();
if (n == 0)
return 0;
//从左边扫一遍动规
vector<int> leftMax(n);
leftMax[0] = height[0];
for (int i = 1; i < n; ++i)
leftMax[i] = max(leftMax[i - 1], height[i]);
//从右边扫一遍动规
vector<int> rightMax(n);
rightMax[n - 1] = height[n - 1];
for (int i = n - 2; i >= 0; --i)
rightMax[i] = max(rightMax[i + 1], height[i]);
int ans = 0;
for (int i = 0; i < n; ++i)
ans += min(leftMax[i], rightMax[i]) - height[i];
return ans;
}
};
62. 不同路径(矩阵+二维动规)
设置二维数组dp为当前点的路径和,该值等于x-1或者y-1走来的点路径相加
class Solution {
public:
int uniquePaths(int m, int n) {
vector<vector<int>> f(m, vector<int>(n));
for (int i = 0; i < m; ++i)
f[i][0] = 1;
for (int j = 0; j < n; ++j)
f[0][j] = 1;
for (int i = 1; i < m; ++i) {
for (int j = 1; j < n; ++j)
f[i][j] = f[i - 1][j] + f[i][j - 1];
}
return f[m - 1][n - 1];
}
};
64. 最小路径和(矩阵+二维动规)
用二维数组dp记录当前最小路径,当前dp等于上面来的左边来的最小值加当前值
class Solution {
public:
int minPathSum(vector<vector<int>>& grid) {
if (grid.size() == 0 || grid[0].size() == 0)
return 0;
int rows = grid.size(), columns = grid[0].size();
auto dp = vector < vector <int> > (rows, vector <int> (columns));
dp[0][0] = grid[0][0];
for (int i = 1; i < rows; i++)
dp[i][0] = dp[i - 1][0] + grid[i][0];
for (int j = 1; j < columns; j++)
dp[0][j] = dp[0][j - 1] + grid[0][j];
for (int i = 1; i < rows; i++)
for (int j = 1; j < columns; j++)
dp[i][j] = min(dp[i - 1][j], dp[i][j - 1]) + grid[i][j];
return dp[rows - 1][columns - 1];
}
};
70. 爬楼梯(一次遍历+一维动规)
1、本题不能用递归,楼梯过多会超出时间限制,用动态规划
2、用first和second代替动规状态数组,存储前两步楼梯的各自方案数之和
3、当前等于first+second,再更新first和second
int climbStairs(int n) {
if(n==0) return 1;
else if (n==1) return 1;
else if (n==2) return 2;
int first=1,second=2,now=3,sum=0;
while(now<=n) {
sum=first+second;
first=second;
second=sum;
now++;
}
return sum;
}
72. 编辑距离
1、有三种编辑方法,在a字符串加一个字符,在b字符串加一个字符,在a或b字符串修改一个字符
2、二维dp初始化为ab中有一个为0时,另一个字符串只能增加对应长度的字符
3、dp[i][j]等于a加一个,b加一个,若相等则不用改,不相等则加一个,这三种情况中最小的一个
class Solution {
public:
int minDistance(string word1, string word2) {
int n = word1.length(),m = word2.length(),D[n + 1][m + 1];
if (n * m == 0) return n + m; // 有一个字符串为空串
// 边界状态初始化
for (int i = 0; i < n + 1; i++)
D[i][0] = i;
for (int j = 0; j < m + 1; j++)
D[0][j] = j;
// 计算所有 DP 值
for (int i = 1; i < n + 1; i++) {
for (int j = 1; j < m + 1; j++) {
int left = D[i - 1][j] + 1;
int down = D[i][j - 1] + 1;
int left_down = D[i - 1][j - 1];
if (word1[i - 1] != word2[j - 1]) left_down += 1;
D[i][j] = min(left, min(down, left_down));
}
}
return D[n][m];
}
};
96. 不同的二叉搜索树
个数从小到大构建,每次构建等于以不同数为根节点,其他分到左右两边
class Solution {
public:
int numTrees(int n) {
vector<int> G(n + 1, 0);
G[0] = 1;
G[1] = 1;
for (int i = 2; i <= n; ++i) {
for (int j = 1; j <= i; ++j) {
G[i] += G[j - 1] * G[i - j];
}
}
return G[n];
}
};
152. 乘积最大子数组(一次遍历+双一维动规)
本题存在正负,不能只考虑前一个的最大值,还要考虑前一个的最小值,因为负负得正,通过渐进dp减小时间复杂度
class Solution {
public:
int maxProduct(vector<int>& nums) {
int maxF = nums[0], minF = nums[0], ans = nums[0];
for (int i = 1; i < nums.size(); ++i) {
int mx = maxF, mn = minF;
maxF = max(mx * nums[i], max(nums[i], mn * nums[i]));
minF = min(mn * nums[i], min(nums[i], mx * nums[i]));
ans = max(maxF, ans);
}
return ans;
}
};
198. 打家劫舍(一次遍历+一维动规)
当前取得的最大值取决于前两个,要么偷前一家不偷当前家,要么偷前两家加上当前家
class Solution {
public:
int rob(vector<int>& nums) {
if (nums.empty())
return 0;
int size = nums.size();
if (size == 1)
return nums[0];
vector<int> dp = vector<int>(size, 0);
dp[0] = nums[0];
dp[1] = max(nums[0], nums[1]);
for (int i = 2; i < size; i++)
dp[i] = max(dp[i - 2] + nums[i], dp[i - 1]);
return dp[size - 1];
}
};
213. 打家劫舍 II(两次遍历+一维动规)
这题加了一个限制条件:首尾相连,所以第一个和最后一个只能偷一个,所以进行两次打家劫舍,要么偷第一家,要么偷最后一家,取两个的最大值
class Solution {
public:
int robRange(vector<int>& nums, int start, int end) {
int first = nums[start], second = max(nums[start], nums[start + 1]);
for (int i = start + 2; i <= end; i++) {
int temp = second;
second = max(first + nums[i], second);
first = temp;
}
return second;
}
int rob(vector<int>& nums) {
int length = nums.size();
if (length == 1)
return nums[0];
else if (length == 2)
return max(nums[0], nums[1]);
return max(robRange(nums, 0, length - 2), robRange(nums, 1, length - 1));
}
};
337. 打家劫舍 III(哈希表+递归+动规)
用两个哈希表存节点的两个状态,选择和不选择,如果选择,子节点必须不选择,如果不选择,则选子节点中选择和不选择的较大者
class Solution {
public:
unordered_map <TreeNode*, int> f, g;
void dfs(TreeNode* node) {
if (!node)
return;
dfs(node->left);
dfs(node->right);
f[node] = node->val + g[node->left] + g[node->right];
g[node] = max(f[node->left], g[node->left]) + max(f[node->right], g[node->right]);
}
int rob(TreeNode* root) {
dfs(root);
return max(f[root], g[root]);
}
};
221. 最大正方形(双重遍历+二维动规)
1、二维动规数字dp表示以该点为右下角的最大正方形边长
2、最上面和最左边的点的dp值只能为1
3、dp值取决于它上面左面和左上方三个点的最小值加1,因为他想构成正方形必须保证这三个点起码都有
class Solution {
public:
int maximalSquare(vector<vector<char>>& matrix) {
if (matrix.size() == 0 || matrix[0].size() == 0)
return 0;
int maxSide = 0,rows = matrix.size(), columns = matrix[0].size();
vector<vector<int>> dp(rows, vector<int>(columns));
for (int i = 0; i < rows; i++) {
for (int j = 0; j < columns; j++) {
if (matrix[i][j] == '1') {
if (i == 0 || j == 0)
dp[i][j] = 1;
else
dp[i][j] = min(min(dp[i - 1][j], dp[i][j - 1]), dp[i - 1][j - 1]) + 1;
maxSide = max(maxSide, dp[i][j]);
}
}
}
int maxSquare = maxSide * maxSide;
return maxSquare;
}
};
322. 零钱兑换(双重遍历+一维动规)
设置dp为当前价格的最少方案,一层遍历价格,一层遍历硬币面值,从小到大,看哪一个面值硬币方案最少,保存为当前dp
class Solution {
public:
int coinChange(vector<int>& coins, int amount) {
int Max = amount + 1;
vector<int> dp(amount + 1, Max);
dp[0] = 0;
for (int i = 1; i <= amount; ++i) {
for (int j = 0; j < (int)coins.size(); ++j) {
if (coins[j] <= i)
dp[i] = min(dp[i], dp[i - coins[j]] + 1);
}
}
return dp[amount] > amount ? -1 : dp[amount];
}
};
518. 零钱兑换 II(双重遍历+一维动规)
上一题求的是硬币个数,这一题求的是方案总数,上一题外层遍历方案内层遍历硬币,这一题外层遍历硬币内层遍历方案
class Solution {
public:
int change(int amount, vector<int>& coins) {
vector<int> dp(amount + 1);
dp[0] = 1;
for (int& coin : coins) {
for (int i = coin; i <= amount; i++) {
dp[i] += dp[i - coin];
}
}
return dp[amount];
}
};
354. 俄罗斯套娃信封问题
1、按照一维升序二维降序的规则对信封排序
2、对二维查找最长子序列,一维dp存当前信封可以套几个
class Solution {
public:
int maxEnvelopes(vector<vector<int>>& envelopes) {
if (envelopes.empty())
return 0;
int n = envelopes.size();
sort(envelopes.begin(), envelopes.end(), [](const auto& e1, const auto& e2) {
return e1[0] < e2[0] || (e1[0] == e2[0] && e1[1] > e2[1]);
});
vector<int> f(n, 1);
for (int i = 1; i < n; ++i) {
for (int j = 0; j < i; ++j) {
if (envelopes[j][1] < envelopes[i][1]) {
f[i] = max(f[i], f[j] + 1);
}
}
}
return *max_element(f.begin(), f.end());
}
};
509. 斐波那契数(一次遍历+一维动规)
一次遍历,不断更新第一个值和第二个值,使用滚动数组暂存前两个数
class Solution {
public:
int fib(int n) {
if (n < 2) {
return n;
}
int p = 0, q = 0, r = 1;
for (int i = 2; i <= n; ++i) {
p = q;
q = r;
r = p + q;
}
return r;
}
};