动态规划作为面试高频的考点被大多数程序员所重视,本期就带来的是动态规划的一些基本题目以及解答思路,希望能对大家有所帮助
难度:中等Medium
idea:
状态dp[i]:第[i]间房舍必偷获得的最高金额(注意此时第[i-1]间就不能偷了)
状态转移方程:取第[i-1]间必偷的最高金额和第[i-2]间必偷的最高金额加上偷当前房租的金额之间的较大值,即 d p [ i ] = m a x ( d p [ i − 1 ] , d p [ i − 2 ] + n u m s [ i ] ) dp[i]=max(dp[i-1],dp[i-2]+nums[i]) dp[i]=max(dp[i−1],dp[i−2]+nums[i])
class Solution {
public int rob(int[] nums) {
if (nums == null || nums.length == 0) return 0;
if (nums.length == 1) return nums[0];
int n = nums.length; // 保存一下长度,养成好习惯
int[] dp = new int[n]; // 建立一个和数组等长的dp数组
// 赋初值
dp[0] = nums[0]; // 若第[0]间房屋必偷,值为nums[0]
dp[1] = Math.max(dp[0], nums[1]); // 偷第[1]间,也可以写Math.max(nums[0],nums[1])
for (int i = 2; i < n; i++) { // 因为[0]和[1]已经赋初值了,所以从[2]开始
// 两种选择: 要么偷第[i-1]间,要么偷第[i-2]间和第[i]间
dp[i] = Math.max(dp[i - 1], dp[i - 2] + nums[i]); // 滚雪球,原材料为上次和上上次的结果
}
return dp[n - 1]; // 返回dp数组的最后一个即可
}
}
改进版
因为状态推进的过程中只用到上一次和上上次的状态,故可以只定义两个整形变量prev和curr,分别存储后者dp[i-1] 和 前者dp[i-2],其中prev代表前者dp[i-2],curr代表后者dp[i-1],其中后者下次还要接着用,就变成新前者了,即 p r e v = c u r r — — ( 1 ) prev=curr——(1) prev=curr——(1),而新后者是由偷和不偷求出来的:
c u r r = m a x ( c u r r , p r e v + n u m s [ i ] ) — — ( 2 ) curr=max(curr,prev+nums[i])——(2) curr=max(curr,prev+nums[i])——(2)
其中等式左边的是新后者,右边的是旧前者和旧后者。
综合(1)(2)两式,我们可以先把旧后者存起来,等算完新后者之后把旧后者赋给新前者:
t e m p = o l d _ c u r r , n e w _ c u r r = m a x ( o l d c u r r , o l d _ p r e v + n u m s [ i ] ) , n e w _ p r e v = t e m p , temp = old\_curr,\\ new\_curr=max(old_curr,old\_prev+nums[i]),\\ new\_prev=temp, temp=old_curr,new_curr=max(oldcurr,old_prev+nums[i]),new_prev=temp,
或者把旧前者存起来,先得到新前者,再用旧后者和旧前者计算新前者:
t e m p = o l d _ p r e v , n e w _ p r e v = o l d _ c u r r , n e w _ c u r r = m a x ( o l d _ c u r r , t e m p + n u m s [ i ] ) , temp = old\_prev,\\ new\_prev = old\_curr,\\ new\_curr=max(old\_curr,temp+nums[i]), temp=old_prev,new_prev=old_curr,new_curr=max(old_curr,temp+nums[i]),
逻辑是一样的,都行。
class Solution {
public:
int rob(vector<int>& nums) {
int prev = 0;
int curr = 0;
// 循环整个数组
for(int num : nums) {
int temp = max(curr, prev + num); // 先用temp把算出来的new_curr存起来,此时curr还是old_curr
prev = curr; // 先把old_curr赋给new_prev
curr = temp; // 再把上面计算好的new_curr给curr,这两句顺序不能反了
}
return curr;
}
};
idea: 动态规划(DP method)
因为是环形的,所以处理状态转移关系的时候要小心些,分为两部分:
a. 若偷了第[0]间的房子,则第[n-1]间,即最后一间就不能偷了;
b. 若没偷第[0]间的房子,则第[n-1]间可以偷;
即调取上一题198打家劫舍的函数两次,取二者的较大值
状态dp[i]: 第[i]间必偷时所能得到的最大金额;
状态转移方程:
d p [ i ] = M a t h . m a x ( d p [ i − 1 ] , d p [ i − 2 ] + n u m s [ i ] ) dp[i] = Math.max(dp[i-1],dp[i-2]+nums[i]) dp[i]=Math.max(dp[i−1],dp[i−2]+nums[i])
把原数组分成两部分,调用两次函数,最后取较大值
m a x ( h e l p e r ( n u m s [ 1 : ] ) , h e l p e r ( n u m s [ : − 1 ] ) ) max(helper(nums[1:]), helper(nums[:-1])) max(helper(nums[1:]),helper(nums[:−1]))
class Solution:
def rob(self, nums: List[int]) -> int:
# boundary case
if not nums : return 0
if len(nums) == 1: return nums[0]
# 下面要调用该函数两次
def helper(nums):
if not nums : return 0
if len(nums) == 1: return nums[0]
n = len(nums)
dp = [0] * n
dp[0] = nums[0]
dp[1] = max(dp[0], nums[1])
for i in range(2, n): # i的范围: [2, n - 1]
dp[i] = max(dp[i - 1], dp[i - 2] + nums[i])
return dp[-1] # 或返回dp[n-1]
return max(helper(nums[1:]), helper(nums[:-1]))
idea: 动态规划(DP method)
状态dp[i]: 从nums[0]到nums[i]为止连续的若干个数的最大和,计算的原则就是看从nums[0]到nums[i-1]的最大和dp[i-1]
1.若dp[i-1]是负的,证明当前的数nums[i]没必要加上负数dp[i-1]让自己变小了,故弃之,只选nums[i];
2.若dp[i-1]是正的,则nums[i]可以加上一个正数让自己变得更大.
由于dp[i]只能代表从nums[0]到nums[i]的最大和,故它可能忽高忽低,所以要定义一个整型变量res存储历史最大和
e.g nums = [-2,1,-3,4,-1,2,1,-5,4]
dp[0] = -2;(没办法弃nums[0],只能取)
dp[1] = 1;(弃dp[0],只取nums[1])
dp[2] = -2(nums[2]=-3加上了1让自己变得更大了)
dp[3] = 4(弃dp[2],只取nums[3])
dp[4] = 3(nums[4]=-1加上dp[3]=4让自己变得更大了)
dp[5] = 5(nums[5]=2加上dp[4]=3让自己变得更大了)
dp[6] = 6(nums[6]=1加上dp[5]=5让自己变得更大了)
class Solution:
def maxSubArray(self, nums: List[int]) -> int:
length = len(nums)
if length == 0: return 0
dp = [0] * length
res = nums[0] # 记录历史最大和,不管nums[0]是正是负,res初始值都是它
dp[0] = nums[0] # 虽然nums[0]有可能是负的,但是舍不掉
for i in range(1, length):
dp[i] = max(dp[i - 1], 0) + nums[i]
res = max(res,dp[i])
return res
难度:中等Medium
因为定义两个指针i和j,故动态规划数组dp是二维的。
状态dp[i][j]
:代表子串s[i…j]是否是回文字符串(左闭右闭)
状态转移方程: dp[i][j]要看s[i]和s[j]是否相等且dp[i+1][j-1]是否是回文字符串,即i和j各向里走一步.
d p [ i ] [ j ] = ( s [ i ] = = s [ j ] ) & & ( j − i < 3 ∣ ∣ d p [ i + 1 ] [ j − 1 ] ) dp[i][j] = (s[i] == s[j]) \&\& (j - i < 3 || dp[i + 1][j - 1]) dp[i][j]=(s[i]==s[j])&&(j−i<3∣∣dp[i+1][j−1])
class Solution {
public:
string longestPalindrome(string s) {
int length = s.size(); // 养成好习惯
int maxLength = INT_MIN;
int end = 0, start = 0;
vector<vector<bool>> dp(length, vector<bool>(length)); // vector(length)赋值length个第一行
for (int i = length - 1; i >= 0; i--) { // i从后往前,则j从i到头;若i从前往后走,则j从0到i
for (int j = i; j < length; j++) {
dp[i][j] = (s[i] == s[j]) && (j - i < 3 || dp[i + 1][j - 1]); // 状态转移方程看两个元素s[i] == s[j]和dp[i + 1][j - 1]
if(dp[i][j] && (j - i) > maxLength) { // 当找到更长的元素时,找两个东西:起始坐标start和长度maxLength
start = i;
end = j;
maxLength = j - i;
}
}
}
return s.substr(start, maxLength + 1);
}
};
难度:困难Hard
idea: 动态规划(DP method)
用两个指针left和right代表当前水柱的下标,分别指向两端;
另外再定义两个变量:left_max:代表height[0…left]的最高柱子高度,right_max:代表height[right…end]的最高柱子高度,类比第53题最大子序和:dp[i]代表从[0…i]的最大和.
对于每一轮做两次判断:
1.更新当前left_max和right_max的大小;
2.再比较left_max和right_max的大小:
a. 若left_max小,则用left_max减去height[left]的高度,求height[left]这一列盛水的面积;
b. 若right_max小,则用right_max减去height[right]的高度,求height[right]这一列盛水的面积;
也可以:
1.比较height[left]和height[right]的大小;
a.若height[left]小,先更新left_max,再计算height[left]能盛水的体积
b.若height[right]小,先更新right_max,再计算height[right]能盛水的体积
注意到这种方法求盛水面积是一列一列计算的,而用栈的方法是一行一行计算的
class Solution {
public:
int trap(vector<int>& height) {
int left = 0, right = height.size() - 1; // 定义左右两个指针
int res = 0; // 最大盛水的面积
int left_max = 0, right_max = 0;
while(left <= right) { // 当两个指针还没相交时
if(height[left] <= height[right] ) {
if(height[left] >= left_max) left_max = height[left];
res += left_max - height[left];
left++;
} else {
if(height[right] >= right_max) right_max = height[right];
res += right_max - height[right];
right--;
}
}
return res;
}
};
难度:困难Hard
分别定义状态dp[i][j]和状态转移方程
状态dp[i][j]: 不戳破nums[i]和nums[j],仅戳破i和j之间的气球能得到的最大金币数
状态转移方程: dp[i][j] = dp[i][k] + dp[k][j] + nums[i][j][k],因为dp[i][k]是戳破[i…k]之间的气球, dp[k][j]是戳破[k…j]之间的气球,故[i…j]之间只剩一个气球,就是nums[k],戳破它得到的金币数为: nums[i] * nums[k] * nums[j] 即nums[i][j][k],故
dp[i][j] = dp[i][k] + dp[k][j] + nums[i][j][k] = dp[i][k] + dp[k][j] + nums[i]*nums[k]*nums[j]
(注意此时nums[i]和nums[j]还没有被戳破)
定义3个指针i、j和k,i是最外层,从后往前; j是中间层, 从i+2到最后, 对于nums[i]和nums[j]之间的每一个气球nums[k]都戳一戳试试,即
for(int k = i + 1; k < j; k++) {
int temp = dp[i][k] + dp[k][j] + nums2[i] * nums2[k] * nums2[j];
}
因为从nums[i]到nums[j]中间会尝试好多好多次,故不能直接把结果赋给dp[i][j],故再定义一个变量max,当尝试了所有可能的k后,找到最大的temp再赋给dp[i][j]
class Solution:
def maxCoins(self, nums: List[int]) -> int:
nums = [1] + nums + [1] # 因为要戳破两头的气球,故把原数组加长
length = len(nums)
dp = [[0] * length for _ in range(length)] # 建立二维数组,混合了乘法和for循环
for i in range(length - 1, -1, -1): # 从[n-1]取到[0],每次往左走1步
for j in range(i + 2, length): # i是从[i+2]取到[n-1]
for k in range(i + 1, j): # k是从[i+1]取到[j-1]
dp[i][j] = max(dp[i][j], nums[i] * nums[k] * nums[j] + dp[i][k] + dp[k][j]) # 还是Python方便,当k遍历过程中,dp[i][j]始终保持最大
return dp[0][length - 1] # 不戳破nums[0]和nums[length-1],因为本来就是额外添加上去的
难度:困难Hard
idea: 暴力法(brute force)
每一行记录以当前数字结尾的连续1的个数,把每个数字都记录好之后宽也就出来了,不过这只是"高度为1"时的宽,要想求更大的面积还要往上倒,找到"高度为2时的宽",“高度为3时的宽”,来求最大面积
e.g
matrix =
[
[1,0,1,0,0]
[1,0,1,1,1]
[1,1,1,1,1]
[1,0,0,1,0]
]
width =
[
[1,0,1,0,0]
[1,0,1,2,3]
[1,2,3,4,5]
[1,0,0,1,0]
]
例如width[2][2]=3这个点,以它为矩阵右下角的点时,高为1,宽为3;往上倒,当高为2的时候发现以matrix[1][2]为宽就只剩1了,取两个宽的较小值:Math.min(width[2][2],matrix[1][2])=1,求最大面积;
再例如width[2][4]=5这个点,以它为矩阵右下角的点时,高为1,宽为5;往上倒,当高为2的时候发现以matrix[1][4]为宽就只剩3了,取两个宽的较小值:Math.min(width[2][4],matrix[1][4])=3,求最大面积;
/* 暴力法 */
class Solution {
public int maximalRectangle(char[][] matrix) {
if (matrix.length == 0) {
return 0;
}
// 保存以当前数字结尾的连续1的个数
int[][] width = new int[matrix.length][matrix[0].length];
int maxArea = 0;
// 遍历每一行
for (int row = 0; row < matrix.length; row++) { // 行数从上往下
for (int col = 0; col < matrix[0].length; col++) { // 列数从左往右
// 当某一行找到了一个1,看它左边还有多少个连续的1
if (matrix[row][col] == '1') {
if (col == 0) { // 如果是第一列的1
width[row][col] = 1;
} else { // 如果不是第一列的1
width[row][col] = width[row][col - 1] + 1;
}
} else {
width[row][col] = 0;
}
// 先Mark一下以当前数字结尾作为宽
int minWidth = width[row][col];
// 按当前数字同一列往上找更小的值,作为矩阵的宽
for (int up_row = row; up_row >= 0; up_row--) {
int height = row - up_row + 1; // 求一下高度(注意1的高度差)
// 同一列需要找到最小的数作为矩阵的宽
minWidth = Math.min(minWidth, width[up_row][col]);
// 每次更新面积
maxArea = Math.max(maxArea, height * minWidth);
}
}
}
return maxArea;
}
}
idea: 动态规划(DP method)
跟接雨水那题类似,用两个变量leftLessMax[i]和rightLessMin[i]分别记录当下标为i时,紧挨着它的左边和右边的仅次于它的高度,另外再定义一个变量height[i],记录当前下标i对应的1的高度.
e.g
matrix =
[
[1,0,1,0,0]
[1,0,1,1,1]
[1,1,1,1,1]
[1,0,0,1,0]
]
第一行遍历结束: height = [1,0,1,0,0]; 第二行遍历结束: height = [2,0,2,1,1]; 第三行遍历结束: height = [3,1,3,2,2]; 第四行遍历结束: height = [4,0,0,3,0];
leftLessMax赋值结束后:
[
b=-1 L[col]=-1, b=1 L[col]=-1, b=1 L[col]=1, b=3 L[col]=-1, b=4 L[col]=-1
b=-1 L[col]=-1, b=1 L[col]=-1, b=1 L[col]=1, b=1 L[col]=1, b=1 L[col]=1
b=-1 L[col]=-1, b=-1 L[col]=-1, b=-1 L[col]=1, b=-1 L[col]=1, b=-1 L[col]=1
b=-1 L[col]=-1, b=1 L[col]=-1, b=2 L[col]=-1,b=2 L[col]=2, b=4 L[col]=-1
]
每次遇到matrix[row][col]=1时,leftLessMax[col]看左边boundary界限的下标和当前列上一个leftLessMax[col]谁更大点,取较大的;
每次遇到matrix[row][col]=0时,把它作为boundary左边的边界,leftLessMax[col]置-1,不会对当前列下面的所有1有作用了
/* 动态规划 */
class Solution {
public int maximalRectangle(char[][] matrix) {
if (matrix.length == 0) {
return 0;
}
int maxArea = 0;
int cols = matrix[0].length; // cols记录一共有多少列
int[] leftLessMax = new int[cols];
int[] rightLessMin = new int[cols];
// 把每个leftLessMax都初始化为-1,即左边比当前高度height[i]稍矮的是最左边的位置
Arrays.fill(leftLessMax, -1);
// 把每个rightLessMin都初始化为cols,即右边比当前高度height[i]矮的是最右边的位置
Arrays.fill(rightLessMin, cols);
// 按行从上往下遍历,到当前行的某个数字时的当前列的高度
int[] heights = new int[cols];
for(int row = 0; row < matrix.length; row++) {
// 更新所有高度,过程见最上面注释
for (int col = 0; col < cols; col++) {
if (matrix[row][col] == '1') {
heights[col] += 1;
} else {
heights[col] = 0;
}
}
// 更新所有leftLessMax
// 注意: boundary是计算别的leftLessMax用的,而leftLessMax[i]是计算面积用的
int boundary = -1; // 记录上次出现0的位置,即左边的门神
for(int col = 0; col < cols; col++) {
if(matrix[row][col] == '1') {
// 当找到"1"时,拿该列上一个位置的leftLessMax和本行的boundary比较较大值
leftLessMax[col] = Math.max(leftLessMax[col], boundary);
} else {
// 当前是0代表当前坐标没有高度,更谈不少"紧挨着它的左边的较小高度"了,所以初始化为-1,以后不考虑它了
leftLessMax[col] = -1;
//更新0的位置
boundary = col;
}
}
// 更新所有rightLessMin
boundary = cols;
for (int col = cols - 1; col >= 0; col--) {
if (matrix[row][col] == '1') {
rightLessMin[col] = Math.min(rightLessMin[col], boundary);
} else {
rightLessMin[col] = cols;
boundary = col;
}
}
// 更新所有面积
for (int col = cols - 1; col >= 0; col--) {
// 矩形的高为heights[col](纵向),宽为rightLessMin[col]-leftLessMax[col]-1(横向)
int area = (rightLessMin[col] - leftLessMax[col] - 1) * heights[col];
maxArea = Math.max(area, maxArea);
}
}
return maxArea;
}
}
难度:困难Hard
idea: 动态规划(DP method)
状态dp[i][j]: 代表word1从[0]到[i]和word2从[0]到[j]的编辑距离是多少
状态转移方程,分两种情况: (word1和word2都从后往前看)
1.当word1[i] == word2[j]时, 例如word1=“abcd”,word2=“xyd”,则word1[3]==word2[2],则当前没有新增编辑距离,看各自前一位的状态,即 dp[3][2]=dp[2][i]
2.当word1[i] != wrod2[j]时, 例如word1=“horse”, word2=“ros”,则需要增加编辑距离了:
a.状态1: dp[i-1][j],相当于打掉word1的最后一个字母(编辑距离+1),即word1=“hors”,比较word1[0…i-1]和word2[0…j]的编辑距离: dp[i-1][j],或者是给word2增加一个word1的最后一个字母(编辑距离+1),即word2=“rose”,也是一样的.
b.状态2: dp[i][j-1],相当于打掉word2的最后一个字母(编辑距离+1),即word2=“ro”,比较word1[0…i]和word2[0…j-1]的编辑距离: dp[i][j-1],或者是给word1增加一个word2的最后一个字母(编辑距离+1),即word1=“horses”,也是一样的.
c.状态3: dp[i-1][j-1],相当于更改word1的最后一个字母或更改word2的最后一个字母(编辑距离+1),即word1="horss"了,或者word2="roe"了,比较word1[0…i-1]和word2[0…j-1]的编辑距离: dp[i-1][j-1]
class Solution {
public:
int minDistance(string word1, string word2) {
int m = word1.size(), n = word2.size();
vector<vector<int>> dp(m + 1, vector<int>(n + 1, 0)); // 建立二维数组(考虑到word1或word2有可能为0,故多申请一个空间),行为m+1,列为n+1,全部填充0
for(int i = 1; i <= m; i++) { // 当word2为空时,word1的长度即编辑距离的大小
dp[i][0] = i;
}
for(int j = 1; j <= n; j++) {
dp[0][j] = j;
}
// 自底至顶填充
for(int i = 1; i <= m; i++) {
for (int j = 1; j <= n; j++) {
if (word1[i - 1] == word2[j - 1]) { // 因为dp[i][j]多申请了一行空间,故dp[i][j]的状态是看word1[i-1]和word2[j-1]是否相等
dp[i][j] = dp[i - 1][j - 1];
} else {
dp[i][j] = min(dp[i - 1][j - 1], min(dp[i][j - 1], dp[i - 1][j])) + 1;
}
}
}
return dp[m][n]; // 返回最后一个状态
}
};
难度:中等Medium
idea: 动态规划(DP method)
状态dp[i][j]: 代表走到grid[i][j]格子的最小花费;
状态转移方程: 由于走到grid[i][j]格子只能从上面grid[i-1][j]或者左边grid[i][j-1]走过来,故取代表二者状态dp[i-1][j]和dp[i][j-1]的较小值,再加上当前格子的花费grid[i][j]即可:
dp[i][j] = min(dp[i-1][j], dp[i][j-1]) + grid[i][j]
class Solution {
public:
int minPathSum(vector<vector<int>>& grid) {
int m = grid.size();
int n = grid[0].size();
vector<vector<int>> dp(m, vector<int>(n, grid[0][0])); // 定义m行n列的数组,赋初值grid[0][0]
// 首列赋初值,每个格子都只能从上面走过来
for (int i = 1; i < m; i++)
{
dp[i][0] = dp[i - 1][0] + grid[i][0];
}
// 首行赋初值,每个格子都只能从左边走过来
for (int j = 1; j < n; j++)
{
dp[0][j] = dp[0][j - 1] + grid[0][j];
}
// 一般情况,去左边和上面状态的较小者加当前格子的花费,即为当前格子的状态
for (int i = 1; i < m; i++)
{
for (int j = 1; j < n; j++)
{
dp[i][j] = min(dp[i - 1][j], dp[i][j - 1]) + grid[i][j];
}
}
return dp[m - 1][n - 1];
}
};
难度:中等Medium
idea: 动态规划(DP method)
由题意只到某一个格子[i][j]只能从上面[i-1][j]和左边[i][j-1]走过来,故走到当前格子的方法数为二者之和
状态dp[i][j]: 代表走到第[i][j]格子的方法数
状态转移方程: dp[i][j] = dp[i-1][j] + dp[i][j-1]
class Solution {
public:
int uniquePaths(int m, int n) {
vector<vector<int>> dp(m, vector<int>(n, 1)); // 建立一个m*n的二维矩阵dp,全都赋初值1
// 由于在第一行走和在第一列走都只有1中方法,故不用再特殊赋初值了
for (int i = 1; i < m; i++) {
for (int j = 1; j < n; j++) {
dp[i][j] = dp[i - 1][j] + dp[i][j - 1];
}
}
return dp[m - 1][n - 1];
}
};
idea: 动态规划(DP method)
状态dp[i]: nums从[0]到[i-1]的和是多少, dp数组的大小比nums多1
状态转移方程: dp[i+1] = dp[i] + nums[i]
求从nums[i]到nums[j]的和是多少,包含nums[i]和nums[j], dp[i]是从nums[0]到nums[i-1]的和,dp[j+1]是从nums[0]到nums[j]的和,中间的差正好是从nums[i]到nums[j]
class NumArray {
private:
vector<int> dp; // 因为vector是动态大小的,故不用定义大小
public:
NumArray(vector<int>& nums) {
dp.push_back(0);
for(int num : nums)
dp.push_back(dp.back() + num);
}
int sumRange(int left, int right) {
return dp[right + 1] - dp[left];
}
};
/**
* Your NumArray object will be instantiated and called as such:
* NumArray* obj = new NumArray(nums);
* int param_1 = obj->sumRange(left,right);
*/
难度:中等Medium
idea: 动态规划(DP method)
状态dp[i]: 以A[i]结尾的等差序列个数,
状态转移方程
1.若找到新的A[i]-A[i-1]==A[i-1]-A[i-2],则dp[i]=dp[i-1]+1,最后把所有的dp都加起来即可
e.g
A = [0,1,2,3,4]
dp[2] = 1, 此时只有[0,1,2]
dp[3] = dp[2] + 1,此时在[0,1,2]的基础上多了一个1,2,3
dp[4] = dp[3] + 1,此时在0,1,2和1,2,3的基础上多了一个[2,3,4],
最后把dp[2]代表的[0,1,2],dp[3]代表的[0,1,2,3]和[1,2,3],dp[4]代表的[0,1,2,3,4],[1,2,3,4]和[2,3,4]都加到一起即可
2.若没找到,则此时的dp[i]置为0
class Solution {
public:
int numberOfArithmeticSlices(vector<int>& A) {
vector<int> dp(A.size()); // 建立dp数组,大小为数组A的大小
int res = 0;
if(A.size() < 3) return res; // 根本构不成3个序列
dp[0] = 0;
dp[1] = 0;
for (int i = 2; i < A.size(); i++)
{
if(A[i - 1] - A[i - 2] == A[i] - A[i - 1])
{
dp[i] = dp[i - 1] + 1;
res += dp[i];
} else {
dp[i] = 0;
}
}
return res;
}
};
难度:中等Medium
idea: 动态规划(DP method)
状态dp[i]: 从s的第1个数到s的第i个数的总的解码方法数,(由于是字符串题目,故多申请一个空间,代表空字符串)
本题采用加腿的思路,主要是看后两位能否单独构成字符串,即看 s [ i − 1 ] 和 s [ i ] s[i-1]和s[i] s[i−1]和s[i],若能构成单独字符串,则 d p [ i ] = d p [ i − 1 ] + d p [ i − 2 ] dp[i]=dp[i-1]+dp[i-2] dp[i]=dp[i−1]+dp[i−2](相当于既可以把 s [ i ] s[i] s[i]加到 s [ 0... i − 1 ] ) s[0...i-1]) s[0...i−1])上,也可以把 s [ i − 1 ] s[i-1] s[i−1]s [ i ] [i] [i]作为整体加到 s [ 0... i − 2 ] s[0...i-2] s[0...i−2]上),若不能单独构成字符串,则 d p [ i ] = d p [ i − 2 ] dp[i] = dp[i-2] dp[i]=dp[i−2](此时相当于把 s [ i − 1 ] s [ i ] s[i-1]s[i] s[i−1]s[i]作为整体加到 s [ 0... i − 2 ] s[0...i-2] s[0...i−2]上),举两个例子:
e.g1: s = "22624"
226:3种方法 2 -> 2 -> 6; 2 -> 26; 22 -> 6;
2262:3种方法 2 -> 2 -> 6 -> 2; 2 -> 26 -> 2; 22 -> 6 -> 2; (即看后两位62,它不能作为一个整体加到前两个数"22"上,故只能把最后一位2加到前三个数"226"上)
22624: 6种方法 2 -> 2 -> 6 -> 2 -> 4 (把2加到2262上)
-> 24 (把24加到226上)
2 -> 26 -> 2 -> 4 (把2加到2262上)
-> 24 (把24加到226上)
22 -> 6 -> 2 -> 4 (把2加到2262上)
-> 24 (把24加到226上)
e.g2: s = "210"
2: 1种方法: 2
21: 2种方法: 2 -> 1; 21 (看后两位"21",既可以把1加到2上,也可以把21当作整体加到空字符串" "上)
210: 1种方法: 2 -> 10 (看后两位"10",只能把它当作整体加到第一位"2"上)
难度:中等Medium
状态dp[i]: 代表以nums[i]结尾的最长的递增子序列的长度, 初始均为1,代表它们本身
状态转移方程: 当遍历到nums[i]时,依次比较从nums[0]到nums[i-1]与nums[i]的大小,若nums[i] > nums[j],则在nums[j]对应的状态dp[j]基础上+1,即可以把nums[i]加在nums[j]后面,可以构成更长的上升子序列,并比较+1后的dp[j]和dp[i]的大小,取较大者.
e.g nums = [1,3,4,5,2,9],
当遍历到nums[5]=9时, 此时dp[0] = 1, dp[1] = 2, dp[2] = 3, dp[3] = 4, dp[4] = 2
nums[5] > nums[0], 更新dp[5]: dp[5] = 2, 即上升序列可以为[1,9]
nums[5] > nums[1], 更新dp[5]: dp[5] = 3, 即上升序列可以为[1,3,9]
nums[5] > nums[2], 更新dp[5]: dp[5] = 4, 即上升序列可以为[1,3,4,9]
nums[5] > nums[3], 更新dp[5]: dp[5] = 5, 即上升序列可以为[1,3,4,5,9]
nums[5] > nums[4], 更新dp[5]: max(dp[4]+1, dp[5]) = max(3, 5) = 5,
class Solution {
public:
int lengthOfLIS(vector<int>& nums) {
const int size = nums.size(); // 前面加个const,代表不能改变了
if (size == 0) { return 0; }
vector<int> dp(size, 1); // 申请大小为size的数组,初始值均为1
int res = 1;
for (int i = 1; i < size; ++i) {
for (int j = 0; j < i; ++j) { // 拿nums[i]分别和nums[0]到nums[j]比
if (nums[j] < nums[i]) {
dp[i] = max(dp[i], dp[j] + 1); // 每次都尝试更新一下dp[i]
}
}
// 最长上升序列不一定是在以nums[n]结尾的数上, dp长度是参差不齐的.
res = max(res, dp[i]); // 要是python就方便了,直接取所有dp[i]的最大值
}
return res;
}
};
状态方程dp[i]: 以nums[i]为结尾的最长上升子序列的长度,初始dp[i]都等于1,意思是所有的数自己也是一个上升子序列
状态转移方程:
对于每一个数nums[i],for循环j从[0]遍历到[i-1],比较nums[i]与每一个nums[j]的大小, 若nums[i]>nums[j], 证明nums[i]可以放在nums[j]的后面,即找到了以nums[i]结尾的更长的上升序列,此时看nums[j]对应的dp[j]加1后是否比当前的dp[i]长,若更长,则更新dp[i], 由于数组大小是参差不齐的,所以最终返回dp数组里最大的(而不是最后一个dp[-1])
e.g nums = [10,9,2,5,3,7,101,18],
当nums[6] = 101时,初始dp[6]=1
此时dp[0]=1, dp[1]=1, dp[2]=1, dp[3]=2, dp[4]=2, dp[5]=3, dp[6]=1
定住nums[6], 依次比较(nums[0]到nums[5])和nums[6]的大小,
nums[6] > nums[0], 可以更新dp[6]了: dp[6] = max(dp[0]+1, dp[6]) = max(1+1, 1) = 2; 此时最长上升子序列为[10,101]
nums[6] > nums[1], 又可以更新dp[6]了: dp[6] = max(dp[1]+1, dp[6]) = max(1+1, 2) = 2; 此时最长上升子序列为[9,101]
nums[6] > nums[2], 又双可以更新dp[6]了: dp[6] = max(dp[2]+1, dp[6]) = max(1+1, 2) = 2; 此时最长上升子序列为[2,101]
nums[6] > nums[3], 又双叒可以更新dp[6]了: dp[6] = max(dp[3]+1, dp[6]) = max(2+1, 2) = 3; 此时最长上升子序列为[2,5,101]
nums[6] > nums[4], 又双叒叕可以更新dp[6]了: dp[6] = max(dp[4]+1, dp[6]) = max(2+1, 3) = 3; 此时最长上升子序列为[2,5,7,101]
依次类推
class Solution:
def lengthOfLIS(self, nums: List[int]) -> int:
if not nums:
return 0
length = len(nums) # 养成好习惯
# dp数组初始化,每个初始都为1,表示每个数本身就是一个上升子序列
dp = [1] * length # c++: vector dp(length, 1)
# 正片开始
# 外层i从[0]到[length-1],对于每一个i,内层j从[0]到[i-1]
for i in range(length):
for j in range(i):
if nums[i] > nums[j]: # 当发现一个nums[i]大于nums[j]时,证明有门,可以更新dp[i]了(即可以把nums[i]放在nums[j]后面,dp[j]变长了1)
dp[i] = max(dp[i], dp[j] + 1)
# 此处也可以用一个数res记录最大长度,每次更新完dp[i]后,再拿它和历史最大res比
return max(dp) # 返回dp里最大的
状态方程:
例如dp[i]=5, cnt[i]=2,即以nums[i]结尾的最长递增子序列长度是5,一共有两个.
状态转移方程: 外层还是从0开始遍历,当遍历到第i个数nums[i]时,内层j从0到i-1. 每发现nums[i] > nums[j]时就可以更新dp[i]和cnt[i]了:
dp[i] = max(dp[i], dp[j] + 1)看看是否有更长的递增子序列了;
cnt[i]的更新分两种情况:
a.当dp[j] + 1 > 原dp[i]时,证明找到了一个更长的递增子序列, 此时更新dp[i],对于cnt[i],直接继承cnt[j]即可: cnt[i]=cnt[j],代表从nums[j]后面接了一个nums[i],还是原来那些递增序列,只不过变长了.
b. 当dp[j] + 1 == 原dp[i]时, 证明之前有dp[j]+1这种长度的子序列了,且以nums[i]结尾的,现在又来了cnt[j]个,就在cnt[i]的基础上加上cnt[j]即可.
举个简单的例子:
e.g nums=[1,3,5,4,7],当遍历到nums[4]=7时
nums[4] > nums[2], 更新dp[4] = dp[2]+1=4; cnt[4]=cnt[2]=1(继承cnt[2]的个数)
nums[4] > nums[3], 再次更新dp[4]: dp[3]+1=dp[4](均为4),证明之前存在dp[3]+1长度的子序列了,此时
更新cnt[4]就需要用新的加上老的: cnt[4] = cnt[4] + cnt[3]
旧的 新的(继承cnt[3]的)
class Solution {
public int findNumberOfLIS(int[] nums) {
int n = nums.length, res = 0, max_len = 0;
int[] len = new int[n], cnt = new int[n];
for(int i = 0; i<n; i++){
len[i] = cnt[i] = 1;
for(int j = 0; j <i ; j++){
if(nums[i] > nums[j]){
if(len[i] == len[j] + 1) cnt[i] += cnt[j]; // 证明以前出现过len[j]+1长度的序列了,总个数为之前cnt[j]+1的个数cnt[i]和新的cnt[j]+1的个数求和
if(len[i] < len[j] + 1){ // 找到新的最长长度的递增子序列了
len[i] = len[j] + 1; // 更新最长长度
cnt[i] = cnt[j]; // cnt直接继承旧的(相当于在nums[j]的后面加了个nums[i],个数还是原来的个数)
}
}
}
if(max_len == len[i]) res += cnt[i]; // 每比较完一组nums[i]就要看看得到的cnt[i]与历史最多那个大,若是相同,就把结果相加.例如以nums[i]结尾的最长递增子序列长度为5,有2个,此时max_len=5,res=2; 后来又找到以nums[j]结尾的最长子序列长度同样为5(都是最长长度),有3个,则res要加上cnt[j]: res = res + cnt[j]
if(max_len < len[i]) {
max_len = len[i];
res = cnt[i];
}
}
return res;
}
}
状态方程dp[i]: 代表以i为结尾的最长上升子序列的长度
首先按每个子元素的第二个数字排序,即按右端点排序,(若右端点一样,左端点大的在后面)
初始长度依旧是1,代表它本身,对每一个子列表intervals[i],用它的左端点intervals[i][0]往前找最接近的右端点intervals[j][1].当找到第一个的时候intervals[j],代表它的右端点intervals[j][1]一定是与当前列表的左端点intervals[i][0]最靠近的,且找到的第一个的左端点intervals[j][0]一定是离它自己的右端点intervals[j][1]较近的.找到左边第一个右端点比它左端点小的就可以了,因为是按右端点排序过的,左边第一个肯定是最接近它的左端点的.
e.g 排序完之后
[[1,2],[6,7],[2.5,1000],[998,1000],[1002,1005]]
初始:dp[0]=1, dp[1]=1, dp[2]=1, dp[3]=1, dp[4]=1
第一轮: dp[0]=1, dp[1]=2,
第二轮: dp[0]=1, dp[1]=2, dp[2]=2,
第三轮: dp[0]=1, dp[1]=2, dp[2]=2, dp[3]=3,
class Solution:
def eraseOverlapIntervals(self, intervals: List[List[int]]) -> int:
n = len(intervals) # 养成好习惯
if n == 0: return 0
dp = [1] * n # 初始赋为1,代表子列表本身
res = 1
intervals.sort(key = lambda a: (a[1],a[0])) # 先按每个子元素的第二个数字排序,若相同,再按第一个数字排序
for i in range(len(intervals)):
for j in range(i - 1, -1, -1):
if intervals[i][0] >= intervals[j][1]:
dp[i] = max(dp[i], dp[j] + 1)
# 由于最开始排序了,故倒着找的时候,找到的第一个一定是最大的数,因此不用往前继续找了
break
dp[i] = max(dp[i], dp[i - 1]) # 想不通这一步在干嘛
res = max(res, dp[i])
return n - res
idea: 贪心算法(greedy method)
假设从前往后遍历,无非就是拿当前的start跟之前的end比,若start比end大,则证明个数+1;
假设从后往前遍历,无非就是拿当前的end跟之前的start比,若end比start小,则证明个数+1;
下面就是看是按start排序还是按end排序了,目的是要防止捣乱的,例如[3,10000]这种,如果按start排,并且从前往后遍历,这种就会排在前面,这是不愿意看到的,故按end排序.
形象地形容一下,当从前往后遍历时,张开双臂,从起点朝向终点,要把尽可能多的子序列收进来,肯定是要让end越小越好.
同理从后往前遍历时,要让[3,10000]这种尽可能放到前面,故按start排即可
class Solution:
def eraseOverlapIntervals(self, intervals: List[List[int]]) -> int:
n = len(intervals) # 养成好习惯
intervals.sort(key = lambda x: x[1]) # 从前往后遍历时,要按end排序
end = -float('inf')
res = 0
for i in intervals: # 从前往后遍历
start = i[0]
if start >= end:
res += 1
end = i[1]
return n - res
idea: 动态规划(DP method)
把所有的数对按升序排序,先按第[0]维排序,若第[0]维相等再看第[1]维.
状态dp[i]: 以第[i]个数对结尾的最长数对链的个数
状态转移方程: 对于第[i]个数对pairs[i],分别拿第[0]个到第[i-1]个数对的尾[1]与它的头[0]进行对比,若头大于尾,则更新dp[i]
class Solution {
public:
int findLongestChain(vector<vector<int>>& pairs) {
if (pairs.empty()) return 0;
// 定义排序数组sort,按从小到大排序;先看第[0]维,即先看头,若头相等,则再看尾
sort(pairs.begin(), pairs.end(), [](const vector<int>& a, const vector<int>& b){
return (a[0] == b[0] && a[1] < b[1]) || (a[0] < b[0]);
});
int n = pairs.size(), res = 0;
vector<int> dp(n, 1);
for(int i = 0; i < n; ++i) {
for(int j = 0; j < i; ++j) {
if(pairs[j][1] < pairs[i][0]) {
dp[i] = max(dp[i], dp[j] + 1);
}
}
res = max(res, dp[i]); // 每看完一轮dp[i]都更新下结果,因为最后dp长度是参差不齐的.
}
return res;
}
};
难度:中等Medium
idea: 动态规划(DP method)
定义两个状态: up[i]: 表示最后两个数字递增的最长摆动序列的长度;
down[i]: 表示最后两个数字递减的摆动序列的长度;
状态转移方程:
当nums[i] > nums[j]时,证明该上升了,更新up[i+1], 找到前一个down[i],再此基础上+1,即up[i+1] = down[i] + 1;
当nums[i] < nums[j]时,证明该下降了,更新down[i+1], 找到前一个up[i],再此基础上+1,即down[i+1] = up[i] + 1;
class Solution {
public:
int wiggleMaxLength(vector<int>& nums) {
int size = nums.size();
if (size == 0) {
return 0;
}
vector<int> up(size, 0); // 定义上升dp数组,初始值均为0
vector<int> down(size, 0); // 定义下降dp数组,初始值均为0
up[0] = 1;
down[0] = 1;
for(int i = 1; i < size; ++i)
{
if (nums[i] > nums[i - 1]) { // 找到了一个上升对,更新up(在前一个down的基础上)
up[i] = down[i - 1] + 1;
down[i] = down[i - 1]; // 下降的不用更新,长度跟以前一样
}
else if (nums[i] < nums[i - 1]) {
down[i] = up[i - 1] + 1;
up[i] = up[i - 1];
}
else { // nums[i]和nums[i-1]相等,都不更新
up[i] = up[i - 1];
down[i] = down[i - 1];
}
}
return max(up[size - 1], down[size - 1]);
}
};
idea: 动态规划(DP method)
因为是两个字符串,类比编辑距离那道题,故要定义二维dp数组,因为是字符串题目,故要多申请一个空间,代表空字符串情况
状态dp[i][j]: 代表text1的第[0…i]和text2的第[0…j]为止公共序列的长度;
状态转移方程:
a.当text1[i]==text2[j]时,证明找到了新的更长的公共子序列,在原来的基础上+1: dp[i][j] = dp[i-1][j-1] + 1
b.当text1[i]!=text2[j]时,text1往里收一位(看text1[0…i-1]和text2[0…j])或者text2往里收一位,即dp[i][j]由dp[i-1][j]或者dp[i][j-1]中的较大者决定
class Solution {
public:
int longestCommonSubsequence(string text1, string text2) {
// 申请二维dp数组,行为text1.size()+1,列为text2.size()+1,初始值均为0,就不用赋边界值了
vector<vector<int>> dp(text1.size() + 1, vector<int>(text2.size() + 1, 0));
for (int i = 1; i <= text1.size(); i++) {
for (int j = 1; j <= text2.size(); j++) {
if (text1[i - 1] == text2[j - 1]) { // 注意字符串题目给定字符串始终比dp下标小1
dp[i][j] = dp[i - 1][j - 1] + 1;
} else {
dp[i][j] = max(dp[i - 1][j], dp[i][j - 1]);
}
}
}
return dp[text1.size()][text2.size()];
}
};
问题描述:
有一个容量为 N 的背包,要用这个背包装下物品的价值最大,这些物品有两个属性:体积 w 和价值 v。
状态dp[i][j]: 表示前i件商品体积最大为j时能获得的最大价值
状态转移方程:根据第[i]件物品是否放入背包中分为两种情况:
// W 为背包总体积
// N 为物品数量
// weights 数组存储 N 个物品的重量
// values 数组存储 N 个物品的价值
public int knapsack(int W, int N, int[] weights, int[] values) {
int[][] dp = new int[N + 1][W + 1];
for (int i = 1; i <= N; i++) {
int w = weights[i - 1], v = values[i - 1];
for (int j = 1; j <= W; j++) {
if (j >= w) {
dp[i][j] = Math.max(dp[i - 1][j], dp[i - 1][j - w] + v);
} else {
dp[i][j] = dp[i - 1][j];
}
}
}
return dp[N][W];
}
难度:中等Medium
idea: 动态规划(DP method)
状态dp[i][j]: 从前nums[0…i]个数中选出一些数,使其和为j
状态转移方程:
类比0-1背包问题,对于每个数nums[i]可以选或者不选
e.g nums = [1,5,10,6], 定义一个4行,12列(和的一半为11)的二维数组:
__,__,__,__,__,__,__,__,__,__,__,__,
__,__,__,__,__,__,__,__,__,__,__,__,
__,__,__,__,__,__,__,__,__,__,__,__,
__,__,__,__,__,__,__,__,__,__,__,__,
开始赋值,第一行代表只能用1,此时除了dp[0][1]是T,其他的都为F(因为用nums[0]自己去填自己肯定是T)
填第二行,只能用[1,5],先把第一行的结果抄下来,即dp[1][1] = T,(因为用[1,5]两个数,去填1,肯定可以);
第二行填两个值: dp[1][5] = dp[0]5 || dp[0]0,两个均为false,故dp[1][5] = F;
dp[1][6] = dp[0]6 || dp[0]1,两个均为true,故dp[1][5] = T;
class Solution {
public boolean canPartition(int[] nums) {
int len = nums.length;
// 把给定的数组加起来,并且得到其值的一半,作为背包的最大容积
int sum = 0;
for (int num : nums) {
sum += num;
}
// 若sum为奇数,则不能二等分
if ((sum & 1) == 1) { // 奇数时"101101" & "1" = 1
return false;
}
int target = sum / 2;
// 类比0-1背包问题,创建二维状态数组,行:物品索引(本题为数字下标),列:容量(本题为总和target)
boolean[][] dp = new boolean[len][target + 1];
// 先填表格第0行,第1个数只能让容积为它自己的背包恰好装满
if (nums[0] <= target) {
dp[0][nums[0]] = true;
}
// 再填表格后面几行
for (int i = 1; i < len; i++) {
for (int j = 0; j <= target; j++) {
// 直接从上一行先把结果抄下来,然后再修正
dp[i][j] = dp[i - 1][j];
if (nums[i] == j) { // 终于找到了,之前的i-1个数都不用了,就用nums[i]这一个
dp[i][j] = true;
continue;
}
if (nums[i] < j) {
dp[i][j] = dp[i - 1][j] || dp[i - 1][j - nums[i]];
}
}
}
return dp[len - 1][target];
}
}
难度:中等Medium
状态dp[i][j]:从[0…i-1]个数能凑出[j]的方法数
状态转移方程:当当前的数字1选择+号时,剩下的数字需要凑够的和为:j+nums[i],当当前的数字选择-号时,剩下的数字需要凑够的和为:j-nums[i]
class Solution {
public static int findTargetSumWays(int[] nums, int s) {
int sum = 0;
for (int i = 0; i < nums.length; i++) {
sum += nums[i];
}
// 绝对值范围超过了sum的绝对值范围则无法得到
if (Math.abs(s) > Math.abs(sum)) return 0;
int len = nums.length;
// - 0 +
int t = sum * 2 + 1;
int[][] dp = new int[len][t];
// 初始化
if (nums[0] == 0) {
dp[0][sum] = 2;
} else {
dp[0][sum + nums[0]] = 1;
dp[0][sum - nums[0]] = 1;
}
for (int i = 1; i < len; i++) {
for (int j = 0; j < t; j++) {
// 边界
int l = (j - nums[i]) >= 0 ? j - nums[i] : 0;
int r = (j + nums[i]) < t ? j + nums[i] : 0;
dp[i][j] = dp[i - 1][l] + dp[i - 1][r];
}
}
return dp[len - 1][sum + s];
}
}
idea: 动态规划(DP method)
定义状态dp[i][j][k], 从第[0]个到底[i]个字符串用的字符串包含[j]个0[k]个1的个数的最大字符串个数
状态转移方程: 对于一个新的字符串dp[i],其包含ones个[1]和zeros个[0],用它(上一个状态为dp[i - 1][j - zeros][k - ones]),最大字符串个数+1,或者不用dp[i - 1][j][k]个数较多的
class Solution {
public int findMaxForm(String[] strs, int m, int n) {
int len = strs.length;
int[][][] dp = new int[len + 1][m + 1][n + 1];
for (int i = 1; i <= len; i++) {
// 注意:有一位偏移
int[] count = countZeroAndOne(strs[i - 1]);
for (int j = 0; j <= m; j++) {
for (int k = 0; k <= n; k++) {
// 先把上一行抄下来
dp[i][j][k] = dp[i - 1][j][k];
int zeros = count[0];
int ones = count[1];
if (j >= zeros && k >= ones) {
dp[i][j][k] = Math.max(dp[i - 1][j][k], dp[i - 1][j - zeros][k - ones] + 1);
}
}
}
}
return dp[len][m][n];
}
// 对于一个新的字符串,统计"0"和"1"的个数
private int[] countZeroAndOne(String str) {
int[] cnt = new int[2];
for (char c : str.toCharArray()) {
cnt[c - '0']++;
}
return cnt;
}
}
idea: 动态规划(DP method)
状态dp[i]: 当剩余总金额为i时凑够它需要的最小硬币数;
状态转移方程:
因为对于从1到amount每一个金额,都要用所有的硬币去尝试,故代表amount的for循环在外层,代表coins的for循环在内层.
dp[i] = min(dp[i], dp[i-coin]+1), 即对于每一个coin都要尝试更新dp[i]
/*动态规划*/
class Solution {
public:
int coinChange(vector<int>& coins, int amount) {
int Max = amount + 1;
vector<int> dp(amount + 1, Max); // 即对于每一个金额i,都用1块的硬币来买
dp[0] = 0; // 对于金额0,不需要硬币凑,置为0
for (int i = 1; i <= amount; i++) { // 对于从1到amount的每一个金额i
for (int j = 0; j < coins.size(); j++) { // 用每一个硬币去尝试,然后更新dp[i]
if (coins[j] <= i) {
dp[i] = min(dp[i], dp[i - coins[j]] + 1);
}
}
}
return dp[amount] > amount ? -1 : dp[amount]; // 因为dp的初始值为amount+1,若还是该值证明dp[amount]未更新
}
};
idea: 动态规划(DP method)
状态dp[i][j]: 代表用前i个硬币凑出面额为j的方法数
状态转移方程: 对于第i个硬币的金额coins[i],若剩余面额大于coins[i],代表可以用它,为dp[i][j - coins[i - 1]],也可以不用:dp[i - 1][j]; 若剩余面额小于coins[i],就肯定没法用,为dp[i-1][j]
class Solution {
public:
int change(int amount, vector<int>& coins) {
int K = coins.size() + 1;
int I = amount + 1;
vector<vector<int>> dp(K, vector<int>(I, 0)); // 表示用前k个硬币凑出金额为i的组合数
// 初始化基本状态
for (int k = 0; k < coins.size() + 1; k++){
dp[k][0] = 1;
}
for(int j = 1; j <= amount; j++){ // 对于某一个剩余金额i
for (int i = 1; i <= coins.size() ; i++) { // 遍历所有硬币coins[k-1]
if (j >= coins[i - 1]) {
dp[i][j] = dp[i][j - coins[i - 1]] + dp[i - 1][j]; // 剩余金额大于硬币
} else {
dp[i][j] = dp[i - 1][j];
}
}
}
return dp[coins.size()][amount];
}
};
idea: 动态规划(DP method)
这是个完全背包问题,
状态dp[i]: 剩余字符串长度为i时是否能拆分;
状态转移方程: 当剩余字符串长度为i时,遍历字典中的每一个单词word,看两个东西:
class Solution {
public:
bool wordBreak(string s, vector<string>& wordDict) {
if(wordDict.size()==0) return false;
set<string> dict;
for(auto w: wordDict)
dict.insert(w);
vector<bool> dp(s.size() + 1,false);
dp[0] = true;
for(int i = 1; i <= s.size(); i++)
{
for(int j = i - 1; j >= 0; j--)
{
if(dp[j])
{
string word = s.substr(j, i - j);
if(dict.find(word) != dict.end())
{
dp[i] = true;
break; // 跳出当前i,遍历下一个
}
}
}
}
return dp[s.size()];
}
};
难度:中等Medium
idea: 动态规划(DP method)
状态dp[i][j]: 用nums的前i个数去凑剩余目标为j的组合个数
状态转移方程: 对于某个剩余目标j,尝试所有的nums数字: nums0, nums1…把它们所有都加起来,即dp[i][j]的总组合数.
可以理解成爬楼梯,到达第[j]阶的总方法数可以由[j-nums[0]],[j-nums[1]]等等爬上来,把它们加起来即可
class Solution:
def combinationSum4(self, nums: List[int], target: int) -> int:
if not nums:
return 0
dp = [0] * (target + 1)
dp[0] = 1
for i in range(1, target + 1):
for num in nums:
if i >= num:
dp[i] += dp[i-num] # 相当于爬楼梯问题,到第i阶可以由i-nums[0],i-nums[1],i-nums[2]...爬上来
return dp[target]
难度:中等Medium
idea: 动态规划(DP method)
定义状态dp[i][j]: word1从[0]到[i-1]和word2从[0]到[j-1]相同时所需的最小步数
状态转移方程:
class Solution {
public int minDistance(String word1, String word2) {
char[] ch1 = word1.toCharArray(), ch2 = word2.toCharArray(); // Java麻烦的一点就在于要把字符串变成字符串数组
int len1 = word1.length(), len2 = word2.length();
int[][] dp = new int[len1 + 1][len2 + 1]; // 字符串题目要多申请一个空间,代表空字符串
// 初始化dp数组,当word1或者word2为空字符串时
for (int i = 1; i <= len1; i++) dp[i][0] = i; // 注意word1从[0]到[i-1]是i个字母,不是i-1个
for (int j = 1; j <= len2; j++) dp[0][j] = j;
// 一般情况,分两种
for (int i = 1; i <= len1; i++) {
for (int j = 1; j <= len2; j++) {
if (ch1[i - 1] == ch2[j - 1]) { // 注意dp数组下标始终比word1下标多1,因为dp[0]代表空字符串,而word1[0]代表第一个字母
dp[i][j] = dp[i - 1][j - 1];
} else {
dp[i][j] = Math.min(dp[i - 1][j] + 1, dp[i][j - 1] + 1);
}
}
}
return dp[len1][len2];
}
}
难度:困难Hard
idea: 动态规划(DP method)
状态dp[i][j]: 代表word1从[0]到[i]和word2从[0]到[j]的编辑距离是多少
状态转移方程,分两种情况: (word1和word2都从后往前看)
1.当word1[i] == word2[j]时, 例如word1=“abcd”,word2=“xyd”,则word1[3]==word2[2],则当前没有新增编辑距离,看各自前一位的状态,即 dp[3][2]=dp[2][i]
2.当word1[i] != wrod2[j]时, 例如word1=“horse”, word2=“ros”,则需要增加编辑距离了:
a.状态1: dp[i-1][j],相当于打掉word1的最后一个字母(编辑距离+1),即word1=“hors”,比较word1[0…i-1]和word2[0…j]的编辑距离: dp[i-1][j],或者是给word2增加一个word1的最后一个字母(编辑距离+1),即word2=“rose”,也是一样的.
b.状态2: dp[i][j-1],相当于打掉word2的最后一个字母(编辑距离+1),即word2=“ro”,比较word1[0…i]和word2[0…j-1]的编辑距离: dp[i][j-1],或者是给word1增加一个word2的最后一个字母(编辑距离+1),即word1=“horses”,也是一样的.
c.状态3: dp[i-1][j-1],相当于更改word1的最后一个字母或更改word2的最后一个字母(编辑距离+1),即word1="horss"了,或者word2="roe"了,比较word1[0…i-1]和word2[0…j-1]的编辑距离: dp[i-1][j-1]
class Solution {
public:
int minDistance(string word1, string word2) {
int m = word1.size(), n = word2.size();
vector<vector<int>> dp(m + 1, vector<int>(n + 1, 0)); // 建立二维数组(考虑到word1或word2有可能为0,故多申请一个空间),行为m+1,列为n+1,全部填充0
for(int i = 1; i <= m; i++) { // 当word2为空时,word1的长度即编辑距离的大小
dp[i][0] = i;
}
for(int j = 1; j <= n; j++) {
dp[0][j] = j;
}
// 自底至顶填充
for(int i = 1; i <= m; i++) {
for (int j = 1; j <= n; j++) {
if (word1[i - 1] == word2[j - 1]) { // 因为dp[i][j]多申请了一行空间,故dp[i][j]的状态是看word1[i-1]和word2[j-1]是否相等
dp[i][j] = dp[i - 1][j - 1];
} else {
dp[i][j] = min(dp[i - 1][j - 1], min(dp[i][j - 1], dp[i - 1][j])) + 1;
}
}
}
return dp[m][n]; // 返回最后一个状态
}
};
idea: 动态规划(DP method)
状态dp[i][j]: 当前有i个A,剪切板里此时有j个A了
状态转移方程: dp[i][j]=min(dp[i-j][j]+1, dp[i][j])要得到dp[i][j],当此时有i-j个A时,剪切板里有j个A,很显然就粘贴一下就ok了,即dp[i][j] = dp[i-j][j]+1,后面解答就看不懂了…
class Solution {
public:
/*650. 只有两个键的键盘 ctrl+c ctrl+v*/
//思路,满足最优子结构,无后效性,重复子结构,可考虑动态规划
//阶段: n阶问题,每阶有j个子问题,既可以在已复制j个的基础上粘贴
//状态: 粘贴之前必须复制(限定条件),复制有j个'A' 当前有‘i’个'A'
//状态转移方程:dp[i][j]=min(dp[i-j][j]+1,dp[i][j]);
//边界条件,每复制一次j个"A"要用一次操作,当i==j的时候可以全部复制
int minSteps(int n) {
if (n == 0)
return 0;
vector<vector<int>> dp(n + 1);
for (int i = 0; i <= n; i++)
{
dp[i].resize(n + 1,n);//因为是求最小值,这里随便初始化一个值,但是要足够大,分析可知就算是每次只粘贴一个,那么最多操作数是n
}
dp[1][1] = 0; //初始状态
for (int i = 1; i <= n; i++)
{
int minNum = dp[i][1]; //纪录第二层循环的最小值 下面会用到
for (int j = 1; j <=i; j++)
{
if (i - j >= 1) //注意不要越界
{
dp[i][j] = min(dp[i - j][j] + 1, dp[i][j]); //i个A 要在i-j个A的基础上粘贴j个A
minNum = min(minNum, dp[i][j]);
}
if (i == j) // 当i=j的时候 代表复制所有的"A" 肯定是在最小操作数的基础上复制,所以前面要纪录最小值
dp[i][j] = minNum + 1;
}
}
return dp[n][n]-1;
}
};