算法--动态规划
动态规划
- 算法原理
- 经典例题
- 入门
- 1137. [第 N 个泰波那契数](https://leetcode.cn/problems/n-th-tribonacci-number/)
- [面试题 08.01. 三步问题](https://leetcode.cn/problems/three-steps-problem-lcci/)
- 746. [使用最小花费爬楼梯](https://leetcode.cn/problems/min-cost-climbing-stairs/)
- [91. 解码方法](https://leetcode.cn/problems/decode-ways/description/)
- 不同路径问题
- [62. 不同路径](https://leetcode.cn/problems/unique-paths/)
- [63. 不同路径 II](https://leetcode.cn/problems/unique-paths-ii/description/)
- [LCR 166. 珠宝的最高价值](https://leetcode.cn/problems/li-wu-de-zui-da-jie-zhi-lcof/description/)
- [931. 下降路径最小和](https://leetcode.cn/problems/minimum-falling-path-sum/description/)
- [64. 最小路径和](https://leetcode.cn/problems/minimum-path-sum/description/)
- [174. 地下城游戏](https://leetcode.cn/problems/dungeon-game/)
- 简单多状态
- [面试题 17.16. 按摩师](https://leetcode.cn/problems/the-masseuse-lcci/description/)
- [213. 打家劫舍 II](https://leetcode.cn/problems/house-robber-ii/description/)
- [740. 删除并获得点数](https://leetcode.cn/problems/delete-and-earn/description/)
- [LCR 091. 粉刷房子](https://leetcode.cn/problems/JEj789/)
- [309. 买卖股票的最佳时机含冷冻期](https://leetcode.cn/problems/best-time-to-buy-and-sell-stock-with-cooldown/description/)
- [714. 买卖股票的最佳时机含手续费](https://leetcode.cn/problems/best-time-to-buy-and-sell-stock-with-transaction-fee/description/)
- [123. 买卖股票的最佳时机 III](https://leetcode.cn/problems/best-time-to-buy-and-sell-stock-iii/description/)
- [188. 买卖股票的最佳时机 IV](https://leetcode.cn/problems/best-time-to-buy-and-sell-stock-iv/description/)
- 子数组问题
- [53. 最大子数组和](https://leetcode.cn/problems/maximum-subarray/)
- [918. 环形子数组的最大和](https://leetcode.cn/problems/maximum-sum-circular-subarray/description/)
- [152. 乘积最大子数组](https://leetcode.cn/problems/maximum-product-subarray/)
- [1567. 乘积为正数的最长子数组长度](https://leetcode.cn/problems/maximum-length-of-subarray-with-positive-product/description/)
- [413. 等差数列划分](https://leetcode.cn/problems/arithmetic-slices/description/)
- [978. 最长湍流子数组](https://leetcode.cn/problems/longest-turbulent-subarray/description/)
- [139. 单词拆分](https://leetcode.cn/problems/word-break/description/)
- [467. 环绕字符串中唯一的子字符串](https://leetcode.cn/problems/unique-substrings-in-wraparound-string/description/)
- 子序列问题
- [300. 最长递增子序列](https://leetcode.cn/problems/longest-increasing-subsequence/)
- [376. 摆动序列](https://leetcode.cn/problems/wiggle-subsequence/)
- [673. 最长递增子序列的个数](https://leetcode.cn/problems/number-of-longest-increasing-subsequence/)
- [646. 最长数对链](https://leetcode.cn/problems/maximum-length-of-pair-chain/description/)
- [1218. 最长定差子序列](https://leetcode.cn/problems/longest-arithmetic-subsequence-of-given-difference/)
- [873. 最长的斐波那契子序列的长度](https://leetcode.cn/problems/length-of-longest-fibonacci-subsequence/)
- [1027. 最长等差数列](https://leetcode.cn/problems/longest-arithmetic-subsequence/description/)
- [446. 等差数列划分 II - 子序列](https://leetcode.cn/problems/arithmetic-slices-ii-subsequence/description/)
- 回文串问题
- [647. 回文子串](https://leetcode.cn/problems/palindromic-substrings/)
- [5. 最长回文子串](https://leetcode.cn/problems/longest-palindromic-substring/description/)
- [1745. 分割回文串 IV](https://leetcode.cn/problems/palindrome-partitioning-iv/)
- [132. 分割回文串 II](https://leetcode.cn/problems/palindrome-partitioning-ii/)
- [516. 最长回文子序列](https://leetcode.cn/problems/longest-palindromic-subsequence/)
- [1312. 让字符串成为回文串的最少插入次数](https://leetcode.cn/problems/minimum-insertion-steps-to-make-a-string-palindrome/)
- 两个数组的dp问题
- [1143. 最长公共子序列](https://leetcode.cn/problems/longest-common-subsequence/description/)
- [1035. 不相交的线](https://leetcode.cn/problems/uncrossed-lines/description/)
- [115. 不同的子序列](https://leetcode.cn/problems/distinct-subsequences/description/)
- [44. 通配符匹配](https://leetcode.cn/problems/wildcard-matching/description/)
- [10. 正则表达式匹配](https://leetcode.cn/problems/regular-expression-matching/description/)
- [97. 交错字符串](https://leetcode.cn/problems/interleaving-string/description/)
- [712. 两个字符串的最小ASCII删除和](https://leetcode.cn/problems/minimum-ascii-delete-sum-for-two-strings/)
- [718. 最长重复子数组](https://leetcode.cn/problems/maximum-length-of-repeated-subarray/)
- 01背包
- [DP41 【模板】01背包](https://www.nowcoder.com/practice/fd55637d3f24484e96dad9e992d3f62e?tpId=230&tqId=2032484&ru=/exam/oj&qru=/ta/dynamic-programming/question-ranking&sourceUrl=/exam/oj?page=1&tab=%25E7%25AE%2597%25E6%25B3%2595%25E7%25AF%2587&topicId=196)
- [416. 分割等和子集](https://leetcode.cn/problems/partition-equal-subset-sum/)
- [494. 目标和](https://leetcode.cn/problems/target-sum/description/)
- [1049. 最后一块石头的重量 II](https://leetcode.cn/problems/last-stone-weight-ii/description/)
- 完全背包
- [DP42 【模板】完全背包](https://www.nowcoder.com/practice/237ae40ea1e84d8980c1d5666d1c53bc?tpId=230&tqId=2032575&ru=/exam/oj&qru=/ta/dynamic-programming/question-ranking&sourceUrl=/exam/oj?page=1&tab=%25E7%25AE%2597%25E6%25B3%2595%25E7%25AF%2587&topicId=196)
- [322. 零钱兑换](https://leetcode.cn/problems/coin-change/)
- [518. 零钱兑换 II](https://leetcode.cn/problems/coin-change-ii/description/)
- [279. 完全平方数](https://leetcode.cn/problems/perfect-squares/)
- 二维费用的背包问题
- [474. 一和零](https://leetcode.cn/problems/ones-and-zeroes/)
- [879. 盈利计划](https://leetcode.cn/problems/profitable-schemes/description/)
- 其它
- [377. 组合总和 Ⅳ](https://leetcode.cn/problems/combination-sum-iv/description/)
- [96. 不同的二叉搜索树](https://leetcode.cn/problems/unique-binary-search-trees/description/)
算法原理
动态规划解决问题的流程为:
-
状态表示
一般采用一个表格dp记录问题解决过程中不同的状态,状态表的具体含义需要根据具体问题和经验分析确定 -
确定状态转移方程
得到不同状态之间的关系 -
初始化
填写已知的状态表部分和处理一些细节部分 -
填写状态表
根据状态转移方程填写状态表 -
获取结果
根据已经填写的状态表返回结果
以上是解决动态规划问题的一般流程,有时候我们可能仅仅只需要特定的几个状态就可以解决问题,此时我们就可以进行一定的空间优化,即用几个变量表示状态,从而将时间复杂度降到O(1)。
经典例题
入门
1137. 第 N 个泰波那契数
泰波那契序列 Tn 定义如下:
T0 = 0, T1 = 1, T2 = 1, 且在 n >= 0 的条件下 Tn+3 = Tn + Tn+1 + Tn+2
给你整数 n,请返回第 n 个泰波那契数 Tn 的值。
class Solution {
public:
int tribonacci(int n) {
//细节处理
if(n<2){
return n;
}
//状态表示
vector<int> dp(n+1,0);
//初始化
dp[1]=dp[2]=1;
//填写状态表
int i=3;
for(;i<=n;++i){
//转移方程
dp[i]=dp[i-1]+dp[i-2]+dp[i-3];
}
//返回结果
return dp[n];
}
};
下面考虑空间优化:
class Solution {
public:
int tribonacci(int n) {
if(n == 0) return 0;
if(n == 1 || n == 2) return 1;
int a = 0, b = 1, c = 1, d = 0;
for(int i = 3; i <= n; i++)
{
d = a + b + c;
a = b; b = c; c = d;
}
return d;
}
};
面试题 08.01. 三步问题
三步问题。有个小孩正在上楼梯,楼梯有n阶台阶,小孩一次可以上1阶、2阶或3阶。实现一种方法,计算小孩有多少种上楼梯的方式。结果可能很大,你需要对结果模1000000007。
class Solution {
public:
int waysToStep(int n) {
vector<int> count(n+2,0);
count[0]=1;
count[1]=2;
count[2]=4;
int i=3;
for(;i<n;++i){
count[i]=((count[i-3]+count[i-2])%1000000007+count[i-1])%1000000007;
}
return count[n-1];
}
};
746. 使用最小花费爬楼梯
给你一个整数数组 cost ,其中 cost[i] 是从楼梯第 i 个台阶向上爬需要支付的费用。一旦你支付此费用,即可选择向上爬一个或者两个台阶。
你可以选择从下标为 0 或下标为 1 的台阶开始爬楼梯。
请你计算并返回达到楼梯顶部的最低花费。
class Solution {
public:
int minCostClimbingStairs(vector<int>& cost) {
if(2==cost.size()){
return min(cost[0],cost[1]);
}
vector<int> dp(cost.size()+1,0);
int i=2;
for(;i<dp.size();++i){
dp[i]=min(cost[i-1]+dp[i-1],cost[i-2]+dp[i-2]);
}
return dp.back();
}
};
91. 解码方法
一条包含字母 A-Z 的消息通过以下映射进行了 编码 :
“1” -> ‘A’
“2” -> ‘B’
…
“25” -> ‘Y’
“26” -> ‘Z’
然而,在 解码 已编码的消息时,你意识到有许多不同的方式来解码,因为有些编码被包含在其它编码当中(“2” 和 “5” 与 “25”)。
例如,“11106” 可以映射为:
“AAJF” ,将消息分组为 (1, 1, 10, 6)
“KJF” ,将消息分组为 (11, 10, 6)
消息不能分组为 (1, 11, 06) ,因为 “06” 不是一个合法编码(只有 “6” 是合法的)。
注意,可能存在无法解码的字符串。
给你一个只含数字的 非空 字符串 s ,请计算并返回 解码 方法的 总数 。如果没有合法的方式解码整个字符串,返回 0。
题目数据保证答案肯定是一个 32 位 的整数。
class Solution {
public:
int numDecodings(string s) {
if('0'==s[0]){
return 0;
}
if(1==s.size()){
return 1;
}
vector<int> dp(s.size(),0);
dp[0]=1;
if('0'!=s[1]){
dp[1]++;
}
if(10*(s[0]-'0')+s[1]-'0'<27){
dp[1]++;
}
if(0==dp[1]){
return 0;
}
int i=2;
for(;i<s.size();++i){
if('0'==s[i-1]&&'0'==s[i]){
return 0;
}
if('0'!=s[i]){
dp[i]+=dp[i-1];
}
if('1'==s[i-1]||('2'==s[i-1]&&s[i]<'7')){
dp[i]+=dp[i-2];
}
}
return dp.back();
}
};
不同路径问题
62. 不同路径
一个机器人位于一个 m x n 网格的左上角 (起始点在下图中标记为 “Start” )。
机器人每次只能向下或者向右移动一步。机器人试图达到网格的右下角(在下图中标记为 “Finish” )。
问总共有多少条不同的路径?
class Solution {
public:
int uniquePaths(int m, int n) {
vector<vector<int>> path(m+1,vector<int>(n+1,0));
path[0][1]=1;
int i=1;
int j=1;
for(;i<m+1;++i){
for(j=1;j<n+1;++j){
path[i][j]=path[i-1][j]+path[i][j-1];
}
}
return path[m][n];
}
};
63. 不同路径 II
给定一个 m x n 的整数数组 grid。一个机器人初始位于 左上角(即 grid[0][0])。机器人尝试移动到 右下角(即 grid[m - 1][n - 1])。机器人每次只能向下或者向右移动一步。
网格中的障碍物和空位置分别用 1 和 0 来表示。机器人的移动路径中不能包含 任何 有障碍物的方格。
返回机器人能够到达右下角的不同路径数量。
测试用例保证答案小于等于 2 * 109。
class Solution {
public:
int uniquePathsWithObstacles(vector<vector<int>>& obstacleGrid) {
vector<vector<int>> dp(obstacleGrid.size()+1,vector<int>(obstacleGrid[0].size()+1,0));
dp[0][1]=1;
int i=0;
int j=0;
for(i=1;i<dp.size();++i){
for(j=1;j<dp[0].size();++j){
if(1==obstacleGrid[i-1][j-1]){
continue;
}
dp[i][j]=dp[i-1][j]+dp[i][j-1];
}
}
return dp.back().back();
}
};
LCR 166. 珠宝的最高价值
现有一个记作二维矩阵 frame 的珠宝架,其中 frame[i][j] 为该位置珠宝的价值。拿取珠宝的规则为:
只能从架子的左上角开始拿珠宝
每次可以移动到右侧或下侧的相邻位置
到达珠宝架子的右下角时,停止拿取
注意:珠宝的价值都是大于 0 的。除非这个架子上没有任何珠宝,比如 frame = [[0]]。
class Solution {
public:
int jewelleryValue(vector<vector<int>>& frame) {
vector<vector<int>> dp(frame.size()+1,vector<int>(frame[0].size()+1,0));
int i=0;
int j=0;
for(i=1;i<dp.size();++i){
for(j=1;j<dp[0].size();++j){
dp[i][j]=max(dp[i-1][j],dp[i][j-1])+frame[i-1][j-1];
}
}
return dp.back().back();
}
};
931. 下降路径最小和
给你一个 n x n 的 方形 整数数组 matrix ,请你找出并返回通过 matrix 的下降路径 的 最小和 。
下降路径 可以从第一行中的任何元素开始,并从每一行中选择一个元素。在下一行选择的元素和当前行所选元素最多相隔一列(即位于正下方或者沿对角线向左或者向右的第一个元素)。具体来说,位置 (row, col) 的下一个元素应当是 (row + 1, col - 1)、(row + 1, col) 或者 (row + 1, col + 1) 。
class Solution {
public:
int minFallingPathSum(vector<vector<int>>& matrix) {
int i=1;
int j=1;
for(i=1;i<matrix.size();++i){
for(j=0;j<matrix[0].size();++j){
int tmp=matrix[i-1][j];
if(0!=j){
tmp=min(tmp,matrix[i-1][j-1]);
}
if(j+1!=matrix[0].size()){
tmp=min(tmp,matrix[i-1][j+1]);
}
matrix[i][j]+=tmp;
}
}
int ans=INT_MAX;
for(i=matrix.size()-1,j=0;j<matrix[0].size();++j){
ans=min(ans,matrix[i][j]);
}
return ans;
}
};
64. 最小路径和
给定一个包含非负整数的 m x n 网格 grid ,请找出一条从左上角到右下角的路径,使得路径上的数字总和为最小。
说明:每次只能向下或者向右移动一步。
class Solution {
public:
int minPathSum(vector<vector<int>>& grid) {
int i=1;
int j=1;
for(i=0;i<grid.size();++i){
for(j=0;j<grid[0].size();++j){
int tmp=0;
if(0!=i&&0!=j){
tmp=min(grid[i-1][j],grid[i][j-1]);
}
else if(0!=i){
tmp=grid[i-1][j];
}else if(0!=j){
tmp=grid[i][j-1];
}
grid[i][j]+=tmp;
}
}
return grid.back().back();
}
};
174. 地下城游戏
恶魔们抓住了公主并将她关在了地下城 dungeon 的 右下角 。地下城是由 m x n 个房间组成的二维网格。我们英勇的骑士最初被安置在 左上角 的房间里,他必须穿过地下城并通过对抗恶魔来拯救公主。
骑士的初始健康点数为一个正整数。如果他的健康点数在某一时刻降至 0 或以下,他会立即死亡。
有些房间由恶魔守卫,因此骑士在进入这些房间时会失去健康点数(若房间里的值为负整数,则表示骑士将损失健康点数);其他房间要么是空的(房间里的值为 0),要么包含增加骑士健康点数的魔法球(若房间里的值为正整数,则表示骑士将增加健康点数)。
为了尽快解救公主,骑士决定每次只 向右 或 向下 移动一步。
返回确保骑士能够拯救到公主所需的最低初始健康点数。
注意:任何房间都可能对骑士的健康点数造成威胁,也可能增加骑士的健康点数,包括骑士进入的左上角房间以及公主被监禁的右下角房间。
这里dp[ i ][ j ]应表示从[ i ][ j ]到公主房间所需的最少建康点数,而不是骑士到[ i ][ j ]所需的最少建康点数,如果dp[ i ][ j ]表示骑士到[ i ][ j ]所需的最少建康点数,现在开始考虑到dungeon[ i ][ j ],那么dp[ i ][ j ]前面的状态如dp[ i-1 ][ j ]应该发生修改,才可以保证骑士可以顺利地从dungeon[ i-1 ][ j ]到dungeon[ i ][ j ],其状态转移方程就难以确定。
但如果dp[ i ][ j ]应表示从[ i ][ j ]到公主房间所需的最少建康点数,就算现在开始考虑到dungeon[ i ][ j ],dp[ i ][ j ]后面的状态也不发生改变。
class Solution {
public:
int calculateMinimumHP(vector<vector<int>>& dungeon) {
vector<vector<int>> dp(dungeon.size()+1,vector<int>(dungeon[0].size()+1,INT_MAX));
int i=dungeon.size()-1;
int j=dungeon[0].size()-1;
dp[i+1][j]=1;
for(i=dungeon.size()-1;i>=0;--i){
for(j=dungeon[0].size()-1;j>=0;--j){
dp[i][j]=min(dp[i+1][j],dp[i][j+1])-dungeon[i][j];
if(dp[i][j]<=0){
dp[i][j]=1;
}
}
}
return dp[0][0];
}
};
简单多状态
面试题 17.16. 按摩师
一个有名的按摩师会收到源源不断的预约请求,每个预约都可以选择接或不接。在每次预约服务之间要有休息时间,因此她不能接受相邻的预约。给定一个预约请求序列,替按摩师找到最优的预约集合(总预约时间最长),返回总的分钟数。
注意:本题相对原题稍作改动
dp[ i ]表示接受预约nums[ i ]后nums[ 0 ] 到 nums[ i ]预约的最大时长,故有状态转移方程:
dp[i]=max(dp[i-3],dp[i-2])+nums[i];
由于最后一个预约不一定选上,故最后返回:
max(dp[dp.size()-1],dp[dp.size()-2]);
class Solution {
public:
int massage(vector<int>& nums) {
vector<int> dp(nums.size()+3,0);
int i=0;
for(i=0;i<nums.size();++i){
dp[i+3]=max(dp[i],dp[i+1])+nums[i];
}
return max(dp[i+1],dp[i+2]);
}
};
213. 打家劫舍 II
你是一个专业的小偷,计划偷窃沿街的房屋,每间房内都藏有一定的现金。这个地方所有的房屋都 围成一圈 ,这意味着第一个房屋和最后一个房屋是紧挨着的。同时,相邻的房屋装有相互连通的防盗系统,如果两间相邻的房屋在同一晚上被小偷闯入,系统会自动报警 。
给定一个代表每个房屋存放金额的非负整数数组,计算你 在不触动警报装置的情况下 ,今晚能够偷窃到的最高金额。
设共有房屋n间,我们分为两类情况:
①偷 0 号房屋:2 到 n-2 号房屋就变成了 打家劫舍 I 问题,求解得到最大值 x
②不偷 0 号房屋:1 到 n-1 号房屋就变成了 打家劫舍 I 问题,求解得到最大值 y
返回max(x,y)即可
class Solution {
public:
int rob1(vector<int>& nums,int start,int end){
if(end<start){
return 0;
}
if(start==end){
return nums[start];
}
vector<int> dp(end-start+4,0);
int i=0;
for(i=0;start+i<=end;++i){
dp[i+3]=max(dp[i],dp[i+1])+nums[start+i];
}
return max(dp[i+1],dp[i+2]);
}
int rob(vector<int>& nums) {
int x=nums[0]+rob1(nums,2,nums.size()-2);
int y=rob1(nums,1,nums.size()-1);
return max(x,y);
}
};
740. 删除并获得点数
给你一个整数数组 nums ,你可以对它进行一些操作。
每次操作中,选择任意一个 nums[i] ,删除它并获得 nums[i] 的点数。之后,你必须删除 所有 等于 nums[i] - 1 和 nums[i] + 1 的元素。
开始你拥有 0 个点数。返回你能通过这些操作获得的最大点数。
由于点数都是非负整数,可以用一个数组v,v[ i ]表示nums中 元素 i 的和,这样对于数组v来说就变成了一个 打家劫舍I 问题
class Solution {
public:
int deleteAndEarn(vector<int>& nums) {
int maxElement=0;
int i=0;
for(i=0;i<nums.size();++i){
maxElement=max(maxElement,nums[i]);
}
vector<int> v(maxElement+1,0);
for(i=0;i<nums.size();++i){
v[nums[i]]+=nums[i];
}
vector<int> dp(v.size()+3,0);
for(i=0;i<v.size();++i){
dp[i+3]=max(dp[i],dp[i+1])+v[i];
}
return max(dp[i+1],dp[i+2]);
}
};
LCR 091. 粉刷房子
假如有一排房子,共 n 个,每个房子可以被粉刷成红色、蓝色或者绿色这三种颜色中的一种,你需要粉刷所有的房子并且使其相邻的两个房子颜色不能相同。
当然,因为市场上不同颜色油漆的价格不同,所以房子粉刷成不同颜色的花费成本也是不同的。每个房子粉刷成不同颜色的花费是以一个 n x 3 的正整数矩阵 costs 来表示的。
例如,costs[0][0] 表示第 0 号房子粉刷成红色的成本花费;costs[1][2] 表示第 1 号房子粉刷成绿色的花费,以此类推。
请计算出粉刷完所有房子最少的花费成本。
class Solution {
public:
int minCost(vector<vector<int>>& costs) {
int i=0;
for(i=1;i<costs.size();++i){
costs[i][0]+=min(costs[i-1][1],costs[i-1][2]);
costs[i][1]+=min(costs[i-1][0],costs[i-1][2]);
costs[i][2]+=min(costs[i-1][0],costs[i-1][1]);
}
i=costs.size()-1;
return min(costs[i][0],min(costs[i][1],costs[i][2]));
}
};
309. 买卖股票的最佳时机含冷冻期
给定一个整数数组prices,其中第 prices[i] 表示第 i 天的股票价格 。设计一个算法计算出最大利润。在满足以下约束条件下,你可以尽可能地完成更多的交易(多次买卖一支股票):
卖出股票后,你无法在第二天买入股票 (即冷冻期为 1 天)。
注意:你不能同时参与多笔交易(你必须在再次购买前出售掉之前的股票)。
class Solution {
public:
int maxProfit(vector<int>& prices) {
vector<vector<int>> dp(prices.size()+1,vector<int>(3,0));
dp[0][0]=-prices[0];
int i=0;
for(i=1;i<=prices.size();++i){
dp[i][0]=max(dp[i-1][0],dp[i-1][2]-prices[i-1]);
dp[i][1]=dp[i-1][0]+prices[i-1];
dp[i][2]=max(dp[i-1][2],dp[i-1][1]);
}
return max(dp[i-1][1],dp[i-1][2]);
}
};
714. 买卖股票的最佳时机含手续费
给定一个整数数组 prices,其中 prices[i]表示第 i 天的股票价格 ;整数 fee 代表了交易股票的手续费用。
你可以无限次地完成交易,但是你每笔交易都需要付手续费。如果你已经购买了一个股票,在卖出它之前你就不能再继续购买股票了。
返回获得利润的最大值。
注意:这里的一笔交易指买入持有并卖出股票的整个过程,每笔交易你只需要为支付一次手续费。
class Solution {
public:
int maxProfit(vector<int>& prices, int fee) {
vector<vector<int>> dp(prices.size()+1,vector<int>(2,0));
int i=1;
dp[0][0]=-prices[0];
for(i=1;i<=prices.size();++i){
dp[i][0]=max(dp[i-1][0],dp[i-1][1]-prices[i-1]);
dp[i][1]=max(dp[i-1][1],dp[i-1][0]+prices[i-1]-fee);
}
return dp[i-1][1];
}
};
123. 买卖股票的最佳时机 III
给定一个数组,它的第 i 个元素是一支给定的股票在第 i 天的价格。
设计一个算法来计算你所能获取的最大利润。你最多可以完成 两笔 交易。
注意:你不能同时参与多笔交易(你必须在再次购买前出售掉之前的股票)。
class Solution {
public:
int maxProfit(vector<int>& prices) {
int m=-0x3f3f3f3f;
vector<vector<int>> dp1(prices.size()+1,{m,m,m});
vector<vector<int>> dp2(prices.size()+1,{m,m,m});
dp1[0][0]=-prices[0];
dp2[0][0]=0;
int i=0;
for(i=0;i<prices.size();++i){
int j=0;
for(j=0;j<3;++j){
dp1[i+1][j]=max(dp1[i][j],dp2[i][j]-prices[i]);
if(j>0){
dp2[i+1][j]=max(dp2[i][j],dp1[i][j-1]+prices[i]);
}else{
dp2[i+1][j]=dp2[i][j];
}
}
}
return max(dp2[i][0],max(dp2[i][1],dp2[i][2]));
}
};
188. 买卖股票的最佳时机 IV
class Solution {
public:
int maxProfit(int k, vector<int>& prices) {
int m=-0x3f3f3f3f;
vector<vector<int>> dp1(prices.size()+1,vector<int>(k+1,m));
vector<vector<int>> dp2(prices.size()+1,vector<int>(k+1,m));
dp1[0][0]=-prices[0];
dp2[0][0]=0;
int i=0;
for(i=0;i<prices.size();++i){
int j=0;
for(j=0;j<=k;++j){
dp1[i+1][j]=max(dp1[i][j],dp2[i][j]-prices[i]);
if(j>0){
dp2[i+1][j]=max(dp2[i][j],dp1[i][j-1]+prices[i]);
}else{
dp2[i+1][j]=dp2[i][j];
}
}
}
int ans=0;
for(auto e:dp2[i]){
ans=max(ans,e);
}
return ans;
}
};
子数组问题
53. 最大子数组和
给你一个整数数组 nums ,请你找出一个具有最大和的连续子数组(子数组最少包含一个元素),返回其最大和。
子数组是数组中的一个连续部分。
class Solution {
public:
int maxSubArray(vector<int>& nums) {
vector<int> dp(nums.size()+1,0);
int i=0;
int ans=INT_MIN;
for(i=0;i<nums.size();++i){
dp[i+1]=dp[i]<0?nums[i]:dp[i]+nums[i];
ans=max(ans,dp[i+1]);
}
return ans;
}
};
918. 环形子数组的最大和
给定一个长度为 n 的环形整数数组 nums ,返回 nums 的非空 子数组 的最大可能和 。
环形数组 意味着数组的末端将会与开头相连呈环状。形式上, nums[i] 的下一个元素是 nums[(i + 1) % n] , nums[i] 的前一个元素是 nums[(i - 1 + n) % n] 。
子数组 最多只能包含固定缓冲区 nums 中的每个元素一次。形式上,对于子数组 nums[i], nums[i + 1], …, nums[j] ,不存在 i <= k1, k2 <= j 其中 k1 % n == k2 % n 。
最大子数组的分布情况有以下两种:
对于情况一:只需要进行一次求解最大子数组的和即可
对于情况二:产生了两种解法:
解法一:进行一次左最大和和右最大和
解法二:进行一次最小子数组求和,最后用总和减去该和
解法一:
class Solution {
public:
int maxSubarraySumCircular(vector<int>& nums) {
vector<int> dp(nums.size() + 1, 0);
int i = 0;
int ans = INT_MIN;
for (i = 0; i < nums.size(); ++i) {
dp[i + 1] = dp[i] < 0 ? nums[i] : dp[i] + nums[i];
ans = max(ans, dp[i + 1]);
}
vector<int> leftSum(nums.size(),0);
vector<int> rightSum(nums.size(),0);
leftSum[0]=nums[0];
rightSum[nums.size()-1]=nums.back();
int sum=0;
for(i=1,sum=nums[0];i<nums.size();++i){
sum+=nums[i];
leftSum[i]=max(leftSum[i-1],sum);
}
for(i=nums.size()-2,sum=nums.back();i>=0;--i){
sum+=nums[i];
rightSum[i]=max(rightSum[i+1],sum);
}
for(i=0;i+1<leftSum.size();++i){
ans=max(ans,leftSum[i]+rightSum[i+1]);
}
return ans;
}
};
解法二:
class Solution {
public:
int maxSubarraySumCircular(vector<int>& nums) {
int sum=0;
vector<int> maxdp(nums.size() + 1, 0);
vector<int> mindp(nums.size() + 1, 0);
int i=0;
int maxsSum=INT_MIN;
int minSum=INT_MAX;
for(i=0;i<nums.size();++i){
sum+=nums[i];
maxdp[i+1]=max(maxdp[i]+nums[i],nums[i]);
mindp[i+1]=min(mindp[i]+nums[i],nums[i]);
maxsSum=max(maxsSum,maxdp[i+1]);
minSum=min(minSum,mindp[i+1]);
}
if(sum==minSum){
return maxsSum;
}
return max(maxsSum,sum-minSum);
}
};
152. 乘积最大子数组
给你一个整数数组 nums ,请你找出数组中乘积最大的非空连续 子数组(该子数组中至少包含一个数字),并返回该子数组所对应的乘积。
测试用例的答案是一个 32-位 整数。
使用两个数组,一个记录以当前为止结尾的最大积,另一个记录以当前为止结尾的最小积
class Solution {
public:
int maxProduct(vector<int>& nums) {
if(1==nums.size()){
return nums[0];
}
vector<vector<int>> dp(nums.size()+1,{0,0});
int ans=INT_MIN;
int i=0;
for(i=0;i<nums.size();++i){
if(nums[i]>0){
dp[i+1][0]=max(nums[i]*dp[i][0],nums[i]);
dp[i+1][1]=nums[i]*dp[i][1];
}else{
dp[i+1][0]=dp[i][1]*nums[i];
dp[i+1][1]=min(nums[i],dp[i][0]*nums[i]);
}
ans=max(dp[i+1][0],ans);
}
return ans;
}
};
1567. 乘积为正数的最长子数组长度
给你一个整数数组 nums ,请你求出乘积为正数的最长子数组的长度。
一个数组的子数组是由原数组中零个或者更多个连续数字组成的数组。
请你返回乘积为正数的最长子数组长度。
class Solution {
public:
int getMaxLen(vector<int>& nums) {
vector<vector<int>> dp(nums.size() + 1, { 0,0 });
int ans = INT_MIN;
int i = 0;
for (i = 0; i < nums.size(); ++i) {
if (nums[i] > 0) {
dp[i + 1][0] = dp[i][0]+1;
dp[i + 1][1] = dp[i][1]?dp[i][1]+1:0;
}
else if(nums[i]<0){
dp[i + 1][0] = dp[i][1] ?dp[i][1]+1:0;
dp[i + 1][1] = dp[i][0]+1;
}else{
dp[i+1][0]=0;
dp[i+1][1]=0;
}
ans = max(dp[i + 1][0], ans);
}
return ans;
}
};
413. 等差数列划分
如果一个数列 至少有三个元素 ,并且任意两个相邻元素之差相同,则称该数列为等差数列。
例如,[1,3,5,7,9]、[7,7,7,7] 和 [3,-1,-5,-9] 都是等差数列。
给你一个整数数组 nums ,返回数组 nums 中所有为等差数组的 子数组 个数。
子数组 是数组中的一个连续序列。
由于子数组是连续序列,因此可以用dp[i]表示以下标i所在元素为结尾的等差数列的个数,如果构成等差数列则有dp[i]=dp[i-1]+1,否则dp[i]=0。
class Solution {
public:
int numberOfArithmeticSlices(vector<int>& nums) {
int ans=0;
vector<int> dp(nums.size(),0);
int i=0;
int gap=INT_MAX;
for(i=1;i<nums.size();++i){
if(nums[i]-nums[i-1]==gap){
dp[i]=dp[i-1]+1;
}else{
gap=nums[i]-nums[i-1];
}
ans+=dp[i];
}
return ans;
}
};
978. 最长湍流子数组
给定一个整数数组 arr ,返回 arr 的 最大湍流子数组的长度 。
如果比较符号在子数组中的每个相邻元素对之间翻转,则该子数组是 湍流子数组 。
更正式地来说,当 arr 的子数组 A[i], A[i+1], …, A[j] 满足仅满足下列条件时,我们称其为湍流子数组:
若 i <= k < j :
当 k 为奇数时, A[k] > A[k+1],且
当 k 为偶数时,A[k] < A[k+1];
或 若 i <= k < j :
当 k 为偶数时,A[k] > A[k+1] ,且
当 k 为奇数时, A[k] < A[k+1]。
class Solution {
public:
int maxTurbulenceSize(vector<int>& nums) {
int ans=1;
vector<int> dp(nums.size(),1);
int i=0;
long long int gap=0;
for(i=1;i<nums.size();++i){
if(gap*(nums[i]-nums[i-1])<0){
dp[i]=dp[i-1]+1;
}else{
dp[i]=nums[i]==nums[i-1]?1:2;
}
gap=nums[i]-nums[i-1];
ans=max(ans,dp[i]);
}
return ans;
}
};
139. 单词拆分
给你一个字符串 s 和一个字符串列表 wordDict 作为字典。如果可以利用字典中出现的一个或多个单词拼接出 s 则返回 true。
注意:不要求字典中出现的单词全部都使用,并且字典中的单词可以重复使用。
对于任意一个0-n的子数组,如果0到j-1可以拼接而且j-n单词存在,则0-n是可以拼接的。
class Solution {
public:
bool wordBreak(string s, vector<string>& wordDict) {
vector<int> dp(s.size(),0);
unordered_set<string> us(wordDict.begin(),wordDict.end());
int i=0;
for(;i<s.size();++i){
int j=0;
for(j=i;j<s.size();++j){
if(us.end()!=us.find(s.substr(i,j-i+1))){
if(j+1==s.size()){
return true;
}
dp[j]=1;
}
}
}
return false;
}
};
467. 环绕字符串中唯一的子字符串
定义字符串 base 为一个 “abcdefghijklmnopqrstuvwxyz” 无限环绕的字符串,所以 base 看起来是这样的:
“…zabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcd…”.
给你一个字符串 s ,请你统计并返回 s 中有多少 不同非空子串 也在 base 中出现。
dp[i]表示以下标i元素为末尾子串的个数,最后对于相同末尾元素子串,我们只需要个数最多那个
class Solution {
public:
int findSubstringInWraproundString(string s) {
vector<int> dp(s.size(),1);
vector<int> count(26,0);
int i=1;
count[s[0]-'a']=1;
for(;i<s.size();++i){
if(s[i-1]+1==s[i]||(s[i-1]=='z'&&s[i]=='a')){
dp[i]=dp[i-1]+1;
}
count[s[i]-'a']=max(count[s[i]-'a'],dp[i]);
}
int ans=0;
for(i=0;i<26;++i){
ans+=count[i];
}
return ans;
}
};
子序列问题
300. 最长递增子序列
给你一个整数数组 nums ,找到其中最长严格递增子序列的长度。
子序列 是由数组派生而来的序列,删除(或不删除)数组中的元素而不改变其余元素的顺序。例如,[3,6,2,7] 是数组 [0,3,1,6,2,2,7] 的子序列。
dp[ i ]表示以nums[ i ]结尾的最长子序列的长度
class Solution {
public:
int lengthOfLIS(vector<int>& nums) {
int m=nums.size();
int i=0;
int j=0;
int res=1;
vector<int> dp(m,1);
dp[0]=1;
for(i=1;i<m;++i){
for(j=i-1;j>=0;--j){
if(nums[j]<nums[i]){
dp[i]=max(dp[i],dp[j]+1);
}
res=max(res,dp[i]);
}
}
return res;
}
};
376. 摆动序列
如果连续数字之间的差严格地在正数和负数之间交替,则数字序列称为 摆动序列 。第一个差(如果存在的话)可能是正数或负数。仅有一个元素或者含两个不等元素的序列也视作摆动序列。
例如, [1, 7, 4, 9, 2, 5] 是一个 摆动序列 ,因为差值 (6, -3, 5, -7, 3) 是正负交替出现的。
相反,[1, 4, 7, 2, 5] 和 [1, 7, 4, 5, 5] 不是摆动序列,第一个序列是因为它的前两个差值都是正数,第二个序列是因为它的最后一个差值为零。
子序列 可以通过从原始序列中删除一些(也可以不删除)元素来获得,剩下的元素保持其原始顺序。
给你一个整数数组 nums ,返回 nums 中作为 摆动序列 的 最长子序列的长度 。
dp1[ i ]表示以nums[ i ]结尾且是呈现上升趋势的最长摆动序列的长度
dp2[ i ]表示以nums[ i ]结尾且是呈现下降趋势的最长摆动序列的长度
class Solution {
public:
int wiggleMaxLength(vector<int>& nums) {
int i=0;
int j=0;
int m=nums.size();
int res=1;
vector<int> dp1(m,1);
vector<int> dp2(m,1);
for(i=1;i<m;++i){
for(j=i-1;j>=0;--j){
if(nums[i]>nums[j]) dp1[i]=max(dp2[j]+1,dp1[i]);
if(nums[i]<nums[j]) dp2[i]=max(dp1[j]+1,dp2[i]);
}
res=max(res,max(dp1[i],dp2[i]));
}
return res;
}
};
673. 最长递增子序列的个数
给定一个未排序的整数数组 nums , 返回最长递增子序列的个数 。
注意 这个数列必须是 严格 递增的。
使用两个数组len和count,len[i]表示以nums[i]元素作为尾元素的最长子序列的长度,count[i]表示以nums[i]元素作为尾元素的最长子序列的个数。对任意元素nums[i],遍历len[0]-len[i-1],如果nums[i]>nums[j]:
①len[j]+1==len[i]:
count[i]+=count[j]
②len[j]+1>len[i]:
count[i]=count[j]
len[i]=len[j]+1
class Solution {
public:
int findNumberOfLIS(vector<int>& nums) {
vector<int> len(nums.size(),1);
vector<int> count(nums.size(),1);
int i=0;
int j=0;
for(i=1;i<nums.size();++i){
for(j=0;j<i;++j){
if(nums[i]>nums[j]){
if(len[j]+1==len[i]){
count[i]+=count[j];
}else if(len[j]+1>len[i]){
count[i]=count[j];
len[i]=len[j]+1;
}
}
}
}
int ans=count[0];
int maxLen=len[0];
for(i=1;i<len.size();++i){
if(maxLen==len[i]){
ans+=count[i];
}else if(maxLen<len[i]){
ans=count[i];
maxLen=len[i];
}
}
return ans;
}
};
646. 最长数对链
给你一个由 n 个数对组成的数对数组 pairs ,其中 pairs[i] = [lefti, righti] 且 lefti < righti 。
现在,我们定义一种 跟随 关系,当且仅当 b < c 时,数对 p2 = [c, d] 才可以跟在 p1 = [a, b] 后面。我们用这种形式来构造 数对链 。
找出并返回能够形成的 最长数对链的长度 。
你不需要用到所有的数对,你可以以任何顺序选择其中的一些数对来构造。
class Solution {
public:
int findLongestChain(vector<vector<int>>& pairs) {
sort(pairs.begin(),pairs.end(),[&](vector<int>&v1,vector<int>& v2)->bool{
if(v1[0]==v2[0]) return v1[1]<v2[1];
return v1[0]<v2[0];
});
int i=0;
int j=0;
int m=pairs.size();
int res=1;
vector<int> dp(m,1);
for(i=1;i<m;++i){
for(j=i-1;j>=0;--j){
if(pairs[i][0]>pairs[j][1]){
dp[i]=max(dp[i],dp[j]+1);
}
res=max(res,dp[i]);
}
}
return res;
}
};
1218. 最长定差子序列
给你一个整数数组 arr 和一个整数 difference,请你找出并返回 arr 中最长等差子序列的长度,该子序列中相邻元素之间的差等于 difference 。
子序列 是指在不改变其余元素顺序的情况下,通过删除一些元素或不删除任何元素而从 arr 派生出来的序列。
class Solution {
public:
int longestSubsequence(vector<int>& arr, int difference) {
int i=0;
int m=arr.size();
int res=1;
map<int,int> mp;
mp[arr[0]]=1;
for(i=1;i<m;++i){
int tmp=arr[i]-difference;
int len=1;
if(mp.end()!=mp.find(tmp)){
len=mp[tmp]+1;
}
mp[arr[i]]=len;
res=max(res,len);
}
return res;
}
};
873. 最长的斐波那契子序列的长度
如果序列 X_1, X_2, …, X_n 满足下列条件,就说它是 斐波那契式 的:
n >= 3
对于所有 i + 2 <= n,都有 X_i + X_{i+1} = X_{i+2}
给定一个严格递增的正整数数组形成序列 arr ,找到 arr 中最长的斐波那契式的子序列的长度。如果一个不存在,返回 0 。
(回想一下,子序列是从原序列 arr 中派生出来的,它从 arr 中删掉任意数量的元素(也可以不删),而不改变其余元素的顺序。例如, [3, 5, 8] 是 [3, 4, 5, 6, 7, 8] 的一个子序列)
对于任意一个斐波那契式的子序列,只要我们知道了末尾两个数,就可以推导出整个斐波那契式的子序列,因此使用dp[ i ][ j ]表示以nums[ i ]为倒数第二个数、nums[ j ]为末尾的数的斐波那契式的子序列的最长长度。如果倒数第三个数存在且对应下标为k,一定有
k<i<j,dp[ i ][ j ]=dp[ k ][ i ]+1
class Solution {
public:
int lenLongestFibSubseq(vector<int>& nums) {
map<int,int> mp;
vector<vector<int>> dp(nums.size(),vector<int>(nums.size(),0));
int i=0;
int j=0;
int ans=0;
for(i=0;i<nums.size();++i){
mp[nums[i]]=i;
}
for(i=0;i<nums.size();++i){
for(j=i+1;j<nums.size();++j){
int tmp=nums[j]-nums[i];
if(tmp<nums[i]&&mp.end()!=mp.find(tmp)&&mp[tmp]<i){
dp[i][j]=dp[mp[tmp]][i]+1;
}else{
dp[i][j]=2;
}
ans=max(ans,dp[i][j]);
}
}
return ans>2?ans:0;
}
};
1027. 最长等差数列
给你一个整数数组 nums,返回 nums 中最长等差子序列的长度。
回想一下,nums 的子序列是一个列表 nums[i1], nums[i2], …, nums[ik] ,且 0 <= i1 < i2 < … < ik <= nums.length - 1。并且如果 seq[i+1] - seq[i]( 0 <= i < seq.length - 1) 的值都相同,那么序列 seq 是等差的。
class Solution {
public:
int longestArithSeqLength(vector<int>& nums) {
map<int,set<int>> mp;
vector<vector<int>> dp(nums.size(),vector<int>(nums.size(),2));
int i=0;
int j=0;
int ans=2;
for(i=0;i<nums.size();++i){
mp[nums[i]].insert(i);
for(j=i+1;j<nums.size();++j){
int tmp=2*nums[i]-nums[j];
auto it=mp[tmp].rbegin();
while(mp[tmp].rend()!=it){
if(*it<i){
dp[i][j]=max(dp[i][j],dp[*it][i]+1);
ans=max(ans,dp[i][j]);
break;
}
++it;
}
}
}
return ans;
}
};
446. 等差数列划分 II - 子序列
给你一个整数数组 nums ,返回 nums 中所有 等差子序列 的数目。
如果一个序列中 至少有三个元素 ,并且任意两个相邻元素之差相同,则称该序列为等差序列。
例如,[1, 3, 5, 7, 9]、[7, 7, 7, 7] 和 [3, -1, -5, -9] 都是等差序列。
再例如,[1, 1, 2, 5, 7] 不是等差序列。
数组中的子序列是从数组中删除一些元素(也可能不删除)得到的一个序列。
例如,[2,5,10] 是 [1,2,1,2,4,1,5,10] 的一个子序列。
题目数据保证答案是一个 32-bit 整数。
dp[ i ][ j ]表示以nums[ i ]为倒数第二个数、nums[ j ]为末尾的数的等差数列的个数。如果倒数第三个数存在(可能有很多个)且它们的下标为Kx(x=0,1,2,3…),一定有Kx<i,新加的等差数列如下:
故只需令dp[ i ][ j ]+=dp[ Kx ][ i ]+1即可
class Solution {
public:
int numberOfArithmeticSlices(vector<int>& nums) {
map<long long int, set<int>> mp;
vector<vector<int>> dp(nums.size(), vector<int>(nums.size(), 0));
int i = 0;
int j = 0;
int ans = 0;
for (i = 0; i < nums.size(); ++i) {
mp[nums[i]].insert(i);
for (j = 0; j < i; ++j) {
long long int tmp = (long long int)2 * nums[j] - nums[i];
auto it = mp[tmp].rbegin();
while (mp[tmp].rend() != it) {
if (*it < j) {
dp[i][j]+= dp[j][*it] + 1;
}
++it;
}
ans += dp[i][j];
}
}
return ans;
}
};
回文串问题
647. 回文子串
给你一个字符串 s ,请你统计并返回这个字符串中 回文子串 的数目。
回文字符串 是正着读和倒过来读一样的字符串。
子字符串 是字符串中的由连续字符组成的一个序列。
解法一:
由于回文串的长度可能是奇数也可能是偶数,因此我们可以从某一个字符或两个相邻字符开始从两边拓展,这样就可以得到一个最长回文子串,进而得到以该字符中心的所有回文子串的数目
class Solution {
public:
int Get(string& s,int left,int right){
for(;left>=0&&right<s.size()&&s[left]==s[right];--left,++right);
return (right-left)/2;
}
int countSubstrings(string s) {
int ans=0;
int i=0;
for(i=0;i<s.size();++i){
ans+=Get(s,i,i);
ans+=Get(s,i,i+1);
}
return ans;
}
};
解法二:动态规划
dp[ i ][ j ]表示s[ i ] - s[ j ]是否是回文子串,我们需要借助
dp[ i+1 ][ j-1 ]以判断当前子串dp[ i ][ j ]是否是回文子串,只需要进行一定的分类讨论即可。
class Solution {
public:
int countSubstrings(string s) {
int i = 0;
int j = 0;
int res = 0;
int m = s.size();
vector<vector<bool>> dp(m, vector<bool>(m, false));
for (i = m - 1; i >= 0; --i) {
for (j = i; j < m; ++j) {
if (i == j || (i + 1 == j && s[i] == s[j]) ||
(j - i > 1 && s[i] == s[j] && dp[i + 1][j - 1])) {
dp[i][j] = true;
}
res += dp[i][j] ? 1 : 0;
}
}
return res;
}
};
除了上面的解法外还有马拉车算法,该算法的时间和空间复杂度均为o(n) ,但其局限性很大,这里不做介绍。
5. 最长回文子串
解法一:选取某一个字符或两个相邻字符开始从两边拓展,就可以得到一个最长回文子串,如此遍历整个字符串s,就可以得到最长回文子串
class Solution {
public:
string longestPalindrome(string s) {
string ans;
int i=0;
for(i=0;i<s.size();++i){
int left=0;
int right=0;
//回文串长度为奇数
for(left=i-1,right=i+1;left>=0&&right<s.size()&&s[left]==s[right];--left,++right);
if(ans.size()<right-left-1){
ans=s.substr(left+1,right-left-1);
}
//回文串长度为偶数
for(left=i-1,right=i;left>=0&&right<s.size()&&s[left]==s[right];--left,++right);
if(ans.size()<right-left-1){
ans=s.substr(left+1,right-left-1);
}
}
return ans;
}
};
解法二:动态规划
dp[ i ][ j ]表示s[ i ] - s[ j ]是否是回文子串,我们需要借助
dp[ i+1 ][ j-1 ]以判断当前子串dp[ i ][ j ]是否是回文子串,只需进行一定判断就可以得到最长回文子串。
class Solution {
public:
string longestPalindrome(string s) {
int i = 0;
int j = 0;
string res;
int m = s.size();
vector<vector<bool>> dp(m, vector<bool>(m, false));
for (i = m - 1; i >= 0; --i) {
for (j = i; j < m; ++j) {
if (s[i] == s[j] &&
(i == j || i + 1 == j || dp[i + 1][j - 1])) {
dp[i][j] = true;
if (res.size() < j - i + 1) {
res = s.substr(i, j - i + 1);
}
}
}
}
return res;
}
};
1745. 分割回文串 IV
给你一个字符串 s ,如果可以将它分割成三个 非空 回文子字符串,那么返回 true ,否则返回 false 。
当一个字符串正着读和反着读是一模一样的,就称其为 回文字符串 。
解法一:暴力求解
选取某一个字符或两个相邻字符开始从两边拓展,如果当前子串是回文子串,只需要再判断左边和右边未拓展到的子串是否是回文子串即可
class Solution {
public:
bool IsTrue(string& s,int left,int right){
if(left<0||right>=s.size()){
return false;
}
for(;left<right&&s[left]==s[right];++left,--right);
if(left<right){
return false;
}
return true;
}
bool checkPartitioning(string s) {
int i=1;
for(i=1;i<s.size();++i){
int left=i;
int right=i;
for(;left>0&&right+1<s.size()&&s[left]==s[right];--left,++right){
if(IsTrue(s,0,left-1)&&IsTrue(s,right+1,s.size()-1)){
return true;
}
}
left=i;
right=i+1;
for(;left>0&&right+1<s.size()&&s[left]==s[right];--left,++right){
if(IsTrue(s,0,left-1)&&IsTrue(s,right+1,s.size()-1)){
return true;
}
}
}
return false;
}
};
解法二:动态规划
参考前面回文子串问题,dp[ i ][ j ]表示s[ i ] - s[ j ]是否是回文子串,得到dp表后,只需要遍历dp表查看是否满足
dp[ 0 ][ i-1 ]&&dp[ i ][ j ]&&dp[ j+1 ][ m-1 ]均为真即可。
class Solution {
public:
bool checkPartitioning(string s) {
int i = 0;
int j = 0;
int m = s.size();
vector<vector<bool>> dp(m, vector<bool>(m, false));
for (i = m - 1; i >= 0; --i) {
for (j = i; j < m; ++j) {
if (s[i] == s[j] &&
(i == j || i + 1 == j || dp[i + 1][j - 1])) {
dp[i][j] = true;
}
}
}
for(i=1;i+1<m;++i){
for(j=i;j+1<m;++j){
if(dp[0][i-1]&&dp[i][j]&&dp[j+1][m-1]){
return true;
}
}
}
return false;
}
};
132. 分割回文串 II
给你一个字符串 s,请你将 s 分割成一些子串,使每个子串都是回文串。
返回符合要求的 最少分割次数 。
解法一:
选取某一个字符或两个相邻字符开始从两边拓展,将所有的回文子串的始末坐标存到一个数组对v中,按照回文串的起始坐标对v排好升序,dp[ i ]表示将s[ 0 ] - s[ i ]分割为回文子串的最小次数,则有:
dp[ v[ i ].second ]=dp[ v[ i ].first -1 ] + 1
class Solution {
public:
int minCut(string s) {
vector<pair<int,int>> v;
int i = 0;
for (i = 0; i < s.size(); ++i) {
int left = i;
int right = i;
for (; left >= 0 && right < s.size() && s[left] == s[right]; --left, ++right) {
v.push_back({left,right});
}
left = i;
right = i + 1;
for (; left >= 0 && right < s.size() && s[left] == s[right]; --left, ++right) {
v.push_back({left,right});
}
}
sort(v.begin(),v.end(),[](pair<int,int>&p1,pair<int,int>&p2)->bool{
if(p1.first!=p2.first){
return p1.first<p2.first;
}
return p1.second<p2.second;
});
vector<int> dp(s.size()+1,INT_MAX/2);
dp[0]=0;
for(auto& e:v){
dp[e.second+1]=min(dp[e.second+1],dp[e.first]+1);
}
return dp.back()-1;
}
};
解法二:
参考前面回文子串问题,dp[ i ][ j ]表示s[ i ] - s[ j ]是否是回文子串,得到dp表后,使用dp1[ i ]表示s[ 0 ] - s[ i ]进行最少分割次数后子回文串的个数。
class Solution {
public:
int minCut(string s) {
int i = 0;
int j = 0;
int m = s.size();
vector<vector<bool>> dp(m, vector<bool>(m, false));
for (i = m - 1; i >= 0; --i) {
for (j = i; j < m; ++j) {
if (s[i] == s[j] &&
(i == j || i + 1 == j || dp[i + 1][j - 1])) {
dp[i][j] = true;
}
}
}
vector<int> dp1(m+1,INT_MAX/2);
dp1[0]=0;
for(i=0;i<m;++i){
for(j=0;j<=i;++j){
if(dp[j][i]){
dp1[i+1]=min(dp1[i+1],dp1[j]+1);
}
}
}
return dp1[m]-1;
}
};
516. 最长回文子序列
给你一个字符串 s ,找出其中最长的回文子序列,并返回该序列的长度。
子序列定义为:不改变剩余字符顺序的情况下,删除某些字符或者不删除任何字符形成的一个序列。
dp[ i ][ j ]表示s[ i ]到s[ j ]的最长回文子序列的长度,分类讨论即可。
class Solution {
public:
int longestPalindromeSubseq(string s) {
vector<vector<int>> dp(s.size(),vector<int>(s.size(),0));
int ans=1;
int i=s.size()-1;
for(;i>=0;--i){
int j=i;
for(;j<s.size();++j){
if(s[i]==s[j]){
if(i==j){
dp[i][j]=1;
}else if(i+1==j){
dp[i][j]=2;
}else{
dp[i][j]=dp[i+1][j-1]+2;
}
}else{
dp[i][j]=max(dp[i+1][j],dp[i][j-1]);
}
ans=max(ans,dp[i][j]);
}
}
return ans;
}
};
1312. 让字符串成为回文串的最少插入次数
给你一个字符串 s ,每一次操作你都可以在字符串的任意位置插入任意字符。
请你返回让 s 成为回文串的 最少操作次数 。
「回文串」是正读和反读都相同的字符串。
dp[ i ][ j ]表示把s[ i ] - s[ j ]变为回文串的最少操作次数
如果s[ i ]==s[ j ]:dp[ i ][ j ]=dp[ i+1 ][ j-1 ]
如果s[ i ]!=s[ j ]:dp[ i ][ j ]=min(dp[ i ][ j-1 ] , dp[ i+1 ][ j ])
只需要分类讨论以及处理一些细节问题即可
class Solution {
public:
int minInsertions(string s) {
vector<vector<int>> dp(s.size(), vector<int>(s.size(), 0));
int i=s.size()-1;
for (; i >= 0; --i) {
int j = i;
for (; j < s.size(); ++j) {
if (s[i] == s[j]) {
if (i == j||i + 1 == j) {
dp[i][j] = 0;
}
else {
dp[i][j] = dp[i + 1][j - 1];
}
}
else {
dp[i][j] = min(dp[i + 1][j], dp[i][j - 1])+1;
}
}
}
return dp[0].back();
}
};
两个数组的dp问题
1143. 最长公共子序列
给定两个字符串 text1 和 text2,返回这两个字符串的最长 公共子序列 的长度。如果不存在 公共子序列 ,返回 0 。
一个字符串的 子序列 是指这样一个新的字符串:它是由原字符串在不改变字符的相对顺序的情况下删除某些字符(也可以不删除任何字符)后组成的新字符串。
例如,“ace” 是 “abcde” 的子序列,但 “aec” 不是 “abcde” 的子序列。
两个字符串的 公共子序列 是这两个字符串所共同拥有的子序列。
dp[ i ][ j ]表示text1[0 - i]与text[0 - j]的最长的公共子序列的长度
class Solution {
public:
int longestCommonSubsequence(string text1, string text2) {
vector<vector<int>> dp(text1.size()+1,vector<int>(text2.size()+1,0));
int i=0;
int j=0;
int res=0;
for(i;i<text1.size();++i){
for(j=0;j<text2.size();++j){
if(text1[i]==text2[j]){
dp[i+1][j+1]=dp[i][j]+1;
}else{
dp[i+1][j+1]=max(dp[i+1][j],dp[i][j+1]);
}
res=max(res,dp[i+1][j+1]);
}
}
return res;
}
};
1035. 不相交的线
在两条独立的水平线上按给定的顺序写下 nums1 和 nums2 中的整数。
现在,可以绘制一些连接两个数字 nums1[i] 和 nums2[j] 的直线,这些直线需要同时满足:
nums1[i] == nums2[j]
且绘制的直线不与任何其他连线(非水平线)相交。
请注意,连线即使在端点也不能相交:每个数字只能属于一条连线。
以这种方法绘制线条,并返回可以绘制的最大连线数。
本质就是求两个数组的最长公共子序列
class Solution {
public:
int maxUncrossedLines(vector<int>& nums1, vector<int>& nums2) {
vector<vector<int>> dp(nums1.size()+1,vector<int>(nums2.size()+1,0));
int i=0;
int j=0;
int res=0;
for(i=0;i<nums1.size();++i){
for(j=0;j<nums2.size();++j){
if(nums1[i]==nums2[j]){
dp[i+1][j+1]=dp[i][j]+1;
}else{
dp[i+1][j+1]=max(dp[i+1][j],dp[i][j+1]);
}
res=max(res,dp[i+1][j+1]);
}
}
return res;
}
};
115. 不同的子序列
给你两个字符串 s 和 t ,统计并返回在 s 的 子序列 中 t 出现的个数,结果需要对 109 + 7 取模。
dp[ i ][ j ]表示t[ 0 - i ]在s[ 0 - j ]中出现的个数,
①如果t[ i ] == s[ j ]:如果用t[ i ] 配对 s[ j ]。此时的子序列个数为
dp[ i - 1 ][ j - 1 ];如果用t[ i ] 不配对 s[ j ]。此时的子序列个数为
dp[ i ][ j - 1 ],故dp[ i ][ j ] = dp[ i - 1 ][ j - 1 ] + dp[ i ][ j - 1 ]
②如果t[ i ] != s[ j ]:t[ 0 - i ] 只能在 s[ 0 - j ]中寻找配对,
故dp[ i ][ j ] = dp[ i ][ j - 1 ]
class Solution {
public:
int numDistinct(string s, string t) {
vector<vector<int>> dp(t.size(),vector<int>(s.size()+1,0));
int mod=pow(10,9)+7;
int i=0;
int j=0;
for(i=0;i<s.size();++i){
dp[0][i+1]=t[0]==s[i]?dp[0][i]+1:dp[0][i];
}
for(i=1;i<t.size();++i){
for(j=i;j<s.size();++j){
if(t[i]==s[j]){
dp[i][j+1]=dp[i-1][j]+dp[i][j];
}else{
dp[i][j+1]=dp[i][j];
}
dp[i][j+1]%=mod;
}
}
return dp.back().back();
}
};
44. 通配符匹配
给定⼀个字符串 (s) 和⼀个字符模式 § ,实现⼀个⽀持 ‘?’ 和 ‘’ 的通配符匹配。
‘?’ 可以匹配任何单个字符。
'’ 可以匹配任意字符串(包括空字符串)。
两个字符串完全匹配才算匹配成功。
说明:
s 可能为空,且只包含从 a-z 的⼩写字⺟。
p 可能为空,且只包含从 a-z 的⼩写字⺟,以及字符 ? 和 *。
dp[i][j]表示字符串p[0-i]与s[0-j]是否匹配,现在填表dp[i][j],考虑以下三种情况:
-
①p[i]是普通字符:如果dp[i-1][j-1]==true&&p[i]==s[j],
则dp[i][j]=true,否则d[i][j]=false -
②p[i]==‘?’:如果dp[i-1][j-1]==true,则dp[i][j]=true,否则d[i][j]=false
-
③p[i]==‘*’:只要dp[i-1][j]、dp[i-1][j-1]…dp[i-1][0]任意一个为true,则dp[i][j]=true,否则d[i][j]=false
考虑到这样遍历会使整个算法的时间复杂度上升到o(n3),因此需要对p[i] ==‘*’的情况进行一定的优化,由于当p[i] ==‘*’时:
dp[ i ][ j ]=dp[ i-1 ][ j ] || dp[ i-1 ][ j-1 ] || …|| dp[ i-1 ][ 0 ],而
dp[ i ][ j-1 ]=dp[ i-1 ][ j-1 ] || dp[ i-1 ][ j-2 ] || …|| dp[ i-1 ][ 0 ],
故有dp[ i ][ j ]=dp[ i -1 ][ j ] || dp[ i ][ j-1 ]
class Solution {
public:
bool isMatch(string s, string p) {
vector<vector<bool>> dp(p.size() + 1, vector<bool>(s.size() + 1, false));
int i = 0;
int j = 0;
dp[0][0] = true;
for (i = 0; i < p.size() && '*' == p[i]; dp[i + 1][0] = true, ++i);
for (i = 0; i < p.size(); ++i) {
for (j = 0; j < s.size(); ++j) {
if ('*' == p[i]) {
dp[i + 1][j + 1] = dp[i][j + 1] || dp[i + 1][j];
}
else if (dp[i][j] && (p[i] == s[j] || '?' == p[i])) {
dp[i + 1][j + 1] = true;
}
}
}
return dp.back().back();
}
};
10. 正则表达式匹配
给你一个字符串 s 和一个字符规律 p,请你来实现一个支持 ‘.’ 和 ‘’ 的正则表达式匹配。
‘.’ 匹配任意单个字符
'’ 匹配零个或多个前面的那一个元素
所谓匹配,是要涵盖 整个 字符串 s 的,而不是部分字符串。
dp[i][j]表示字符串p[0-i]与s[0-j]是否匹配,现在填表dp[i][j],考虑以下三种情况:
-
①p[i]是普通字符:如果dp[i-1][j-1]==true&&p[i]==s[j],
则dp[i][j]=true,否则d[i][j]=false -
②p[ i ]==‘.’:如果dp[ i-1 ][ j-1 ]==true,则dp[ i ][ j ]=true,否则d[ i ][ j ]=false
-
③p[ i ]==‘*’:此时还可以细分为两种情况:
a
:p[ i-1 ]==‘.’:
dp[ i ][ j ]=dp[ i-2 ][ j ] || dp[ i-2 ][ j-1 ] || …|| dp[ i-2 ][ 0 ],考虑到这样遍历会使整个算法的时间复杂度上升到o(n3),因此需要对进行一定的优化,由于
dp[ i ][ j-1 ]=dp[ i-2 ][ j-1 ] || dp[ i-2 ][ j-2 ] || …|| dp[ i-2 ][ 0 ],
故有dp[ i ][ j ]=dp[ i -2 ][ j ] || dp[ i ][ j-1 ]
b
:p[ i-1 ]==普通字符:如果把" _* “当作空串匹配,只要
dp[ i-2 ][ j ]=true,那么dp[ i ][ j ]=true,如果把” _* “当作多个”_…_"匹配,只要p[ i-1 ]==s[ j ]&&dp[ i ][ j-1 ]=true时,
dp[ i ][ j ]=true
综上考虑,可以分为以下两种情况:
①p[ i ]==‘.’ || p[ i ]==普通字符:如果
dp[i-1][j-1]==true&&( p[i]==s[j] || p[ i ]==‘.’ ),则
dp[ i ][ j ]=true,否则d[ i ][ j ]=false
②p[ i ]==‘*’:如果
dp[ i-2 ][ j ]==true||( p[ i-1 ]==s[ j ] || p[ i-1 ]==‘.’ )&&dp[ i ][ j-1 ],则
dp[ i ][ j ]=true,否则d[ i ][ j ]=false
class Solution {
public:
bool isMatch(string s, string p) {
int m = p.size();
int n = s.size();
int i = 0;
int j = 0;
vector<vector<bool>> dp(m + 1, vector<bool>(n + 1, false));
dp[0][0] = true;
for(i=1;i<m&&'*'==p[i];dp[i+1][0]=true,i+=2);
for (i = 0; i < m; ++i) {
for (j = 0; j < n; ++j) {
if ('*' == p[i]) {
dp[i + 1][j + 1] =dp[i - 1][j+1]||(p[i-1] == s[j] || p[i - 1]== '.') && dp[i+1][j];
continue;
}
dp[i+1][j+1]=dp[i][j]&&( p[i]==s[j] || p[ i ]=='.' );
}
}
return dp.back().back();
}
};
97. 交错字符串
给定三个字符串 s1、s2、s3,请你帮忙验证 s3 是否是由 s1 和 s2 交错 组成的。
两个字符串 s 和 t 交错 的定义与过程如下,其中每个字符串都会被分割成若干 非空 子字符串:
s = s1 + s2 + … + sn
t = t1 + t2 + … + tm
|n - m| <= 1
交错 是 s1 + t1 + s2 + t2 + s3 + t3 + … 或者 t1 + s1 + t2 + s2 + t3 + s3 + …
注意:a + b 意味着字符串 a 和 b 连接。
dp[ i ][ j ]表示s1[ 0 - i ] + s2[ 0 - j ]是否可以拼接成s3[ 0 - i + j + 1 ]
如果s1[ i ] ==s3[ i + j + 1 ] && dp [ i - 1 ][ j ] == true,
则dp[ i ][ j ] = true
如果s2[ j ] ==s3[ i + j + 1 ] && dp [ i ][ j - 1 ] == true,
则dp[ i ][ j ] = true
其它情况全都拼接失败,例如如果s1[ i ] !=s3[ i + j + 1 ]&&s2[ j ] !=s3[ i + j + 1 ]:则一定无法拼接,dp[ i ][ j ] = false
class Solution {
public:
bool isInterleave(string s1, string s2, string s3) {
if(s1.size()+s2.size()!=s3.size()){
return false;
}
int i=0;
int j=0;
int m=s1.size();
int n=s2.size();
vector<vector<bool>> dp(m+1,vector<bool>(n+1,false));
dp[0][0]=true;
for(i=1;i<=m&&s1[i-1]==s3[i-1];++i){
dp[i][0]=true;
}
for(i=1;i<=n&&s2[i-1]==s3[i-1];++i){
dp[0][i]=true;
}
for(i=1;i<=m;++i){
for(j=1;j<=n;++j){
if(s1[i-1]==s3[i+j-1]&&dp[i-1][j]){
dp[i][j]=true;
}
if(s2[j-1]==s3[i+j-1]&&dp[i][j-1]){
dp[i][j]=true;
}
}
}
return dp.back().back();
}
};
712. 两个字符串的最小ASCII删除和
给定两个字符串s1 和 s2,返回 使两个字符串相等所需删除字符的 ASCII 值的最小和 。
dp[ i ][ j ]表示s1[ 0 - i ]与s2[ 0 - j ]相等所需删除字符的 ASCII 值的最小和 。
class Solution {
public:
int minimumDeleteSum(string s1, string s2) {
vector<vector<int>> dp(s1.size()+1,vector<int>(s2.size()+1,0));
int i=0;
int j=0;
for(i=0;i<s2.size();++i){
dp[0][i+1]=dp[0][i]+s2[i];
}
for(i=0;i<s1.size();++i){
dp[i+1][0]=dp[i][0]+s1[i];
}
for(i=0;i<s1.size();++i){
for(j=0;j<s2.size();++j){
int tmp1=dp[i+1][j]+s2[j];
int tmp2=dp[i][j+1]+s1[i];
int tmp3=dp[i][j];
tmp3+=s1[i]==s2[j]?0:s1[i]+s2[j];
dp[i+1][j+1]=min(tmp1,min(tmp2,tmp3));
}
}
return dp.back().back();
}
};
718. 最长重复子数组
给两个整数数组 nums1 和 nums2 ,返回 两个数组中 公共的 、长度最长的子数组的长度 。
dp[ i ][ j ]表示以nums1[ i ] 和 nums2 [ j ]结尾的最长重复子数组
class Solution {
public:
int findLength(vector<int>& nums1, vector<int>& nums2) {
vector<vector<int>> dp(nums1.size()+1,vector<int>(nums2.size()+1,0));
int i=0;
int j=0;
int res=0;
for(i=0;i<nums1.size();++i){
/ for(j=0;j<nums2.size();++j){
dp[i+1][j+1]=nums1[i]==nums2[j]?dp[i][j]+1:0;
res=max(res,dp[i+1][j+1]);
}
}
return res;
}
};
背包问题有许多种类,01背包就是每种物体的个数只有一个,要么拿要么不拿,完全背包就是每种物体的个数是无穷多个,可以多次挑选。而背包又可以选择必须装满和不必装满。
一般用于解决选或者不选的问题
01背包
DP41 【模板】01背包
描述
你有一个背包,最多能容纳的体积是V。
现在有n个物品,第i个物品的体积为v ,价值为w
(1)求这个背包至多能装多大价值的物品?
(2)若背包恰好装满,求至多能装多大价值的物品?
输入描述:
第一行两个整数n和V,表示物品个数和背包体积。
接下来n行,每行两个数,表示第i个物品的体积和价值。
输出描述:
输出有两行,第一行输出第一问的答案,第二行输出第二问的答案,如果无解请输出0。
问题1:
dp1[ i ][ j ]背包体积为 j 碰到第 i 件物品时的最大价值,我们可以选择不要当前物品 i ,此时dp1[ i ][ j ] = dp1[ i - 1 ][ j ];也可以选择将当前物品 i 装入背包,此时dp1[ i ][ j ] = dp1[ i ][ j - v[ i ] ] + w[ i ],此时需要注意容积 j - v[ i ] 应大于等于0
问题2:
dp2[ i ][ j ]背包体积为 j 碰到第 i 件物品时的恰好可以装满的最大价值,我们使用dp2[ i ][ j ] = -1 表示没有一种组合使得碰到物体 i 时恰好装满容积为 j 的背包。这里不用0表示是为了填表方便,即物体的价值为0时对应的容积是可以填满的。
我们可以选择不要当前物品 i ,此时dp2[ i ][ j ] = dp2[ i - 1 ][ j ],但前提是dp2[ i - 1][ j ] != -1;也可以选择将当前物品 i 装入背包,此时dp2[ i ][ j ] = dp2[ i ][ j - v[ i ] ] + w[ i ],但前提是dp2[ i ][ j - v[ i ] ] != -1,且此时需要注意容积 j - v[ i ] 应大于等于0
#include <iostream>
#include<vector>
#include<algorithm>
using namespace std;
int main() {
int n=0;
int p=0;
cin>>n>>p;
vector<vector<int>> dp1(n+1,vector<int>(p+1,0));
vector<vector<int>> dp2(n+1,vector<int>(p+1,0));
vector<int> w;
vector<int> v;
while(n--){
int vi=0;
int wi=0;
cin>>vi>>wi;
v.push_back(vi);
w.push_back(wi);
}
int i=0;
int j=0;
int ans1=0;
int ans2=0;
//1
for(i=1;i<dp1.size();++i){
for(j=1;j<dp1[0].size();++j){
int tmp1=dp1[i-1][j];
int tmp2=j-v[i-1]>=0?dp1[i-1][j-v[i-1]]+w[i-1]:0;
dp1[i][j]=max(tmp1,tmp2);
}
}
ans1=dp1.back().back();
//2
for(i=0;i<p;++i){
dp2[0][i+1]=-1;
}
for(i=1;i<dp2.size();++i){
for(j=1;j<dp2[0].size();++j){
int k=j-v[i-1];
dp2[i][j]=k>=0&&-1!=dp2[i-1][k]?max(dp2[i-1][k]+w[i-1],dp2[i-1][j]):dp2[i-1][j];
}
}
ans2=dp2.back().back();
ans2=-1==ans2?0:ans2;
printf("%d\n%d\n",ans1,ans2);
return 0;
}
// 64 位输出请用 printf("%lld")
前面解法的空间复杂度为o(n2),下面进行一定的优化将空间复杂度降到o(n)。
我们发现无论是问题1还是问题2,当前一行的数据只和上一行的数据相关,更准确地说是只和上一行左边的数据相关,因此我们可以考虑只使用一个一维数组dp[ i ],i为背包容积,更新数据时从后往前更新数据即可。
#include <iostream>
#include<vector>
#include<algorithm>
using namespace std;
int main() {
int n=0;
int p=0;
cin>>n>>p;
int m=n;
vector<int> dp(p+1,0);
vector<int> w;
vector<int> v;
while(m--){
int vi=0;
int wi=0;
cin>>vi>>wi;
v.push_back(vi);
w.push_back(wi);
}
int i=0;
int j=0;
int ans1=0;
int ans2=0;
//1
for(i=0;i<n;++i){
for(j=p;j>0;--j){
int tmp=j-v[i]>=0?dp[j-v[i]]+w[i]:0;
dp[j]=max(dp[j],tmp);
}
}
ans1=dp.back();
//2
dp[0]=0;
for(i=0;i<p;++i){
dp[i+1]=-1;
}
for(i=0;i<n;++i){
for(j=p;j>0;--j){
int k=j-v[i];
dp[j]=k>=0&&-1!=dp[k]?max(dp[k]+w[i],dp[j]):dp[j];
}
}
ans2=dp.back();
ans2=-1==ans2?0:ans2;
printf("%d\n%d\n",ans1,ans2);
return 0;
}
// 64 位输出请用 printf("%lld")
416. 分割等和子集
给你一个 只包含正整数 的 非空 数组 nums 。请你判断是否可以将这个数组分割成两个子集,使得两个子集的元素和相等。
该问题就是寻找到一个子集使其和为c = sum( nums )/2,本质就是一个01背包问题,背包容积为c,体积为nums,不考虑价值只需将背包装满即可。
class Solution {
public:
bool canPartition(vector<int>& nums) {
int i=0;
int j=0;
int sum=0;
for(auto e:nums){
sum+=e;
}
if(1==sum%2){
return false;
}
sum/=2;
vector<int> dp(sum+1,-1);
dp[0]=0;
int m=nums.size();
for(i=0;i<m;++i){
for(j=sum;j>0;--j){
int k = j - nums[i];
if(k >= 0 && -1 != dp[k]){
dp[j]=1;
}
if(j==sum&&-1!=dp[j]){
return 1;
}
}
}
return false;
}
};
494. 目标和
给你一个非负整数数组 nums 和一个整数 target 。
向数组中的每个整数前添加 ‘+’ 或 ‘-’ ,然后串联起所有整数,可以构造一个 表达式 :
例如,nums = [2, 1] ,可以在 2 之前添加 ‘+’ ,在 1 之前添加 ‘-’ ,然后串联起来得到表达式 “+2-1” 。
返回可以通过上述方法构造的、运算结果等于 target 的不同 表达式 的数目。
sum为nums的和,将数组nums分为两个部分,一部分为取正号的整数,和为a,一部分为取负号的整数,和为b,则有:
a-b=sum
a+b=target
因此a=(sum+target)/2,此时该问题转化为01背包问题,背包容积为a
class Solution {
public:
int findTargetSumWays(vector<int>& nums, int target) {
int m=nums.size();
int i=0;
int j=0;
int sum=target;
for(auto e:nums){
sum+=e;
}
if(1==sum%2||sum<0){
return 0;
}
sum/=2;
vector<int> dp(sum+1,-1);
dp[0]=1;
for(i=0;i<m;++i){
for(j=sum;j>=0;--j){
int k=j-nums[i];
int tmp1=-1==dp[j]?0:dp[j];
int tmp2=k>=0&&-1!=dp[k]?dp[k]:0;
int tmp=tmp1+tmp2==0?-1:tmp1+tmp2;
dp[j]=tmp;
}
}
return -1==dp.back()?0:dp.back();
}
};
1049. 最后一块石头的重量 II
有一堆石头,用整数数组 stones 表示。其中 stones[i] 表示第 i 块石头的重量。
每一回合,从中选出任意两块石头,然后将它们一起粉碎。假设石头的重量分别为 x 和 y,且 x <= y。那么粉碎的可能结果如下:
如果 x == y,那么两块石头都会被完全粉碎;
如果 x != y,那么重量为 x 的石头将会完全粉碎,而重量为 y 的石头新重量为 y-x。
最后,最多只会剩下一块 石头。返回此石头 最小的可能重量 。如果没有石头剩下,就返回 0。
石头的粉碎就是在每一块石头前添上正号或者负号,假设sum为stones的和,将数组stones分为两个部分,一部分为取正号的整数,和为a,一部分为取负号的整数,和为b,则有:
a+b=sum
我们的目标就是求 | a-b |的最小值,因此需要找到一个和a使其尽可能接近b,即从stones中寻找一部分正子集使其和尽可能接近sum/2,此时问题就转换成一个01背包问题,背包容积为sum/2,物体 i 的容积和体积均为stones[i]
class Solution {
public:
int lastStoneWeightII(vector<int>& stones) {
int sum=0;
for(auto e:stones){
sum+=e;
}
int v=sum/2;
int i=0;
int j=0;
int m=stones.size();
vector<int> dp(v+1,0);
for(i=0;i<m;++i){
for(j=v;j>0;--j){
int k=j-stones[i];
int tmp1 = dp[j];
int tmp2 = k >= 0 ? dp[k] + stones[i] : 0;
dp[j] = max(tmp1, tmp2);
}
}
return sum-2*dp.back();
}
};
完全背包
DP42 【模板】完全背包
描述
你有一个背包,最多能容纳的体积是V。
现在有n种物品,每种物品有任意多个,第i种物品的体积为v ,价值为w
(1)求这个背包至多能装多大价值的物品?
(2)若背包恰好装满,求至多能装多大价值的物品?
输入描述:
第一行两个整数n和V,表示物品个数和背包体积。
接下来n行,每行两个数
v 和w ,表示第i种物品的体积和价值。
1≤n,
V≤1000
1≤n,V≤1000
输出描述:
输出有两行,第一行输出第一问的答案,第二行输出第二问的答案,如果无解请输出0。
问题一:
dp[ i ][ j ]表示从前 i 个物品中选择总体积不超过 j 的最大价值
①如果我们不将第 i 个物品装入背包,则dp[ i ][ j ] = dp[ i - 1][ j ]
②如果我们将第 i 个物品装入背包,装入的个数为n,则
dp[ i ][ j ] = max ( dp[ i - 1 ][ j - n*v[ i ] ] + n*w[ i ] ),n=1,2,3…
这两种情况可以统一为:
dp[ i ][ j ] = max( dp[ i - 1 ][ j - n*v[ i ] ] + n*w[ i ] ),n=0,1,2,3…
选择上面所有情况的最大值即可。
下面进行一定的算法优化:
由于
dp[ i ][ j - v [ i ] ] = max( dp[ i - 1 ][ j - v [ i ] - n*v[ i ] ] + n*w[ i ] ),n=0,1,2,3…
故
dp[ i ][ j - v [ i ] ] + w[ i ]
=max( dp[ i - 1 ][ j - v [ i ] - n*v[ i ] ] + n*w[ i ] ) + w[ i ],n=0,1,2,3…
=max( dp[ i - 1 ][ j - v [ i ] - n*v[ i ] ] + n*w[ i ] + w[ i ] ),n=0,1,2,3…
=max( dp[ i - 1 ][ j - n*v[ i ] ] + n*w[ i ] ),n=1,2,3…
此时刚好对应情况②
故
dp[ i ][ j ] = max( dp[ i ][ j - v [ i ] ] + w[ i ] , dp[ i - 1][ j ] )
问题二:
dp[ i ][ j ]表示从前 i 个物品中选择总体积恰好为 j 的最大价值,分析过程与问题一类似,只不过我们需要判断前一个状态是否处于切好装满的状态,用 -1 表示背包无法恰好装满。
如果dp[ i ][ j - v [ i ] ]!= -1,则
dp[ i ][ j ] = max( dp[ i ][ j - v [ i ] ] + w[ i ] , dp[ i - 1][ j ] )
否则
dp[ i ][ j ] = dp[ i - 1][ j ]
最后考虑进行一定的空间优化,我们发现无论是问题一还是问题二,当前状态只与当前行前面的状态和当前列前一个状态相关,因此可以考虑使用一个一维数组dp代替二维状态矩阵,dp的长度代表背包的容积,按照从左往右的顺序填表。
#include <iostream>
#include<vector>
#include<algorithm>
using namespace std;
int main() {
int n = 0;
int p = 0;
cin >> n >> p;
int m=n;
vector<int> w;
vector<int> v;
while (m--) {
int vi = 0;
int wi = 0;
cin >> vi >> wi;
v.push_back(vi);
w.push_back(wi);
}
int i = 0;
int j = 0;
int ans1 = 0;
int ans2 = 0;
//1
vector<int> dp1(p + 1, 0);
for (i = 0; i < n; ++i) {
for (j = 1; j<=p; ++j) {
int k=j-v[i];
int tmp1 = dp1[j];
int tmp2 = k >= 0 ? dp1[k] + w[i]:-1;
dp1[j] = max(tmp1, tmp2);
}
}
ans1 = dp1.back();
//2
vector<int> dp2(p + 1, -1);
dp2[0]=0;
for (i = 0; i < n; ++i) {
for (j = 1; j<=p; ++j) {
int k=j-v[i];
int tmp1 = dp2[j];
int tmp2 = k >= 0&&-1!=dp2[k] ? dp2[k] + w[i]:-1;
dp2[j] = max(tmp1, tmp2);
}
}
ans2 = dp2.back();
if(-1==ans2){
ans2=0;
}
printf("%d\n%d\n", ans1, ans2);
return 0;
}
// 64 位输出请用 printf("%lld")
322. 零钱兑换
给你一个整数数组 coins ,表示不同面额的硬币;以及一个整数 amount ,表示总金额。
计算并返回可以凑成总金额所需的 最少的硬币个数 。如果没有任何一种硬币组合能组成总金额,返回 -1 。
你可以认为每种硬币的数量是无限的。
dp[ i ][ j ]表示从前 i 种硬币中选择总和恰好为 j 的最少硬币数,-1表示当前没有任何一种组合使总和恰好为 j。
①如果我们不选择第 i 种硬币,则dp[ i ][ j ] = dp[ i - 1][ j ]
②如果我们选择了第 i 种硬币,选择该硬币的的个数为 n,则
dp[ i ][ j ] = min ( dp[ i - 1 ][ j - n*coins[ i ] ] + n ),n=1,2,3…,前提是 dp[ i - 1 ][ j - n*coins[ i ] ] != -1,n=1,2,3…
这两种情况可以统一为:
对 dp[ i - 1 ][ j - n*coins[ i ] ] != -1,n=0,1,2,3…
dp[ i ][ j ] = min( dp[ i - 1 ][ j - n*coins[ i ] ] + n),n=0,1,2,3…
选择上面所有情况的最小值即可。
下面进行一定的算法优化:
由于
dp[ i ][ j ] = min( dp[ i - 1 ][ j - n*coins[ i ] ] + n),n=0,1,2,3…
所以
dp[ i ][ j - coins [ i ] ]
= min( dp[ i - 1 ][ j - coins [ i ] - n*coins[ i ] ] + n ),n=0,1,2,3…
故
dp[ i ][ j - coins [ i ] ] +1
=min( dp[ i - 1 ][ j - coins [ i ] - n*coins[ i ] ] + n ) + 1,n=0,1,2,3…
=min( dp[ i - 1 ][ j - coins [ i ] - n*coins[ i ] ] + n + 1 ),n=0,1,2,3…
=min( dp[ i - 1 ][ j - n*coins[ i ] ] + n ),n=1,2,3…
此时刚好对应情况②
故
dp[ i ][ j ] = min( dp[ i ][ j - coins [ i ] ] + 1 , dp[ i - 1][ j ] ),n=0,1,2,3…
前提是dp[ i - 1 ][ j - n*coins[ i ] ] != -1,n=0,1,2,3…
最后的空间优化思路与前一题相同
class Solution {
public:
int coinChange(vector<int>& coins, int amount) {
int i=0;
int j=0;
int m=coins.size();
vector<int> dp(amount + 1, -1);
dp[0]=0;
for (i = 0; i < m; ++i) {
for (j = 1; j <= amount; ++j) {
int k=j-coins[i];
int tmp1 = dp[j];
int tmp2 = k>=0&&-1!=dp[k]?dp[k]+1:-1;
dp[j] = tmp1 > 0 && tmp2 > 0 ? min(tmp1, tmp2) : max(tmp1, tmp2);
}
}
return dp.back();
}
};
518. 零钱兑换 II
给你一个整数数组 coins 表示不同面额的硬币,另给一个整数 amount 表示总金额。
请你计算并返回可以凑成总金额的硬币组合数。如果任何硬币组合都无法凑出总金额,返回 0 。
假设每一种面额的硬币有无限个。
题目数据保证结果符合 32 位带符号整数。
dp[ i ][ j ]表示从前 i 种硬币中选择总和恰好为 j 的组合数,0表示当前没有任何一种组合使总和恰好为 j。
①如果我们不选择第 i 种硬币,则有 dp[ i - 1][ j ]种组合
②如果我们选择了n枚第 i 种硬币,则有组合数
sum ( dp[ i - 1 ][ j - n*coins[ i ] ] ),n=1,2,3…,
此时的前提是 dp[ i - 1 ][ j - n*coins[ i ] ] != 0,n=1,2,3…,由于该前提不影响最终结果,所以可以忽略该前提
这两种情况可以统一为:
dp[ i ][ j ] = sum( dp[ i - 1 ][ j - n*coins[ i ] ]),n=0,1,2,3…
选择上面所有情况的最小值即可。
下面进行一定的算法优化:
由于
dp[ i ][ j ] = sum( dp[ i - 1 ][ j - n*coins[ i ] ]),n=0,1,2,3…
所以
dp[ i ][ j - coins [ i ] ]
= sum( dp[ i - 1 ][ j - coins [ i ] - n*coins[ i ] ] ),n=0,1,2,3…
=sum ( dp[ i - 1 ][ j - n*coins[ i ] ] ),n=1,2,3…
此时刚好对应情况②
故
dp[ i ][ j ] =dp[ i ][ j - coins [ i ] ] + dp[ i - 1][ j ] )
最后的空间优化思路上上题相同
class Solution {
public:
int change(int amount, vector<int>& coins) {
vector<long long int> dp(amount+1,0);
dp[0]=1;
int i=0;
int j=0;
int m=coins.size();
for(i=0;i<m;++i){
for(j=coins[i];j<=amount;++j){
dp[j]+=dp[j-coins[i]];
dp[j]=dp[j]>INT_MAX?0:dp[j];
}
}
return dp.back();
}
};
279. 完全平方数
给你一个整数 n ,返回 和为 n 的完全平方数的最少数量 。
完全平方数 是一个整数,其值等于另一个整数的平方;换句话说,其值等于一个整数自乘的积。例如,1、4、9 和 16 都是完全平方数,而 3 和 11 不是。
将每一个完全平方数看成一种硬币,n看成总金额,该题就是 [322. 零钱兑换]的另一种问法
class Solution {
public:
int numSquares(int n) {
vector<int> nums;
int i=1;
while(true){
if(i*i>n) break;
nums.push_back(i*i);
++i;
}
vector<int> dp(n+1,INT_MAX/2);
dp[0]=0;
int j=0;
int m=nums.size();
for(i=0;i<m;++i){
for(j=nums[i];j<=n;++j){
int tmp1=dp[j];
int tmp2=dp[j-nums[i]]+1;
dp[j]=min(tmp1,tmp2);
}
}
return dp.back();
}
};
二维费用的背包问题
前面的背包问题在寻求目标时,只受到背包容积这一个限制,二维费用的背包问题则是受到两个条件限制,例如同时受到容积和重量限制。其可以分为二维费用的01背包问题和二维费用的完全背包问题
474. 一和零
给你一个二进制字符串数组 strs 和两个整数 m 和 n 。
请你找出并返回 strs 的最大子集的长度,该子集中 最多 有 m 个 0 和 n 个 1 。
如果 x 的所有元素也是 y 的元素,集合 x 是集合 y 的 子集 。
这就是二维费用的01背包问题
count[ i ][ 0 ]表示第 i 个字符串 0 的个数,count[ i ][ 1 ]表示第 i 个字符串 1 的个数,用dp[ i ][ j ][ k ]表示从前 i 个字符串中选择的最多 有 j个 0 和 k 个 1 的最大子集长度,对于第 i 个字符串,可以分为以下两种情况:
①不选择第 i 个字符串:此时dp[ i ][ j ][ k ] = dp[ i - 1 ][ j ][ k ]
②选择第 i 个字符串:此时
dp[ i ][ j ][ k ] = dp[ i - 1 ][ j - count[ i ][ 0 ] ][ k - count[ i ][ 1 ] ] + 1,
综合以上情况:
dp[ i ][ j ][ k ]
=max(dp[ i - 1 ][ j ][ k ] , dp[ i - 1 ][ j - count[ i ][ 0 ] ][ k - count[ i ][ 1 ] ] + 1)
下面考虑空间优化,由于dp[ i ][ j ][ k ]只与前一个 i 的 j、k的前面状态相关,可以考虑使用一个二维数组dp[ j ][ k ]描述不同字符串的状态,从该二维数组右下角往左上角填充。
class Solution {
public:
int findMaxForm(vector<string>& strs, int m, int n) {
vector<vector<int>> count;
int i=0;
int j=0;
int k=0;
for(i=0;i<strs.size();++i){
int countOfZero=0;
int countOfOne=0;
for(j=0;j<strs[i].size();++j){
if('0'==strs[i][j]) ++countOfZero;
if('1'==strs[i][j]) ++countOfOne;
}
count.push_back({countOfZero,countOfOne});
}
int s=count.size();
vector<vector<int>> dp(m+1,vector<int>(n+1,0));
for(i=0;i<s;++i){
for(j=m;j>=count[i][0];--j){
for(k=n;k>=count[i][1];--k){
dp[j][k] = max(dp[j][k], dp[j-count[i][0]][k-count[i][1]]+1);
}
}
}
return dp.back().back();
}
};
879. 盈利计划
集团里有 n 名员工,他们可以完成各种各样的工作创造利润。
第 i 种工作会产生 profit[i] 的利润,它要求 group[i] 名成员共同参与。如果成员参与了其中一项工作,就不能参与另一项工作。
工作的任何至少产生 minProfit 利润的子集称为 盈利计划 。并且工作的成员总数最多为 n 。
有多少种计划可以选择?因为答案很大,所以 返回结果模 10^9 + 7 的值。
dp[ i ][ j ][ k ]表示从前 i 个项目中可行的总人数不超过 j 利润至少为 k 的盈利计划,可以分为以下两种情况:
①不选择项目 i:此时的盈利计划的个数为dp[ i -1 ][ j ][ k ]
②选择项目 i:此时我们考虑状态
dp[ i -1 ][ j - group[ i ] ][ k - profit[ i ] ],由于计划的总人数不能超过 j,故我们必须保证 j - group[ i ] >=0。又计划的利润至少为 k ,故允许 profit[ i ] > k,即k - profit[ i ]允许小于0,此时无论前面的项目怎么组合,只要我们选择了项目 i ,利润一定至少为profit[ i ] > k,此时就等价于dp[ i ][ j ][ 0 ],可得此时的盈利计划的个数为:
dp[ i -1 ][ j - group[ i ] ][ max(0 , k - profit[ i ]) ]
最后可以得到状态方程:
dp[ i ][ j ][ k ]
= dp[ i -1 ][ j ][ k ] + dp[ i -1 ][ j - group[ i ] ][ max(0 , k - profit[ i ]) ]
class Solution {
public:
int profitableSchemes(int n, int minProfit, vector<int>& group, vector<int>& profit) {
int i=0;
int j=0;
int k=0;
int s=group.size();
int mod=pow(10,9)+7;
vector<vector<vector<int>>> dp(s+1,vector<vector<int>>(n+1,vector<int>(minProfit+1,0)));
for(j=0;j<=n;++j){
dp[0][j][0]=1;
}
for(i=1;i<=s;++i){
for(j=0;j<=n;++j){
for(k=0;k<=minProfit;++k){
dp[i][j][k]=dp[i-1][j][k];
if(j-group[i-1]>=0){
dp[i][j][k]+=dp[i-1][j-group[i-1]][max(0,k-profit[i-1])];
}
dp[i][j][k]%=mod;
}
}
}
return dp.back().back().back();
}
};
下面空间优化的思路与前面的题一样
class Solution {
public:
int profitableSchemes(int n, int minProfit, vector<int>& group, vector<int>& profit) {
int i=0;
int j=0;
int k=0;
int s=group.size();
int mod=pow(10,9)+7;
vector<vector<int>> dp(n+1,vector<int>(minProfit+1,0));
for(j=0;j<=n;++j){
dp[j][0]=1;
}
for(i=1;i<=s;++i){
for(j=n;j>=group[i-1];--j){
for(k=minProfit;k>=0;--k){
dp[j][k]=dp[j][k]=dp[j][k]+dp[j-group[i-1]][max(0,k-profit[i-1])];
dp[j][k]%=mod;
}
}
}
return dp.back().back();
}
};
其它
最后一类动态规划的状态表示需要我们结合实际问题,根据问题分析的过程,发现重复的子问题,抽象出状态表示。
377. 组合总和 Ⅳ
给你一个由 不同 整数组成的数组 nums ,和一个目标整数 target 。请你从 nums 中找出并返回总和为 target 的元素组合的个数。
题目数据保证答案符合 32 位整数范围。
这看似是一个背包问题,实际上用背包问题的思路难以解决,我们考虑使用一个数组dp[ i ]表示和为 i 的组合总数。现在我们将nums的任意组合分为以下几类:
①末尾数为nums[0]的
②末尾数为nums[1]的
…
末尾数为nums[n]的
如果想要找到和为 i 的组合数,我们只需要在第①类里面找到除去尾数后和为i - nums[ 0 ]的组合数,在第②类里面找到除去尾数后和为i - nums[ 1 ]的组合数…,最后将所获得的组合数相加即可获得和为 i 的组合数。
class Solution {
public:
int combinationSum4(vector<int>& nums, int target) {
vector<unsigned long long int> dp(target+1,0);
dp[0]=1;
int i=0;
int j=0;
int m=nums.size();
for(i=1;i<=target;++i){
for(j=0;j<m;++j){
dp[i]+=i-nums[j]>=0?dp[i-nums[j]]:0;
}
}
return dp.back();
}
};
96. 不同的二叉搜索树
给你一个整数 n ,求恰由 n 个节点组成且节点值从 1 到 n 互不相同的 二叉搜索树 有多少种?返回满足题意的二叉搜索树的种数。
dp[ i ]表示由 i 个节点组成的二叉搜索树的个数,将二叉搜索树分为左子树、根节点、右子三个部分,只要确定了根节点为 j ,那么左子树就只能有j - 1个节点,右子树就只能有 i - j个节点,此时我们只需要知道左子树和右子树二叉搜索树的个数,就可以知道以 j 为根节点的二叉搜索树的个数
故dp[ i ]=dp[ j - 1 ] * dp[ i - j ]
class Solution {
public:
int numTrees(int n) {
vector<int> dp(n+1,0);
dp[0]=1;
int i=1;
int j=1;
for(i=1;i<=n;++i){
for(j=1;j<=i;++j){
dp[i]+=dp[j-1]*dp[i-j];
}
}
return dp.back();
}
};