改题单选自灵神:分享丨【题单】动态规划(入门/背包/状态机/划分/区间/状压/数位/树形/数据结构优化) - 力扣(LeetCode)
一、入门 DP
1.0 题目清单
1. 爬楼梯
2. 打家劫舍
1.1 爬楼梯类
70. 爬楼梯
题目传送门:70. 爬楼梯 - 力扣(LeetCode)
简单递推直接秒了,最后实现代码如下:
1 2 3 4 5 6 7 8 9 10 11 class Solution {public : int climbStairs (int n) { vector<int > dp (n+1 ,0 ) ; dp[0 ] = 1 ; dp[1 ] = 1 ; for (int i=2 ;i<=n;++i){ dp[i] = dp[i-1 ] + dp[i-2 ]; } return dp[n]; } };
746. 使用最小花费爬楼梯
题目传送门:746. 使用最小花费爬楼梯 - 力扣(LeetCode)
同上,递推即可。
最后实现代码如下:
1 2 3 4 5 6 7 8 9 10 11 class Solution {public : int minCostClimbingStairs (vector<int >& cost) { int n = cost.size (); vector<int > dp (n+1 ,0 ) ; for (int i=2 ;i<=n;++i){ dp[i] = min (dp[i-1 ]+cost[i-1 ], dp[i-2 ]+cost[i-2 ]); } return dp[n]; } };
377. 组合总和 Ⅳ
题目传送门:377. 组合总和 Ⅳ - 力扣(LeetCode)
此题类似一个完全背包问题,这里需要注意一下内外循环顺序,先循环target再循环给定数组中的数。
最后实现代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 class Solution {public : int combinationSum4 (vector<int >& nums, int target) { vector<unsigned > dp (target+1 ,0 ) ; dp[0 ] = 1 ; int n = nums.size (); for (int i=1 ; i<=target; ++i){ for (int j=0 ; j<n; ++j){ if (nums[j] > i) continue ; dp[i] += dp[i-nums[j]]; } } return dp[target]; } };
2466. 统计构造好字符串的方案数
题目传送门:2466. 统计构造好字符串的方案数 - 力扣(LeetCode)
跟上面那道题目思路一样。
最后实现代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 class Solution {public : int countGoodStrings (int low, int high, int zero, int one) { vector<int > dp (1e5 +2 ,0 ) ; dp[0 ] = 1 ; int MOD = 1e9 +7 ; for (int i=1 ;i<=high;++i){ if (i>=zero) dp[i] += dp[i-zero]; if (i>=one) dp[i] += dp[i-one]; dp[i] %= MOD; } int ans = 0 ; for (int i=low; i<=high; ++i){ ans = (ans + dp[i]) % MOD; } return ans; } };
2266. 统计打字方案数
题目传送门:2266. 统计打字方案数 - 力扣(LeetCode)
仍然是爬楼梯类问题,但是并不是那种可以直接解决的,可以采用如下方式解决:
将相同字符分为一组,每组内只有一种字符。考虑以下动态规划(DP)方法:
对于字符不为7或9的情况,定义( f[i] )表示长度为( i )的只有一种字符的字符串对应的文字信息种类数。我们可以将末尾的1个、2个或3个字符单独视作一个字母,那么有转移方程:
f [ i ] = f [ i − 1 ] + f [ i − 2 ] + f [ i − 3 ] f[i] = f[i-1] + f[i-2] + f[i-3]
f [ i ] = f [ i − 1 ] + f [ i − 2 ] + f [ i − 3 ]
对于字符为7或9的情况,定义( g[i] )表示长度为( i )的只有一种字符的字符串对应的文字信息种类数,可以得到类似的转移方程:
g [ i ] = g [ i − 1 ] + g [ i − 2 ] + g [ i − 3 ] + g [ i − 4 ] g[i] = g[i-1] + g[i-2] + g[i-3] + g[i-4]
g [ i ] = g [ i − 1 ] + g [ i − 2 ] + g [ i − 3 ] + g [ i − 4 ]
这样能算出每组字符串的文字信息种类数。
由于不同组之间互不影响,根据乘法原理,把不同组的文字信息种类数相乘,得到答案。
最后实现代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 class Solution {public : int countTexts (string pressedKeys) { int MOD = 1e9 +7 , n = pressedKeys.size (); vector<long long > dp_3 (100003 ,0 ) ,dp_4 (100003 ,0 ) ; dp_3[0 ] = 1 ; dp_3[1 ] = 1 ; dp_3[2 ] = 2 ; dp_3[3 ] = 4 ; dp_4[0 ] = 1 ; dp_4[1 ] = 1 ; dp_4[2 ] = 2 ; dp_4[3 ] = 4 ; for (int i=4 ;i<=n;++i) { dp_3[i] = dp_3[i-1 ] + dp_3[i-2 ] + dp_3[i-3 ]; dp_4[i] = dp_4[i-1 ] + dp_4[i-2 ] + dp_4[i-3 ] + dp_4[i-4 ]; dp_3[i] %= MOD; dp_4[i] %= MOD; } int cnt = 1 ; long long ans = 1 ; char ch = pressedKeys[0 ]; for (int i=1 ;i<n;++i){ if (pressedKeys[i]!=ch){ if (ch == '7' ||ch == '9' ) ans = (dp_4[cnt] * ans) % MOD; else ans = (dp_3[cnt] * ans) % MOD; cnt = 1 ; ch = pressedKeys[i]; } else { cnt++; } } int tmp; if (ch == '7' ||ch == '9' ) tmp = dp_4[cnt]; else tmp = dp_3[cnt]; return ((ans*tmp)%MOD); } };
1.2 打家劫舍
198. 打家劫舍
题目传送门:198. 打家劫舍 - 力扣(LeetCode)
定义动态规划数组dp[i]
表示偷到第i
个房子,所能盗取的最大金额。
那么不难写出状态转移方程:
1 dp[i] = max (dp[i-1 ],dp[i-2 ]+nums[i-1 ]);
最后确定一下初始状态dp[0] = 0,dp[1] = nums[0]
,完成解答。
最后实现代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 class Solution {public : int rob (vector<int >& nums) { int n = nums.size (); vector<int > dp (n+2 , 0 ) ; dp[1 ] = nums[0 ]; for (int i=2 ;i<=n;++i){ dp[i] = max (dp[i-1 ],dp[i-2 ]+nums[i-1 ]); } return dp[n]; } };
740. 删除并获得点数
题目传送门:740. 删除并获得点数
观察数据范围后不难发现就是上面打家劫舍这种类型的题目,排序后,动态规划同上一题目。
最后实现代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 class Solution {public : int deleteAndEarn (vector<int >& nums) { int n = nums.size (); vector<int >nums_all (10004 , 0 ), dp (10004 , 0 ); sort (nums.begin (), nums.end ()); int prev = nums[0 ], cnt = 0 ; for (int i=0 ;i<n;++i){ if (nums[i]==prev) cnt++; else { nums_all[prev] = cnt * prev; cnt = 1 ; prev = nums[i]; } } nums_all[prev] = prev * cnt; dp[1 ] = nums_all[1 ]; for (int i=2 ;i<=nums[n-1 ];++i){ dp[i] = max (dp[i-1 ], dp[i-2 ]+nums_all[i]); } return dp[nums[n-1 ]]; } };
2320. 统计放置房子的方式数
题目传送门:2320. 统计放置房子的方式数 - 力扣(LeetCode)
观察每个位置的放置状态一共有4种情况:
上无下无
上有下无
上无下有
上有下有
因此可以设计一个dp[n][4]
动态规划数组,对于当前每一种状态,都可以从上一个位置其他状态推导而来,最后的答案就是最后一个位置的所有4种状态之和。
最后实现代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 class Solution {public : int countHousePlacements (int n) { int MOD = 1e9 + 7 ; vector<vector<int >> dp (10002 ,vector <int >(4 ,0 )); dp[1 ][0 ] = 1 ; dp[1 ][1 ] = 1 ; dp[1 ][2 ] = 1 ; dp[1 ][3 ] = 1 ; for (int i=2 ;i<=n;++i){ dp[i][1 ] = (dp[i-1 ][0 ] + dp[i-1 ][2 ])%MOD; dp[i][2 ] = (dp[i-1 ][0 ] + dp[i-1 ][1 ])%MOD; dp[i][3 ] = dp[i-1 ][0 ]; dp[i][0 ] = ((dp[i-1 ][0 ]+dp[i-1 ][1 ])%MOD + (dp[i-1 ][2 ]+dp[i-1 ][3 ])%MOD)%MOD; } return ((dp[n][0 ]+dp[n][1 ])%MOD + (dp[n][2 ]+dp[n][3 ])%MOD)%MOD; } };
213. 打家劫舍 II
题目传送门:213. 打家劫舍 II - 力扣(LeetCode)
此题就是之前的打家劫舍但是将顺序的结构改成了环形的结构。为了防止同时选到第一个和最后一个,可以做两遍DP。
注意:
最后实现代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 class Solution {public : int rob (vector<int >& nums) { int n = nums.size (); vector<int > dp1 (n+2 ,0 ) , dp2 (n+2 ,0 ) ; if (n == 1 ) return nums[0 ]; dp1[1 ] = 0 ; for (int i=2 ;i<=n;++i){ dp1[i] = max (dp1[i-1 ], dp1[i-2 ]+nums[i-1 ]); } dp2[1 ] = nums[0 ]; for (int i=2 ;i<n;++i){ dp2[i] = max (dp2[i-1 ], dp2[i-2 ]+nums[i-1 ]); } return max (dp1[n], dp2[n-1 ]); } };
1.3 最大子数组和(最大子段和)
53. 最大子数组和
题目传送门:53. 最大子数组和 - 力扣(LeetCode)
不同于之前的DP,这里定义的DP数组dp[i]
含义是,以第i个数结尾 的连续子数组最大和(因为条件中包含连续 这一要求,因此只有这样才能描述状态转移方程)
最后实现代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 class Solution {public : int maxSubArray (vector<int >& nums) { int n = nums.size (); vector<int > dp (n+2 ,0 ) ; dp[1 ] = nums[0 ]; for (int i=1 ;i<=n;++i){ dp[i] = max (dp[i-1 ]+nums[i-1 ],nums[i-1 ]); } int ans = 0 ; for (int i=1 ;i<=n;++i) ans = max (ans,dp[i]); return ans; } };
2606. 找到最大开销的子字符串
题目传送门:2606. 找到最大开销的子字符串 - 力扣(LeetCode)
思路同上
最后实现代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 class Solution {public : int maximumCostSubstring (string s, string chars, vector<int >& vals) { int n = s.size (); vector<int > dp (n+2 ,0 ) ; unordered_map<char ,int > mp; int m = chars.size (); for (int i=0 ; i<26 ; ++i) mp['a' +i] = i+1 ; for (int i=0 ; i<m; ++i) mp[chars[i]] = vals[i]; for (int i=1 ;i<=n;++i) { int val = mp[s[i-1 ]]; dp[i] = max (dp[i-1 ]+val, val); } int ans = dp[0 ]; for (int i=1 ;i<=n;++i) ans = max (ans, dp[i]); return ans; } };
1749. 任意子数组和的绝对值的最大值
题目传送门:1749. 任意子数组和的绝对值的最大值 - 力扣(LeetCode)
思路上和之前一样,
最后实现代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 class Solution {public : int maxAbsoluteSum (vector<int >& nums) { int n = nums.size (); vector<vector<int >> dp (n+1 ,vector <int >(2 ,0 )); for (int i=1 ;i<=n;++i){ dp[i][0 ] = max (dp[i-1 ][0 ]+nums[i-1 ],nums[i-1 ]); dp[i][1 ] = min (dp[i-1 ][1 ]+nums[i-1 ],nums[i-1 ]); } int ans = 0 ; for (int i=1 ;i<=n;++i){ ans = max (ans,max (-dp[i][1 ], dp[i][0 ])); } return ans; } };
这道题这样做可能还麻烦了一些,还可以利用前缀和的思想进行简化。具体而言就是找到这个数组中前缀和最大和前缀和最小,相减就可以得出想要的答案。
1 2 3 4 5 6 7 8 9 10 11 12 13 class Solution {public : int maxAbsoluteSum (vector<int >& nums) { int maxx = 0 , minn = 0 ; int nw=0 ; for (int num : nums){ nw += num; maxx = max (maxx, nw); minn = min (minn, nw); } return maxx - minn; } };
1191. K 次串联后最大子数组之和
题目传送门:1191. K 次串联后最大子数组之和 - 力扣(LeetCode)
此题定义的DP数组(实际未定义,使用一个变量优化掉了)同之前一样,也是表现的是以当前数结尾的最大子数组和。
这里需要注意的是题目中的k其实是吓人的,我们仔细分析一下便可得知:
当k为1时,同最大子数组和
当k为2时,由于存在特殊情况(选第一个数组后缀+第二个数组的前缀)将数组拼接在一起,同理可以转变为最大子数组和
当k大于2时,相比上一种情况,要多考虑,如果出现横跨数组的情况的话,相当于中间数组的和加上单个数组的最大前缀和与最大后缀和。
综上分三类讨论即可。实际上没有将数组扩展k倍。
最后实现代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 class Solution {public : int kConcatenationMaxSum (vector<int >& arr, int k) { int n = arr.size (); int MOD = 1e9 + 7 ; long long maxx = 0 ,minn = 0 ,sum_arr = 0 ; for (int i=0 ;i<n;++i){ sum_arr += arr[i]; maxx = max (maxx, sum_arr); minn = min (minn, sum_arr); } if (k!=1 ) arr.insert (arr.end (),arr.begin (),arr.end ()); int m = arr.size (); long long tmp = 0 ,ans=0 ; for (int i=0 ;i<m;++i){ tmp = max (tmp,(long long )(0 )) + arr[i]; ans = max (ans, tmp); } if (k<=2 ) return ans%MOD; else return max (ans, (k-2 )*sum_arr + maxx + sum_arr-minn)%MOD; } };
918. 环形子数组的最大和
题目传送门:918. 环形子数组的最大和 - 力扣(LeetCode)
同样还是分类讨论的思想。
对于非环形数组的情况,其解决方法类似于寻找最大子数组和 ,这里不再详述。而对于环形数组,可以将问题转化为寻找最大的前缀和与后缀和 ,这两部分不能有交集。通过计算每个位置的最大前后缀和,可以解决环形问题。因此,整体问题可视为求解“最大子数组和 ”与“最大前后缀和 ”的较大者。最终,比较这两个结果,返回较大的值即可。
最后实现代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 class Solution {public : int maxSubarraySumCircular (vector<int >& nums) { int ans1=nums[0 ], tmp=0 ; for (auto num : nums){ tmp = max (num, num + tmp); ans1 = max (tmp,ans1); } int n = nums.size (); vector<int > pre (n+1 ,0 ) ,pos (n+1 ,0 ) ; int pre_tmp = 0 , pos_tmp = 0 ; for (int i=1 ;i<=n;++i){ pre_tmp += nums[i-1 ]; if (i>1 ) pre[i] = max (pre[i-1 ], pre_tmp); else pre[i] = pre_tmp; pos_tmp += nums[n-i]; if (i>1 ) pos[i] = max (pos[i-1 ], pos_tmp); else pos[i] = pos_tmp; } int ans2 = pre[1 ]+pos[n-1 ]; for (int i=1 ;i<n;++i){ ans2 = max (ans2,pre[i]+pos[n-i]); } return max (ans1, ans2); } };
2321. 拼接数组的最大分数
题目传送门:2321. 拼接数组的最大分数 - 力扣(LeetCode)
我们将两个数组作差得到两个差值数组diff1
和diff2
,仔细观察后可以将此题转化为最大子数组和 这个问题,
最后实现代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 class Solution {public : int maximumsSplicedArray (vector<int >& nums1, vector<int >& nums2) { int n = nums1.size (),sum1=0 ,sum2=0 ; int max2=0 ,max1=0 ,tmp1=0 ,tmp2=0 ; for (int i=0 ;i<n;++i) { sum1 += nums1[i]; sum2 += nums2[i]; tmp1 = max (tmp1, 0 ) + nums1[i] - nums2[i]; tmp2 = max (tmp2, 0 ) + nums2[i] - nums1[i]; max1 = max (max1, tmp1); max2 = max (max2, tmp2); } return max (sum1 + max2, sum2 + max1); } };
152. 乘积最大子数组
题目传送门:152. 乘积最大子数组 - 力扣(LeetCode)
该题作为最大子数组和 的一个扩展,还是非常有意思,多维护一个最小值就可以。
注意:最大值同理也可以状态转移到最小值,最小值同理也可以状态转移到最大值 (比如当前位置是一个负数,就可能发生上述转化)
这里面有一个很“有趣”的测试用例:
1 [0,10,10,10,10,10,10,10,10,10,-10,10,10,10,10,10,10,10,10,10,0]
这个用例很有趣,最大会有 10^19 要存储,加上符号位是65位,刚好不够 long long 来存。而double虽然也是64位,但它的数据结构有所不同,采用 1符号+11指数+52 尾数的方式,最多可以存到 2^1024 的大数。虽然在 52位二进制数以上的精度不能保证,但这题由题目保证最多只用到32位,再多的只是为了满足不溢出。 所以,我们将临时变量由 int 改为 double,即可通过此用例。
最后实现代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 class Solution {public : int maxProduct (vector<int >& nums) { double ans=nums[0 ], max_now=nums[0 ],min_now=nums[0 ]; int n = nums.size (); for (int i=1 ;i<n;++i){ double max_tmp = max_now, min_tmp = min_now; max_now = max (max_now*nums[i], max ((double )nums[i],min_tmp*nums[i])); min_now = min (min_now*nums[i], min ((double )nums[i],max_tmp*nums[i])); ans = max (ans,max_now); } return int (ans); } };
二、网格图 DP
2.0 题目清单
1. 基础
2.1 基础
LCR 166. 珠宝的最高价值
题目传送门:LCR 166. 珠宝的最高价值 - 力扣(LeetCode)
网格DP求最大值题,网格图DP基础板子题,从左侧和上侧进行状态转移即可。
最后实现代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 class Solution {public : int jewelleryValue (vector<vector<int >>& frame) { int n = frame.size (), m = frame[0 ].size (); vector<vector<int >> dp (n+1 ,vector <int >(m+1 ,0 )); for (int i=1 ;i<=n;++i){ for (int j=1 ;j<=m;++j){ dp[i][j] = max (dp[i][j-1 ], dp[i-1 ][j]) + frame[i-1 ][j-1 ]; } } return dp[n][m]; } };
62. 不同路径
题目传送门:62. 不同路径 - 力扣(LeetCode)
网格DP方案数题,同上,网格图DP基础板子题,从左侧和上侧进行状态转移即可。
注意一下初始化即可。
最后实现代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 class Solution {public : int uniquePaths (int m, int n) { vector<vector<int >> dp (m+1 ,vector <int >(n+1 , 0 )); dp[1 ][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][n]; } };
63. 不同路径 II
题目传送门:63. 不同路径 II - 力扣(LeetCode)
网格DP方案数题,在上一道题的基础上增加了障碍物的设定,只需要在每次循环的时候判定当前是不是障碍物即可,如果是就不更新。
最后实现代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 class Solution {public : int uniquePathsWithObstacles (vector<vector<int >>& obstacleGrid) { int n = obstacleGrid.size (), m = obstacleGrid[0 ].size (); vector<vector<int >> dp (n+1 ,vector <int >(m+1 ,0 )); dp[1 ][1 ] = obstacleGrid[0 ][0 ]==0 ?1 :0 ; for (int i=1 ;i<=n;++i){ for (int j=1 ;j<=m;++j){ if (obstacleGrid[i-1 ][j-1 ]) continue ; dp[i][j] += dp[i-1 ][j] + dp[i][j-1 ]; } } return dp[n][m]; } };
64. 最小路径和
题目传送门:64. 最小路径和 - 力扣(LeetCode)
网格DP求最小值题,上面已经遇到了方案数量 和求最大 的设定,此题为一个求最小 设定。
最后实现代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 class Solution {public : int minPathSum (vector<vector<int >>& grid) { int n = grid.size (), m = grid[0 ].size (); vector<vector<int >> dp (n+1 ,vector <int >(m+1 , INT_MAX/2 )); dp[0 ][1 ] = 0 ; dp[1 ][0 ] = 0 ; for (int i=1 ;i<=n;++i){ for (int j=1 ;j<=m;++j){ dp[i][j] = min (dp[i-1 ][j], dp[i][j-1 ]) + grid[i-1 ][j-1 ]; } } return dp[n][m]; } };
120. 三角形最小路径和
题目传送门:120. 三角形最小路径和 - 力扣(LeetCode)
网格DP求最小值题,上一道题的简单改版,从矩形变成了三角形,最后判断一下最后一行最小的值即可。
备注:顺便可以看一下min_element
的用法
最后实现代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 class Solution {public : int minimumTotal (vector<vector<int >>& triangle) { int n = triangle.size (); vector<vector<int >> dp (n+1 , vector <int >(n+1 , (INT_MAX/2 ))); dp[1 ][1 ] = triangle[0 ][0 ]; for (int i=2 ;i<=n;++i){ for (int j=1 ;j<=i;++j){ dp[i][j] = triangle[i-1 ][j-1 ] + min (dp[i-1 ][j-1 ] , dp[i-1 ][j]); } } return *min_element (dp[n].begin (), dp[n].end ()); } };
931. 下降路径最小和
题目传送门:931. 下降路径最小和 - 力扣(LeetCode)
网格DP求最小值题,同上,从上一行相邻三个转移,使用上一行三个值中的最小值加上当前位置值即可。
最后实现代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 class Solution {public : int minFallingPathSum (vector<vector<int >>& matrix) { int n = matrix.size (); vector<vector<int >> dp (n+1 , vector <int >(n+2 , INT_MAX/2 )); for (int i=1 ;i<=n;++i) dp[0 ][i] = 0 ; for (int i=1 ;i<=n;++i){ for (int j=1 ;j<=n;++j){ dp[i][j] = min (dp[i-1 ][j], min (dp[i-1 ][j-1 ], dp[i-1 ][j+1 ])) + matrix[i-1 ][j-1 ]; } } return *min_element (dp[n].begin ()+1 , dp[n].begin ()+1 +n); } };
2684. 矩阵中移动的最大次数
题目传送门:2684. 矩阵中移动的最大次数 - 力扣(LeetCode)
网格DP求最大值题,同上,只不过需要在更新的过程中时刻维护一个全局步数的最大值即可。
最后实现代码如下: