动态规划(英语:Dynamic programming,简称 DP)是一种在数学、管理科学、计算机科学、经济学和生物信息学中使用的,通过把原问题分解为相对简单的子问题的方式求解复杂问题的方法。
动态规划常常适用于有重叠子问题和最优子结构性质的问题,动态规划方法所耗时间往往远少于朴素解法。
动态规划背后的基本思想非常简单。大致上,若要解一个给定问题,我们需要解其不同部分(即子问题),再根据子问题的解以得出原问题的解。动态规划往往用于优化递归问题,例如斐波那契数列,如果运用递归的方式来求解会重复计算很多相同的子问题,利用动态规划的思想可以减少计算量。
通常许多子问题非常相似,为此动态规划法试图仅仅解决每个子问题一次,具有天然剪枝的功能,从而减少计算量:一旦某个给定子问题的解已经算出,则将其记忆化存储,以便下次需要同一个子问题解之时直接查表。这种做法在重复子问题的数目关于输入的规模呈指数增长时特别有用。
- 做法:
- 通过状态转移方程,将大事化小,小事化了。
- 从第一个数开始计算每一个数的value结果。在这么多结果里面寻找最佳的答案。
- 思想:尽量缩小可能解空间。
1、连续子数组最大和
题目描述
输入一个整型数组,数组里有正数也有负数。数组中的一个或连续多个整数组成一个子数组。求所有子数组的和的最大值。
要求时间复杂度为O(n)。
示例
输入: nums = [-2,1,-3,4,-1,2,1,-5,4]s
输出: 6
解释: 连续子数组 [4,-1,2,1] 的和最大,为 6。
分析
假设现在计算到第 i - 1 位为止的连续子数组的和为最大值,现在要添加第 i 位进这个最大数组则有两种情况:
- 将第i位加入第i - 1位对应的最大连续子数组中
- 第i位单独成子数组
很明显第2种操作对应的情况为第i位单独成为一个数组比加入后的数组和大;第1种操作则对应着第i位单独成为一个数组比加入后的数组小。这么做就能够保证得出的第i位的连续子数组最大。所以状态转移方程如下所示:
R [ i ] = m a x { R [ i ] , R [ i − 1 ] + R [ i ] } . R[i] = max\{R[i] , R[i-1] + R[i] \}. R[i]=max{R[i],R[i−1]+R[i]}.
代码
class Solution {
public:
int maxSubArray(vector<int>& nums) {
int n = nums.size();
for (int i = 1; i < n; i++)
nums[i] = max(nums[i], nums[i - 1] + nums[i]);
return *max_element(nums.begin(), nums.end());
}
};
2、 爬楼梯
来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/climbing-stairs
题目描述
假设你正在爬楼梯。需要 n 阶你才能到达楼顶。
每次你可以爬 1 或 2 个台阶。你有多少种不同的方法可以爬到楼顶呢?
示例
输入: 2
输出: 2
解释: 有两种方法可以爬到楼顶。
1. 1 阶 + 1 阶
2. 2 阶
输入: 3
输出: 3
解释: 有三种方法可以爬到楼顶。
1. 1 阶 + 1 阶 + 1 阶
2. 1 阶 + 2 阶
3. 2 阶 + 1 阶
分析
达到第 i 级的方式可以分为:
- 从第i - 1级跳1级
- 从第i - 2级跳2级
所以状态转移方程如下所示:
R [ i ] = R [ i − 1 ] + R [ i − 2 ] R[i] = R[i-1] +R[i-2] R[i]=R[i−1]+R[i−2]
代码
class Solution {
public:
int climbStairs(int n) {
vector<int> n_dp;
n_dp.push_back(1);
n_dp.push_back(2);
int i = 1;
while (++i < n)
n_dp.push_back(n_dp[i - 1] + n_dp[i - 2]);
return n_dp[n - 1];
}
};
3、 爬楼梯(裆鸡立断版本)
如果这个人裆比较好,能一步跨3级。题目为:
三步问题。有个小孩正在上楼梯,楼梯有n阶台阶,小孩一次可以上1阶、2阶或3阶。实现一种方法,计算小孩有多少种上楼梯的方式。结果可能很大,你需要对结果模1000000007。
则状态转移方程改变为:
R [ i ] = R [ i − 1 ] + R [ i − 2 ] + R [ i − 3 ] R[i] = R[i-1] +R[i-2] + R[i-3] R[i]=R[i−1]+R[i−2]+R[i−3]
并且对代码进行优化:牺牲时间来换空间
做法:用三个变量来表示前3个状态
改进代码
class Solution {
public:
int waysToStep(int n) {
int dp_value, dp[3] = { 4,2,1 };
if (n <= 3) return dp[3 - n];
int i = 2;
while (++i < n) {
dp_value = ((dp[0] + dp[1]) % 1000000007 + dp[2]) % 1000000007;
dp[2] = dp[1];
dp[1] = dp[0];
dp[0] = dp_value;
}
return dp_value;
}
};
4、 爬楼梯(踩地雷版本)
来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/min-cost-climbing-stairs
题目描述
数组的每个索引作为一个阶梯,第 i个阶梯对应着一个非负数的踩地雷数 cost[i](索引从0开始)。
每当你爬上一个阶梯你都踩对应地雷数,然后你可以选择继续爬一个阶梯或者爬两个阶梯。
您需要找到达到楼层顶部的踩到最少地雷。在开始时,你可以选择从索引为 0 或 1 的元素作为初始阶梯。
示例
输入: cost = [10, 15, 20]
输出: 15
解释: 最低踩地雷是从cost[1]开始,然后走两步即可到阶梯顶,一共花费15。
输入: cost = [1, 100, 1, 1, 1, 100, 1, 1, 100, 1]
输出: 6
解释: 最低踩地雷方式是从cost[0]开始,逐个经过那些1,跳过cost[3],一共花费6。
分析
达到第 i 级的方式可以分为:
- 从第i - 1级跳1级
- 从第i - 2级跳2级
目标是踩地雷的最少,即是将选择i - 1 和i - 2 中最少的当跳板,所以状态转移方程如下所示:
R [ i ] = m a x { R [ i − 1 ] + R [ i − 2 ] } + R [ i ] R[i] = max\{ R[i-1] +R[i-2]\} + R[i] R[i]=max{R[i−1]+R[i−2]}+R[i]
代码
class Solution {
public:
int minCostClimbingStairs(vector<int>& cost) {
cost.push_back(0); //设最后一级0消耗
for (int i = 2; i < cost.size(); i++)
cost[i] += min(cost[i - 1], cost[i - 2]);
return cost[cost.size() - 1];
}
};
4、 不同的二叉搜索树
来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/unique-binary-search-trees
题目描述
给定一个整数 n,求以 1 ... n 为节点组成的二叉搜索树有多少种?
示例
输入: 3
输出: 5
解释:
给定 n = 3, 一共有 5 种不同结构的二叉搜索树:
1 3 3 2 1
\ / / / \ \
3 2 1 1 3 2
/ / \ \
2 1 2 3
分析
i个结点的每一种结构可以分为以下三个部分:
- 根结点(1)
- 根结点左边所有的子结点(j)
- 根结点右边所有的子结点(i - j - 1)
则以R(x)来表示结构数量的话,根结点左边有R(j)种结构,右边有R(i - j - 1)种结构。在此情况下(右边有j个子结点),结构数量为:
R
(
j
)
∗
R
(
i
−
j
−
1
)
R(j)*R(i - j - 1)
R(j)∗R(i−j−1)
以此类推,则R(i)的表达式(状态转移方程)为:
R ( i ) = ∑ 0 i − 1 R ( j ) ∗ R ( i − j − 1 ) R(i) = \sum_0^{i-1} {R(j)*R(i - j - 1) } R(i)=0∑i−1R(j)∗R(i−j−1)
代码
class Solution {
public:
int numTrees(int n) {
int num;
vector<int> a;
a.push_back(1); // 0
a.push_back(1); // 1
for (int i = 2; i <= n; i++) {
num = 0;
for (int j = 0; j < i; j++)
num += (a[i - j - 1] * a[j]);
a.push_back(num);
}
return a[n];
}
};
5、 最大的以 1 为边界的正方形
来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/largest-1-bordered-square
题目描述
给你一个由若干 0 和 1 组成的二维网格 grid,请你找出边界全部由 1 组成的最大 正方形 子网格,并返回该子网格中的元素数量。如果不存在,则返回 0。
示例 1:
输入:grid = [[1,1,1],[1,0,1],[1,1,1]]
输出:9
示例 2:
输入:grid = [[1,1,0,0]]
输出:1
分析
正方形可以理解为一个点向左延伸&向上延伸,所以设定一个矩阵表示向左延伸的最大值和向上延伸的最大值。这个值可以使用状态方程进行表示:
l e f t [ i ] [ j ] = g r i d [ i ] [ j ] ∗ ( l e f t [ i ] [ j − 1 ] + 1 ) u p [ i ] [ j ] = g r i d [ i ] [ j ] ∗ ( u p [ i − 1 ] [ j ] + 1 ) left[i][j] = grid[i][j] * (left[i][j-1]+1)\\ up[i][j] = grid[i][j] * (up[i-1][j]+1) left[i][j]=grid[i][j]∗(left[i][j−1]+1)up[i][j]=grid[i][j]∗(up[i−1][j]+1)
利用这两个可以得到点(i,j)最大的边长为:
m i n { u p [ i ] [ j ] , l e f t [ i ] [ j ] } min\{up[i][j], left[i][j]\} min{up[i][j],left[i][j]}
在这个范围内对正方形边长进行遍历,满足条件者即可迭代最大值,最终求得最大的正方形
代码
class Solution {
public:
int largest1BorderedSquare(vector<vector<int>>& grid) {
vector<vector<int>> left(grid.size()), up(grid.size());
int answer = 0, c;
for (int i = 0; i < grid.size(); i++)
for (int j = 0; j < grid[0].size(); j++) {
if (i == 0) up[0].push_back(grid[0][j]);
else if (grid[i][j] == 1) up[i].push_back(up[i - 1][j] + 1);
else up[i].push_back(0);
if (j == 0) left[i].push_back(grid[i][0]);
else if (grid[i][j] == 1) left[i].push_back(left[i][j - 1] + 1);
else left[i].push_back(0);
if (grid[i][j] == 0) continue;
c = (left[i][j] < up[i][j]) ? left[i][j] : up[i][j];
for (int k = 0; k < c; k++)
if (answer < k + 1 && up[i][j - k] >= k + 1 && left[i - k][j] >= k + 1) answer = k + 1;
else continue;
}
return answer * answer;
}
};
6、删除与获得点数
来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/delete-and-earn
题目描述
给定一个整数数组 nums ,你可以对它进行一些操作。
每次操作中,选择任意一个 nums[i] ,删除它并获得 nums[i] 的点数。之后,你必须删除每个等于 nums[i] - 1 或 nums[i] + 1 的元素。
开始你拥有 0 个点数。返回你能通过这些操作获得的最大点数。
示例 1:
输入: nums = [3, 4, 2]
输出: 6
解释:
删除 4 来获得 4 个点数,因此 3 也被删除。
之后,删除 2 来获得 2 个点数。总共获得 6 个点数。
示例 2:
输入: nums = [2, 2, 3, 3, 3, 4]
输出: 9
解释:
删除 3 来获得 3 个点数,接着要删除两个 2 和 4 。
之后,再次删除 3 获得 3 个点数,再次删除 3 获得 3 个点数。
总共获得 9 个点数。
分析
从例子可以看出输入的数字没有进行排列,所以应该对其进行排列,然后根据题目可以取一个数字为选择删去或者不选择删去,所以对于一个数字x有两种处理方式,这就类似于斐波那契数列。通过构造一个统计数组d[i],下标代表数字,值为该数字出现的次数。下一个状态的选择就和上一个状态(不选择该数字)和上上个状态相关(选择该数字),则状态转移方程如下所示:
R [ i ] = m a x { R [ i − 1 ] , R [ i − 2 ] + i ∗ d [ i ] } . R[i] = max\{R[i-1] , R[i-2] + i*d[i] \}. R[i]=max{R[i−1],R[i−2]+i∗d[i]}.
代码
class Solution {
public:
int deleteAndEarn(vector<int>& nums) {
sort(nums.begin(), nums.end());
if (nums.size() == 0) return 0;
int max_num = nums[nums.size() - 1] + 1;
vector<int> dl(max_num), dp(max_num);
for (int i = 0; i < nums.size(); i++) dl[nums[i]]++;
dp[0] = 0;
dp[1] = dl[1];
for (int i = 2; i < max_num; i++)
if (dl[i] != 0) dp[i] = max(dp[i - 1], dp[i - 2] + i * dl[i]);
else dp[i] = dp[i - 1];
return dp[dp.size() - 1];
}
};
7、硬币
来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/coin-lcci
题目描述
硬币。给定数量不限的硬币,币值为25分、10分、5分和1分,编写代码计算n分有几种表示法。(结果可能会很大,你需要将结果模上1000000007)
示例 1:
输入: n = 5
输出:2
解释: 有两种方式可以凑成总金额:
5=5
5=1+1+1+1+1
示例 2:
输入: n = 10
输出:4
解释: 有四种方式可以凑成总金额:
10=10
10=5+5
10=5+1+1+1+1+1
10=1+1+1+1+1+1+1+1+1+1
分析
这题和之前的动态规划问题,有一个很明显的区别。比如说6RMB,现在如果使用动态规划,6的状态是从之前6 - 1的状态和6 - 5的状态相加。但是很明显,1+5 和 5+1是一种取法,所以对于本题应该使用动态规划中的完全背包算法。实际上做法就是使得钱的选择有了顺序,这样就没有1+5&5+1的情况出现,所以外循环应该取四种钱,内循环取1->n。这样就能够保证没有之前重复的次数出现。所以状态转移方程如下所示:
R [ j ] = R [ j ] + R [ j − v [ i ] ] R[j] =R[j] + R[j-v[i]] R[j]=R[j]+R[j−v[i]]
代码
class Solution {
public:
int waysToChange(int n) {
int v[4] = { 1,5,10,25 };
vector<int> dp(n + 1);
dp[0] = 1;
for (int i = 0; i < 4; i++)
for (int j = 1; j <= n; j++)
if (j >= v[i]) dp[j] = (dp[j] + dp[j - v[i]]) % 1000000007;
return dp[dp.size() - 1];
}
};
8、移除盒子
来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/remove-boxes
题目描述
给出一些不同颜色的盒子,盒子的颜色由数字表示,即不同的数字表示不同的颜色。
你将经过若干轮操作去去掉盒子,直到所有的盒子都去掉为止。每一轮你可以移除具有相同颜色的连续 k 个盒子(k >= 1),这样一轮之后你将得到 k*k 个积分。
当你将所有盒子都去掉之后,求你能获得的最大积分和。
示例 :
输入:boxes = [1,3,2,2,2,3,4,3,1]
输出:23
解释:
[1, 3, 2, 2, 2, 3, 4, 3, 1]
----> [1, 3, 3, 4, 3, 1] (3*3=9 分)
----> [1, 3, 3, 3, 1] (1*1=1 分)
----> [1, 1] (3*3=9 分)
----> [] (2*2=4 分)
分析
首先使用穷举对其进行搜索,其思路就是对每次的选取进行遍历,这样要取n次则要选取方案数为n!。其代码如下
穷举代码
class Solution {
public:
int removeBoxes(vector<int>& boxes) {
vector<vector<int>> a(2), b;
int cnt = 1;
for (int i = 0; i < boxes.size(); i++)
if (i == boxes.size() - 1 || boxes[i] != boxes[i + 1]) {
a[0].push_back(boxes[i]);
a[1].push_back(cnt);
cnt = 1;
}
else cnt++;
return count(a);
}
int count(vector<vector<int>> a) {
int max_num = 0, num;
vector<vector<int>> b;
if (a[0].size() == 1) return (a[1][0] * a[1][0]);
if (a[0].size() == 2) return (a[1][0] * a[1][0] + a[1][1] * a[1][1]);
for (int i = 0; i < a[0].size(); i++) {
b = a;
b[0].erase(b[0].begin() + i);
b[1].erase(b[1].begin() + i);
if (i > 0 && i < a[0].size() - 1 && a[0][i - 1] == a[0][i + 1]) {
b[1][i - 1] += b[1][i];
b[0].erase(b[0].begin() + i);
b[1].erase(b[1].begin() + i);
}
max_num = max(max_num, ((a[1][i] * a[1][i]) + count(b)));
}
return max_num;
}
};
分析
在运行之后发现输入数组大小为10时就已经超时了,所以要对其进行优化。l、r代表左、右下标,k代表数组后边所有与右下标对应的值相同的数量。(做了一早上想不出来,直接看解析)然后可以由两个方面进行计算,即直接接上k+1个相同的数字,还有一种是这k+1个数字和之前的相同的数字进行合并,然后再进行下一层迭代。则状态转移方程如下所示:
R [ l ] [ r ] [ k ] = m a x { R [ l ] [ r ] [ 0 ] + ( k + 1 ) 2 , R [ l ] [ i ] [ k + 1 ] + R [ i + 1 ] [ r − 1 ] [ 0 ] } R[l][r][k] =max\{ R[l][r][0] + (k+1)^2 ,R[l][i][k+1]+R[i+1][r-1][0]\} R[l][r][k]=max{R[l][r][0]+(k+1)2,R[l][i][k+1]+R[i+1][r−1][0]}
代码
class Solution {
public:
int dp[100][100][100];
int removeBoxes(vector<int>& boxes) {
memset(dp, 0, sizeof dp);
return calculatePoints(boxes, 0, boxes.size() - 1, 0);
}
int calculatePoints(vector<int>& boxes, int l, int r, int k) {
if (l > r) return 0;
if (dp[l][r][k] != 0) return dp[l][r][k];
while (r > l && boxes[r] == boxes[r - 1]) {
r--;
k++;
}
dp[l][r][k] = calculatePoints(boxes, l, r - 1, 0) + (k + 1) * (k + 1);
for (int i = l; i < r; i++) {
if (boxes[i] == boxes[r]) {
dp[l][r][k] = max(dp[l][r][k], calculatePoints(boxes, l, i, k + 1) + calculatePoints(boxes, i + 1, r - 1, 0));
}
}
return dp[l][r][k];
}
};
9、预测赢家
来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/predict-the-winner
题目描述
难度中等283收藏分享切换为英文已关注反馈给定一个表示分数的非负整数数组。 玩家 1 从数组任意一端拿取一个分数,随后玩家 2 继续从剩余数组任意一端拿取分数,然后玩家 1 拿,…… 。每次一个玩家只能拿取一个分数,分数被拿取之后不再可取。直到没有剩余分数可取时游戏结束。最终获得分数总和最多的玩家获胜。
给定一个表示分数的数组,预测玩家1是否会成为赢家。你可以假设每个玩家的玩法都会使他的分数最大化。
示例 :
输入:[1, 5, 2]
输出:False
解释:一开始,玩家1可以从1和2中进行选择。
如果他选择 2(或者 1 ),那么玩家 2 可以从 1(或者 2 )和 5 中进行选择。如果玩家 2 选择了 5 ,那么玩家 1 则只剩下 1(或者 2 )可选。
所以,玩家 1 的最终分数为 1 + 2 = 3,而玩家 2 为 5 。
因此,玩家 1 永远不会成为赢家,返回 False 。
示例 :
输入:[1, 5, 233, 7]
输出:True
解释:玩家 1 一开始选择 1 。然后玩家 2 必须从 5 和 7 中进行选择。无论玩家 2 选择了哪个,玩家 1 都可以选择 233 。
最终,玩家 1(234 分)比玩家 2(12 分)获得更多的分数,所以返回 True,表示玩家 1 可以成为赢家。
分析
由于数据量较小,所以这题使用递归方法对其进行暴力搜索是不会超时的,但是本题最好的做法还是使用动态规划的方法进行求解,方法是进行从后向前的方法进行,思路如下:
首先将长度为1的数字判断先手的净得分(两者得分之差),接着计算长度为n的数字判断先手的净得分,这个时候就有两个选择:1、先取左边的一个;2、先取右边的一个。这各做法导致的就是失去剩下(n-1)数字的优先选择权,即剩下的数字的DP值应该被对手取代,所以n长度的DP值应该是首先取的值(得到的)减去(n-1)的DP值(被对手先手),并且在两种选择方法中取最大值作为n长度的DP值。按照这种关系可以得到此题的状态转移方程式:
R
[
l
]
[
r
]
=
m
a
x
{
n
u
m
[
r
]
−
R
[
l
]
[
r
−
1
]
,
n
u
m
[
l
]
−
R
[
l
+
1
]
[
i
]
}
R[l][r] =max\{ num[r] - R[l][r-1] ,num[l] - R[l+1][i]\}
R[l][r]=max{num[r]−R[l][r−1],num[l]−R[l+1][i]}
代码
class Solution {
public:
bool PredictTheWinner(vector<int>& nums) {
vector<vector<int>> dp(nums.size(), nums);
for (int i = 0; i < nums.size(); i++)
dp[i][i] = nums[i];
for (int i = 1; i < nums.size(); i++)
for (int j = 0; j + i < nums.size(); j++)
dp[j][j + i] = max(nums[i + j] - dp[j][j + i - 1], nums[j] - dp[j + 1][j + i]);
return dp[0][nums.size() - 1] >= 0;
}
};
[持续更新]