动态规划总结
一、状态转移问题
\70. 爬楼梯
假设你正在爬楼梯。需要 n
阶你才能到达楼顶。
每次你可以爬 1
或 2
个台阶。你有多少种不同的方法可以爬到楼顶呢
class Solution {
public:
int climbStairs(int n) {
vector<int > dp(n+1,0);
if(n<3) return n;
dp[1] = 1;
dp[2] = 2;
for(int i = 3;i<=n;i++){
dp[i] = dp[i-1]+dp[i-2];
}
return dp[n];
}
};
63. 不同路径 II
一个机器人位于一个 m x n
网格的左上角 (起始点在下图中标记为 “Start” )。
机器人每次只能向下或者向右移动一步。机器人试图达到网格的右下角(在下图中标记为 “Finish”)。
现在考虑网格中有障碍物。那么从左上角到右下角将会有多少条不同的路径?
网格中的障碍物和空位置分别用 1
和 0
来表示。
class Solution {
public:
int uniquePathsWithObstacles(vector<vector<int>>& obstacleGrid) {
int m = obstacleGrid.size();
int n = obstacleGrid[0].size();
vector<vector<int>> dp(m,vector<int>(n,0));
for(int i = 0;i<m&&obstacleGrid[i][0]==0;i++) dp[i][0]=1;
for(int i = 0;i<n&&obstacleGrid[0][i]==0;i++) dp[0][i]=1;
for(int i = 1;i<m;i++){
for(int j = 1;j<n;j++){
if(obstacleGrid[i][j]==0) dp[i][j]=dp[i-1][j]+dp[i][j-1];
}
}
return dp[m-1][n-1];
}
};
有障碍物的时候,如果障碍物在边界上,题目给的obscale测试用例走不通的地方都是1,所以障碍后面每个点,dp边界初始化要加if判断
若障碍物在中间,则在障碍物所处的i,j处不更新dp,即该点路径数为0,需要加if判断
\343. 整数拆分
给定一个正整数 n
,将其拆分为 k
个 正整数 的和( k >= 2
),并使这些整数的乘积最大化。
class Solution {
public:
int integerBreak(int n) {
vector<int> dp(n + 1);
dp[2] = 1;
for (int i = 3; i <= n ; i++) {
for (int j = 1; j <= i / 2; j++) {
dp[i] = max(dp[i], max((i - j) * j, dp[i - j] * j));
}
}
return dp[n];
}
};
由于递推公式中需要用到第二个变量,所以需要嵌套两个for循环
122 买卖股票2
给你一个整数数组 prices ,其中 prices[i] 表示某支股票第 i 天的价格。
在每一天,你可以决定是否购买和/或出售股票。你在任何时候 最多 只能持有 一股 股票。你也可以先购买,然后在 同一天 出售。
返回 你能获得的 最大 利润 。
class Solution {
public:
int maxProfit(vector<int>& prices) {
vector<vector<int>>dp(prices.size(),vector<int>(2,0));
dp[0][0]=0;
dp[0][1]=-prices[0];
for(int i=1;i<prices.size();i++){
dp[i][0] = max(dp[i-1][0],dp[i-1][1]+prices[i]);
dp[i][1] = max(dp[i-1][0]-prices[i],dp[i-1][1]);
}
return max(dp[prices.size()-1][1],dp[prices.size()-1][0]);
}
};
这道题目中,将问题抽象为股票数组分别有两个状态,取两个状态的两组dp最大值
公共子串计算
给定两个只包含小写字母的字符串,计算两个字符串的最大公共子串的长度。
注:子串的定义指一个字符串删掉其部分前缀和后缀(也可以不删)后形成的字符串。
#include <iostream>
#include<string>
#include<vector>
using namespace std;
int max(string& s1,string& s2){
int m = s1.size();
int n = s2.size();
vector<vector<int>> dp(m+1,vector<int>(n+1,0));
int maxlen=0;
for(int i=0;i<=m;i++) dp[i][0]=0;
for(int i=0;i<=n;i++) dp[0][i]=0;
for(int i =1;i<=m;i++){
for(int j=1;j<=n;j++){
if(s1[i-1]==s2[j-1]) dp[i][j] = dp[i-1][j-1]+1;
// else dp[i][j]=0;
maxlen = maxlen<dp[i][j]? dp[i][j]:maxlen;
}
}
return maxlen;
}
int main() {
string s1,s2;
cin>>s1>>s2;
cout<<max(s1,s2);
return 0;
}
// 64 位输出请用 printf("%lld")
在这道题目中,相当于把两个字符串的对比问题,拉成了一个二维的状态表,然后进行动态规划
判断回文
Catcher是MCA国的情报员,他工作时发现敌国会用一些对称的密码进行通信,比如像这些ABBA,ABA,A,123321,但是他们有时会在开始或结束时加入一些无关的字符以防止别国破解。比如进行下列变化 ABBA->12ABBA,ABA->ABAKK,123321->51233214 。因为截获的串太长了,而且存在多种可能的情况(abaaab可看作是aba,或baaab的加密形式),Cathcer的工作量实在是太大了,他只能向电脑高手求助,你能帮Catcher找出最长的有效密码串吗?
#include <iostream>
#include<string>
#include<vector>
using namespace std;
int huiween(string& s){
int n = s.size();
vector<vector<bool>>dp(n+1,vector<bool>(n+1,false));
int maxlen =1;
for(int i=0;i<n;i++){
for(int j=0;j<=i;j++){
if(i==j) dp[j][i]=true;
else if((i-j)==1&&s[i]==s[j]) dp[j][i]=true;
else dp[j][i]= (s[i]==s[j]&&dp[j+1][i-1] );
if(dp[j][i]){
maxlen = maxlen<(i-j+1)? (i-j+1):maxlen;
}
}
}
return maxlen;
}
int main() {
string s;
cin>>s;
cout<<huiween(s);
return 0;
}
// 64 位输出请用 printf("%lld")
在这道题目中,dp定义为从j到i是否为回文的状态。然后递归方程逐渐缩减到j-i=1或j=i
二、背包问题
1、组合01背包
在01背包中,如果背包内的方案没有顺序不同,但是都算一种,则是组合。外部遍历物品,内部遍历背包
\416. 分割等和子集
给你一个 只包含正整数 的 非空 数组 nums
。请你判断是否可以将这个数组分割成两个子集,使得两个子集的元素和相等。
class Solution {
public:
bool canPartition(vector<int>& nums) {
vector<int> dp(10001,0);
int sum=0;
for(auto c:nums) sum+=c;
if(sum%2==1) return false;
int target = sum/2;
for(int i = 0;i<nums.size();i++){
for(int j = target;j>=nums[i];j--){
dp[j] = max(dp[j],dp[j-nums[i]]+nums[i]);
}
}
if(dp[target]==target) return true;
return false;
}
};
首先定义dp为当前背包重量的价值。
然后归纳递推公式,在j容量下dp和j-i容量+i价值的dp取最大值
确定遍历顺序,先遍历物品,再遍历背包容量
1049.最后一块石头的重量II
有一堆石头,用整数数组 stones
表示。其中 stones[i]
表示第 i
块石头的重量。
每一回合,从中选出任意两块石头,然后将它们一起粉碎。假设石头的重量分别为 x
和 y
,且 x <= y
。那么粉碎的可能结果如下:
class Solution {
public:
int lastStoneWeightII(vector<int>& stones) {
vector<int> dp(1501,0);
int sum = 0;
for(auto c:stones) sum+=c;
int target = sum/2;
for(int i = 0;i<stones.size();i++){
for(int j = target;j>=stones[i];j--){
dp[j] = max(dp[j],dp[j-stones[i]]+stones[i]);
}
}
return sum-2*dp[target];
}
};
这道题本质上,是把石头分成两堆,然后求差值。
先遍历物品,再遍历背包容量。当只有物品1时,遍历背包容量进行初始化。然后剩下的物品依次进行叠加,并判断max
\494. 目标和
给你一个整数数组 nums
和一个整数 target
。
向数组中的每个整数前添加 '+'
或 '-'
,然后串联起所有整数,可以构造一个 表达式 :
- 例如,
nums = [2, 1]
,可以在2
之前添加'+'
,在1
之前添加'-'
,然后串联起来得到表达式"+2-1"
。
返回可以通过上述方法构造的、运算结果等于 target
的不同 表达式 的数目。
class Solution {
public:
int findTargetSumWays(vector<int>& nums, int target) {
int sum = 0;
for(auto c:nums) sum+=c;
if(abs(target)>sum||(target+sum)%2==1) return 0;
int range = (sum+target)/2;
vector<int>dp(range+1,0);
dp[0]=1;
for(int i = 0;i<nums.size();i++){
for(int j =range;j>=nums[i];j-- ){
dp[j] += dp[j-nums[i]];
}
}
return dp[range];
}
};
题目本质是将nums数组分为left和right 两部分,left-right=target
left+right = sum
即求满足条件的left有多少种。所以定义dp[j]为,大小为j的背包,有多少种组合可以填满
递推公式为dp[j] = dp[j-nums[i]],然后再i的循环中,进行物品累加
j的循环中
2、排列01背包
如果是排列问题,外部遍历背包,内部遍历物品
70 爬楼梯
假设你正在爬楼梯。需要 n 阶你才能到达楼顶。
每次你可以爬 1 或 2 个台阶。你有多少种不同的方法可以爬到楼顶呢?
注意:给定 n 是一个正整数。
#include<bits/stdc++.h>
using namespace std;
int n;
int main(){
cin>>n;
vector<int>dp(n+1,0);
dp[0]=1;
for(int i=1;i<=n;i++){
for(int j = 1;j<=2;j++){
if(i-j>=0) dp[i] += dp[i-j];
}
}
cout<<dp[n];
return 0;
}
2、完全背包
518 、零钱兑换
给你一个整数数组 coins 表示不同面额的硬币,另给一个整数 amount 表示总金额。
请你计算并返回可以凑成总金额的硬币组合数。如果任何硬币组合都无法凑出总金额,返回 0 。
假设每一种面额的硬币有无限个。
题目数据保证结果符合 32 位带符号整数。
class Solution {
public:
int change(int amount, vector<int>& coins) {
vector<int>dp(amount+1,0);
dp[0]=1;
for(int i = 0;i<coins.size();i++){
for(int j = 0;j<=amount;j++){
if(j>=coins[i]) dp[j]+=dp[j-coins[i]];
}
}
return dp[amount];
}
};
递归公式中,本题dp[j]就是所有的dp[j-coins[i]],所以不需要+1
本题的遍历顺序,在完全背包问题中,先遍历物品为组合,先遍历背包为排列
139 单词划分
给你一个字符串 s 和一个字符串列表 wordDict 作为字典。请你判断是否可以利用字典中出现的单词拼接出 s 。
注意:不要求字典中出现的单词全部都使用,并且字典中的单词可以重复使用。
class Solution {
public:
bool wordBreak(string s, vector<string>& wordDict) {
vector<bool> dp(s.size()+1,false);
unordered_set<string> set(wordDict.begin(),wordDict.end());
dp[0] = true;
for(int i = 1;i<=s.size();i++){
for(int j = 0;j<i;j++){
string str = s.substr(j,i-j);
if(set.find(str)!=set.end()&&dp[j]){
dp[i]=true;
}
}
}
return dp[s.size()];
}
};
在本题中,将s的长度作为背包容量。遍历物品即这个s的大小内存在的所有字串。所以i-j不是从终点减去起点的距离。而是这段字串最大就是i,通过j实现字串大小遍历。
由于i-j的定义,导致寻找起点为j的字串是否在字典中是,需要判断dp[j]是否为true。即大小为j,索引为j-1的字串是否在字典中。
三、最大子序列问题
300. 最长递增子序列
给你一个整数数组 nums ,找到其中最长严格递增子序列的长度。
子序列 是由数组派生而来的序列,删除(或不删除)数组中的元素而不改变其余元素的顺序。例如,[3,6,2,7] 是数组 [0,3,1,6,2,2,7] 的子序列。
class Solution {
public:
int lengthOfLIS(vector<int>& nums) {
vector<int>dp(nums.size(),1);
int result=1;
for(int i =1;i<nums.size();i++){
for(int j =0;j<i;j++){
if(nums[i]>nums[j]) dp[i]=max(dp[i],dp[j]+1);
}
result = result<dp[i]? dp[i]:result;
}
return result;
}
};
在最大子序列问题中,定义dp为包括当前数字的最大子序列长度,所以需要使用两层循环,外层起始以为1,遍历所有元素。内层起始以为0,遍历当前元素以前的所有元素,并依次和当前元素比较大小,计数。
由于每个元素的最大序列肯定包含自己,所以初始化为1
需要注意的时,最大子序列都是按当前元素来进行比较,所以需要一个int变量来存取中间值,否则替换元素后,最大值会被覆盖
合唱团
N 位同学站成一排,音乐老师要请最少的同学出列,使得剩下的 K 位同学排成合唱队形。
K名同学排成了合唱队形。
通俗来说,能找到一个同学,他的两边的同学身高都依次严格降低的队形就是合唱队形。
例子:
123 124 125 123 121 是一个合唱队形
123 123 124 122不是合唱队形,因为前两名同学身高相等,不符合要求
123 122 121 122不是合唱队形,因为找不到一个同学,他的两侧同学身高递减。
你的任务是,已知所有N位同学的身高,计算最少需要几位同学出列,可以使得剩下的同学排成合唱队形。
#include <iostream>
#include<vector>
using namespace std;
int main(){
int n;
int tmp;
vector<int> vec;
cin>>n;
while(cin>>tmp) vec.push_back(tmp);
if(vec.size()<=1) return vec.size();
vector<int>dp1(n,1);
for(int i =0;i<n;++i){
for(int j=0;j<i;++j){
if(vec[i]>vec[j]) dp1[i] = max(dp1[i],dp1[j]+1);
}
}
vector<int>dp2(n,1);
for(int i =n-1;i>=0;--i){
for(int j=n-1;j>i;--j){
if(vec[i]>vec[j]) dp2[i] = max(dp2[i],dp2[j]+1);
}
}
int maxlen =0;
for(int i =0;i<n;i++){
if(dp1[i]+dp2[i]-1<=n) maxlen = max(maxlen,dp1[i]+dp2[i]-1);
}
cout<<n-maxlen<<endl;
return 0;
}
在这道题目中,可以把合唱队列抽象为首尾两个,最大递增子序列,并且由于尾部子序列是倒序。
最后合唱队列的最大长度为dp1+dp2-1,相当于每一次外部for循环遍历,两个dp相加都是刚好等于总人数+1
// 64 位输出请用 printf("%lld")
公共子串计算
给定两个只包含小写字母的字符串,计算两个字符串的最大公共子串的长度。
注:子串的定义指一个字符串删掉其部分前缀和后缀(也可以不删)后形成的字符串。
#include <iostream>
#include<string>
#include<vector>
using namespace std;
int max(string& s1,string& s2){
int m = s1.size();
int n = s2.size();
vector<vector<int>> dp(m+1,vector<int>(n+1,0));
int maxlen=0;
for(int i=0;i<=m;i++) dp[i][0]=0;
for(int i=0;i<=n;i++) dp[0][i]=0;
for(int i =1;i<=m;i++){
for(int j=1;j<=n;j++){
if(s1[i-1]==s2[j-1]) dp[i][j] = dp[i-1][j-1]+1;
// else dp[i][j]=0;
maxlen = maxlen<dp[i][j]? dp[i][j]:maxlen;
}
}
return maxlen;
}
int main() {
string s1,s2;
cin>>s1>>s2;
cout<<max(s1,s2);
return 0;
}
// 64 位输出请用 printf("%lld")
在这道题目中,相当于把两个字符串的对比问题,拉成了一个二维的状态表,然后进行动态规划。
实际上二维表的递增公式dp[i][j] = dp[i-1][j-1]+1是寻找连续相同的子序列
编辑距离
Levenshtein 距离,又称编辑距离,指的是两个字符串之间,由一个转换成另一个所需的最少编辑操作次数。许可的编辑操作包括将一个字符替换成另一个字符,插入一个字符,删除一个字符。编辑距离的算法是首先由俄国科学家 Levenshtein 提出的,故又叫 Levenshtein Distance 。
例如:
字符串A: abcdefg
字符串B: abcdef
通过增加或是删掉字符 ”g” 的方式达到目的。这两种方案都需要一次操作。把这个操作所需要的次数定义为两个字符串的距离。
要求:
给定任意两个字符串,写出一个算法计算它们的编辑距离。
#include <iostream>
#include<string>
#include<vector>
using namespace std;
int distance(string& s1,string& s2){
int m = s1.size();
int n = s2.size();
vector<vector<int>> dp(m+1,vector<int>(n+1,0));
for(int i=1;i<=m;i++) dp[i][0]=i;
for(int i=1;i<=n;i++) dp[0][i]=i;
for(int i=1;i<=m;i++){
for(int j=1;j<=n;j++){
if(s1[i-1]==s2[j-1]){
dp[i][j] = dp[i-1][j-1];
}
else if(s1[i-1]!=s2[j-1]) dp[i][j] = min(min(dp[i-1][j]+1,dp[i][j-1]+1),dp[i-1][j-1]+1);
}
}
return dp[m][n];
}
int main() {
string s1,s2;
cin>>s1>>s2;
cout<<distance(s1, s2);
return 0;
}
// 64 位输出请用 printf("%lld")
dp定义为i和j之前位置需要修改的最小操作数,画出二维表,发现三条路径都可以以行动,所以需要三者取最小
四、前缀和问题
激光炮
现在有一种新型的激光炸弹,可以摧毁一个包含 R×R�×� 个位置的正方形内的所有目标。
激光炸弹的投放是通过卫星定位的,但其有一个缺点,就是其爆炸范围,即那个正方形的边必须和 x,y�,� 轴平行。
求一颗炸弹最多能炸掉地图上总价值为多少的目标。
#include<iostream>
using namespace std;
const int N = 5e3+10;
int s[N][N];
int n,r;
int main(){
cin>>n>>r;
r = min(5001,r);
for(int i = 0;i<n;i++){
int x,y,w;
cin>>x>>y>>w;
x++;
y++;
s[x][y]+=w;
}
for(int i = 1;i<=5001;i++){
for(int j =1;j<=5001;j++){
s[i][j] += s[i][j-1]+s[i-1][j]-s[i-1][j-1];
}
}
int ans = 0;
for(int i =r;i<=5001;i++){
for(int j = r;j<=5001;j++){
ans = max(ans,s[i][j]-s[i-r][j]-s[i][j-r]+s[i-r][j-r]);
}
}
cout<<ans<<endl;
return 0;
}