算法复习:动态规划
一、动归引入
leetcode 70. 爬楼梯
class Solution { public: int climbStairs(int n) { map<int,int>donser; for(int i=1;i<=n;i++) { if(i==0||i==1||i==2) donser[i]=i; else donser[i]=donser[i-1]+donser[i-2]; } return donser[n]; } }; ------------------------------------------------------------------------ class Solution { public: int climbStairs(int n) { if(n<=3) return n; int now=3,last_1=2,last_2=1; for(int i=3;i<=n;i++) { now=last_1+last_2; last_2=last_1; last_1=now; } return now; } };
leetcode 198. 打家劫舍
前一个位置取了值,下一个位置就不能再取,寻找最大的组合。dp[i]表示抢了i处以后的最大总金额,dp[i]=maxl+nums[i] (maxl=(maxl>dp[0->i-1]?maxl:dp[0->i-1]))
class Solution { public: int rob(vector<int>& nums) { if(nums.size()==0) return 0; if(nums.size()<2) return nums[0]; vector<int>dp(nums.size(),0); dp[0]=nums[0]; dp[1]=nums[1]; for(int i=2;i<nums.size();i++) { int maxl=0; for(int j=0;j<i-1;j++) maxl=(maxl>dp[j]?maxl:dp[j]); dp[i]=maxl+nums[i]; } return (dp[nums.size()-1]>dp[nums.size()-2]?dp[nums.size()-1]:dp[nums.size()-2]); } };
leetcode 213. 打家劫舍 II
在上一题基础上做了首尾相连,也就是头和尾只能选一个。改进方法就是分别查找不含头的最大价值和不含尾的最大价值,输出二者中的最大值
#include<map> class Solution { public: int deal(vector<int>& nums) { vector<int>dp(nums.size(),0); dp[0]=nums[0]; dp[1]=nums[1]; for(int i=2;i<nums.size();i++) { int maxl=0; for(int j=0;j<i-1;j++) maxl=(maxl>dp[j]?maxl:dp[j]); dp[i]=maxl+nums[i]; } return (dp[nums.size()-1]>dp[nums.size()-2]?dp[nums.size()-1]:dp[nums.size()-2]); } int rob(vector<int>& nums) { if(nums.size()==0) return 0; if(nums.size()==1) return nums[0]; if(nums.size()==2) return max(nums[0],nums[1]); vector<int> num1,num2; for(int i=0;i<nums.size();i++) { if(i==0) { num2.push_back(nums[i]); continue; } if(i==nums.size()-1) { num1.push_back(nums[i]); continue; } num1.push_back(nums[i]); num2.push_back(nums[i]); } return max(deal(num1),deal(num2)); } };
递推 f(n,x)=f(n-1,x-1)+f(n-1,x-2)+f(n-1,x-3)+f(n-1,x-4)+f(n-1,x-5)+f(n-1,x-6)
class Solution { public: int f(int n,int x,vector<vector<int> >&dp) { if(x<=0||n<=0) return 0; if(dp[n][x]!=0) return dp[n][x]; int sum=0; for(int i=1;i<=6;i++) { sum+=f(n-1,x-i,dp); } dp[n][x]=sum; return sum; } vector<double> twoSum(int n) { vector<vector<int> >dp(n+1,vector<int>(6*n+1,0)); vector<double>result; for(int i=1;i<=6;i++) dp[1][i]=1; int sum=0,ll=0; for(int x=n;x<=int((n*6+n)/2);x++) { ll=f(n,x,dp); sum+=ll; result.push_back(ll); } int size=result.size(); sum*=2; if(n%2==0) sum-=ll; for(int i=size-1;i>=0;i--) { if(n%2==0&&i==size-1) { result[i]=result[i]/sum; continue; } result[i]=result[i]/sum; result.push_back(result[i]); } return result; } };
72. 编辑距离
递推三个状态:
替换:到dp[i-1][j-1]到k步,那么word1[i-1]==word2[j-1]时dp[i][j]要k步,≠时k+1步
删除:到dp[i-1][j]要k步,那么到dp[i][j]要k+1步
添加:到dp[i][j-1]要k步,那么到dp[i][j]要k+1步
const int inf = 0x3f3f3f3f; class Solution { public: int minDistance(string word1, string word2) { int a=word1.size(); int b=word2.size(); if(a==0||b==0) return a+b; vector<vector<int> >dp(a+1,vector<int>(b+1,inf)); for(int i=0;i<=a;i++) dp[i][0]=i; for(int j=0;j<=b;j++) dp[0][j]=j; for(int i=1;i<=a;i++) { for(int j=1;j<=b;j++) { if(word1[i-1]==word2[j-1]) dp[i][j]=min(dp[i-1][j-1],dp[i][j]); else dp[i][j]=min(dp[i-1][j-1]+1,dp[i][j]); dp[i][j]=min(dp[i-1][j]+1,dp[i][j]); dp[i][j]=min(dp[i][j-1]+1,dp[i][j]); } } /*for(int i=1;i<=a;i++) { for(int j=1;j<=b;j++) { cout<<dp[i][j]<<" "; } cout<<endl; }*/ return dp[a][b]; } };
5. 最长回文子串
设dp[i][j]==1表示从i到j是一个回文串
dp[i][j]=1的条件是dp[i+1][j-1]==1且s[i]==s[j]
特殊情况*bb*这样子,判断一下j-i是不是==1,是的话dp[i][j]=1
class Solution { public: string longestPalindrome(string s) { if(s.size()<=1) return s; int size=s.size(); int max_len=1; int start=0; vector<vector<int> >dp(size,vector<int>(size,0)); for(int i=0;i<size;i++)//对角线置1 dp[i][i]=1; for(int i=size-1;i>=0;i--) { for(int j=i+1;j<size;j++) { if(s[i]==s[j]) { if(j-i==1)//处理bb情况 { dp[i][j]=1; max_len=max(max_len,j-i+1); if(max_len==j-i+1) start=i; } else if(dp[i+1][j-1]==1) { dp[i][j]=dp[i+1][j-1]; max_len=max(max_len,j-i+1); if(max_len==j-i+1) start=i; } } } } return s.substr(start,max_len); } };
二、0-1背包
01背包经典模板
01背包列出了所有可用的元素,每个元素可用一次或者不用。
n是n件物品,m是背包最大容量,v[]存每件物品的价值,w[]存每件物品的体积,
f[]维护一个一维数组:f(m) 当前背包容量为m的情况下取得的价值为f(m)
状态转移方程:f(m)=max( f(m) , f(m-w[i])+v[i] ) 表示不选当前物品 或者 选择当前物品
容量从大到小遍历,那么到最后的f[m]取决的结果是基于上一物品选择
#include<bits/stdc++.h> using namespace std; const int N=1010; int f[N]; int v[N],w[N]; int n,m; int main() { cin>>n>>m; for(int i=0;i<n;i++) cin>>v[i]>>w[i]; for(int i=0;i<n;i++) for(int j=m;j>=w[i];j--) f[j]=max(f[j],f[j-w[i]]+v[i]); cout<<f[m]<<endl; }
leetcode 416. 分割等和子集
class Solution { public: bool canPartition(vector<int>& nums) { int sum=0; vector<int>v,w,f;//v存价值 w存体积 f[i]=j表示容积为i时价值为j for(int i=0;i<nums.size();i++) { sum+=nums[i]; v.push_back(nums[i]); w.push_back(nums[i]); } if(sum%2==1) return false; sum/=2;//背包容量为sum for(int i=0;i<=sum;i++)//初始化f f.push_back(0); for(int i=0;i<nums.size();i++) { for(int j=sum;j>=w[i];j--) { f[j]=max(f[j],f[j-w[i]]+v[i]); } } if(f[sum]==sum) return true; return false; } };
leetcode 474. 一和零
本题是01背包的扩展,二维费用背包,有两个费用指标,多加一层内循环即可
class Solution { public: int findMaxForm(vector<string>& strs, int m, int n) { vector<int>goods_0; vector<int>goods_1; vector<vector<int>>dp(m+1,vector<int>(n+1,0)); for(int i=0;i<strs.size();i++)//创建输入 { string tmp=strs[i]; int x=0,y=0; for(int j=0;j<tmp.size();j++) { if(tmp[j]=='0') x++; if(tmp[j]=='1') y++; } goods_0.push_back(x); goods_1.push_back(y); } for(int i=0;i<strs.size();i++) for(int j=m;j>=goods_0[i];j--) for(int k=n;k>=goods_1[i];k--) dp[j][k]=max(dp[j][k],dp[j-goods_0[i]][k-goods_1[i]]+1); return dp[m][n]; } };
leetcode 494. 目标和
01背包的变体,即:每一个数字都要用,每一个数字或是加或是减,下标需要平移到正数
dp[i][j]=A表示截止到第i+1个数,这i+1个数的数字和为j时的方案数为A个。 状态转移方程:dp[i][j]=dp[i-1][j-nums[i]]+dp[i-1][j+nums[i]]
class Solution { public: int findTargetSumWays(vector<int>& nums, int S) { int count=0; for(int i=0;i<nums.size();i++) count+=nums[i]; if(count<S) return 0; vector<vector<int>>dp(nums.size()+1,vector<int>((count+2)*2,0)); dp[0][nums[0]*(-1)+count]+=1; dp[0][nums[0]+count]+=1; for(int i=1;i<nums.size();i++) { for(int j=0;j<=count*2;j++) { int x=(j-nums[i])>0?(j-nums[i]):0; int y=(j+nums[i])<=count*2?(j+nums[i]):0; dp[i][j]=dp[i-1][x]+dp[i-1][y]; } } return dp[nums.size()-1][abs(S)+count]; } };
三、最长公共子序列
leetcode 1143. 最长公共子序列
分析,两串匹配串放在一起组成一个二维表,逐层更新表内内容,直到最后。
状态转移方程:A==B时 dp[i][j]=dp[i-1][j-1]+1
A!=B时 dp[i][j]=max( dp[i-1][j] , dp[i][j-1] )
class Solution { public: int longestCommonSubsequence(string text1, string text2) { vector<vector<int>> dp(text2.size()+1,vector<int>(text1.size()+1,0));//初始化 for(int i=1;i<=text2.size();i++) { char A=text2[i-1]; for(int j=1;j<=text1.size();j++) { char B=text1[j-1]; if(A==B) dp[i][j]=dp[i-1][j-1]+1; else dp[i][j]=max(dp[i-1][j],dp[i][j-1]); } } return dp[text2.size()][text1.size()]; } };
优化:将二维数组改成只有两行的数组节省空间,原理:原来解法的更新只看两行继续存着意义不大,因此只留两行
class Solution { public: int longestCommonSubsequence(string text1, string text2) { vector<vector<int>> dp(2,vector<int>(text1.size()+1,0)); for(int i=1;i<=text2.size();i++) { char A=text2[i-1]; for(int j=1;j<=text1.size();j++) { char B=text1[j-1]; if(A==B) dp[i&1][j]=dp[(i-1)&1][j-1]+1; else dp[i&1][j]=max(dp[(i-1)&1][j],dp[i&1][j-1]); } } return dp[text2.size()&1][text1.size()]; } };
四、最长上升子序列
leetcode 300. 最长上升子序列
分析:在一个数字串中寻找最长上升,用一个一维dp。dp[i]表示截至当前i位置最长的上升序列
状态转移方程:dp[i]=max(dp[j]+1 j: 0->i-1)
class Solution { public: int maxl(vector<int>&dp) { int max=0; for(int i=0;i<dp.size();i++) { if(dp[i]>max) max=dp[i]; } return max; } int lengthOfLIS(vector<int>& nums) { vector<int>dp(nums.size(),0); for(int i=0;i<nums.size();i++) { int lable=0; if(i==0) { dp[i]=1; continue; } for(int j=i-1;j>=0;j--) { if(nums[j]<nums[i]&&dp[i]<dp[j]+1) { dp[i]=dp[j]+1; lable=1; } } if(lable==0) dp[i]=1; } return maxl(dp); } }; --------------------------------------------------------------- 优化 dp初值付1这样就不要lable了 class Solution { public: int maxl(vector<int>&dp) { int max=0; for(int i=0;i<dp.size();i++) { if(dp[i]>max) max=dp[i]; } return max; } int lengthOfLIS(vector<int>& nums) { vector<int>dp(nums.size(),1); for(int i=0;i<nums.size();i++) { for(int j=i-1;j>=0;j--) { if(nums[j]<nums[i]&&dp[i]<dp[j]+1) dp[i]=dp[j]+1; } } return maxl(dp); } };
五、完全背包
完全背包相比于01背包,每件物品可以取无限多次,完全背包的核心代码:(简记和01背包的不同之处在于第二层循环)
dp[m]表示的就是当容量<=m时的最优解是多少,可能背包并没有装满。如果需要恰好容量==m时的最优解是多少,初始化dp[0]=0 其他的初始化为-∞
dp[j]表示容量最大为j时所能存放的最大价值。 状态转移方程:dp[j]=max(dp[j],dp[j-v[i]]+w[i])
for(int i=0;i<N;i++) for(int j=v[i];j<=m;j++) dp[j]=max(dp[j],dp[j-w[i]]+v[i]);
leetcode 322. 零钱兑换
这道题不是纯的完全背包,因为恰好装满背包的方法有无数种,而本体需要的结果是所有结果中所用数量最少的那一个解
dp[j]表示容量最大为j时所用的最少硬币数量。 状态转移方程:dp[j]=min(dp[j],dp[j-w[i]]+1)
#define MAX 999999 int minl(int a,int b) { return (a>b?b:a); } class Solution { public: int coinChange(vector<int>& coins, int amount) { vector<int>dp(amount+1,MAX); dp[0]=0; for(int i=0;i<coins.size();i++) { for(int j=coins[i];j<=amount;j++) { dp[j]=minl(dp[j],dp[j-coins[i]]+1); } } return (dp[amount]==MAX?-1:dp[amount]); } };
leetcode 518. 零钱兑换 II
和上一题的不同之处在于,本题要求解的是所有装满背包的组合数目
dp[j]表示装满容量j时的可能的组合数目。 状态转移方程:dp[j]=dp[j]+dp[j-w[i]]
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=coins[i];j<=amount;j++) { dp[j]=dp[j]+dp[j-coins[i]]; } } return dp[amount]; } };
六、多重背包
多重背包可以看作是01背包的扩展,每一件物品最多有固定个,简单的来理解就是内层多加一个循环遍历物品数量
dp[j]表示容量为j时物品最大的价值。 状态转移方程:dp[j]=max(dp[j] , dp[j-w[i]]+v[i] , dp[j-2*w[i]]+2*v[i] , ......,dp[j-k*w[i]]+k*v[i] ) 设有最多k个物品i
for(int i=0;i<n;i++) for(int j=m;j>=v[i];j--) for(int k=1;k<=s&&j-k*w[i]>=0;k++) f[j]=max(f[j],f[j-k*w[i]]+k*v[i]);
多重背包的二进制优化
原理:物品A(价值v重量w)有7个,如果全都列举出来就是{(0*v,0*w),(1*v,1*w),......,(7*v,7*w)},这样再当作01背包来做时间复杂度太高
改进:由于2^0=1,2^1=2,2^2=4 ,1 2 4 可以组成0~7的所有情况,所以只需{(1*v,1*w),(2*v,2*w),(4*v,4*w)}就可以表示所有的可能性
再如10 2^0=1,2^1=2,2^2=4。1 2 4 可以组成0~7的所有情况,10-7=3 ,还需要加入一个3 ,只需{(1*v,1*w),(2*v,2*w),(3*v,3*w),(4*v,4*w)}就可以表示所有的可能性
共有n个物品,背包总重量为m。 物品i 的价值为v,重量为w,最多个数为s struct Good { int v,w };//存价值和重量 int main() { '''''' for(int i=0;i<n;i++) { cin>>v>>w>>s; for(int k=1;k<=s;k*=2) { s-=k; goods.push_back({v*k,w*k}); } if(s>0) goods.push_back(v*s,w*s); } '''''''' 同01背包 }
七、买股票问题
买股票问题可以用一个模型来解决:参考文章
dp[i][k][j]=W表示第i天,剩余可交易次数为k,j=0未持有股票 j=1持有股票
dp[i][k][0]=max( dp[i-1][k][0] , dp[i-1][k][1]+prise[i] ) 当前i天未持有股票 -> 前一天就没有持有 或者 前一天持有今天卖出了
dp[i][k][1]=max( dp[i-1][k][1] , dp[i-1][k-1][0]-prise[i] ) 当前i天持有股票 -> 前一天就持有 或者 前一天未持有今天买入了
k当只能交易一次,或者可以交易无限次时无效直接去掉。
只能交易一次dp[i-1][k-1][0]项无效去掉
leetcode 121. 买卖股票的最佳时机 交易一次
class Solution { public: int maxProfit(vector<int>& prices) { if(prices.size()==0) return 0; vector<vector<int>>dp(prices.size(),vector<int>(2,0)); dp[0][0]=0; dp[0][1]=-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][1],-prices[i]); } return dp[prices.size()-1][0]; } };
leetcode 122. 买卖股票的最佳时机 II 交易无限次
class Solution { public: int maxProfit(vector<int>& prices) { if(prices.size()==0) return 0; 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][1],dp[i-1][0]-prices[i]); } return dp[prices.size()-1][0]; } };
leetcode 123. 买卖股票的最佳时机 III 交易两次
class Solution { public: int maxProfit(vector<int>& prices) { if(prices.size()==0) return 0; vector<vector<vector<int> > >dp=(vector<vector<vector<int> > >(prices.size(),vector<vector<int> >(2+1,vector<int>(2,0)))); dp[0][1][1]=-prices[0]; dp[0][2][1]=-prices[0]; for(int i=1;i<prices.size();i++) { for(int k=2;k>0;k--) { dp[i][k][0]=max(dp[i-1][k][0],dp[i-1][k][1]+prices[i]); dp[i][k][1]=max(dp[i-1][k][1],dp[i-1][k-1][0]-prices[i]); } } return max(dp[prices.size()-1][2][0],dp[prices.size()-1][1][0]); } };
leetcode 188. 买卖股票的最佳时机 IV 最多交易K次
int minl(int a,int b) { return (a>b?b:a); } class Solution { public: int max_Profit_2(vector<int>& prices) { if(prices.size()==0) return 0; 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][1],dp[i-1][0]-prices[i]); } return dp[prices.size()-1][0]; } int maxProfit(int k,vector<int>& prices) { if(prices.size()==0) return 0; if(k>prices.size()/2) return max_Profit_2(prices); vector<vector<vector<int> > >dp=(vector<vector<vector<int> > >(prices.size(),vector<vector<int> >(k+1,vector<int>(2,0)))); for(int kk=k;kk>0;kk--) dp[0][kk][1]=-prices[0]; for(int i=1;i<prices.size();i++) { for(int kk=k;kk>0;kk--) { dp[i][kk][0]=max(dp[i-1][kk][0],dp[i-1][kk][1]+prices[i]); dp[i][kk][1]=max(dp[i-1][kk][1],dp[i-1][kk-1][0]-prices[i]); } } int maxl=0; for(int kk=k;kk>0;kk--) if(dp[prices.size()-1][k][0]>maxl) maxl=dp[prices.size()-1][k][0]; return maxl; } };
leetcode 309. 最佳买卖股票时机含冷冻期 冷冻期换成i-2
class Solution { public: int maxProfit(vector<int>& prices) { if(prices.size()<=1) return 0; vector<vector<int>>dp(prices.size(),vector<int>(2,0)); dp[0][1]=-prices[0]; dp[1][1]=max(dp[0][1],-prices[1]); dp[1][0]=max(0,prices[1]-prices[0]); for(int i=2;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][1],dp[i-2][0]-prices[i]); } return dp[prices.size()-1][0]; } };
leetcode 714. 买卖股票的最佳时机含手续费 给价格减去交易费
class Solution { public: int maxProfit(vector<int>& prices,int fee) { if(prices.size()==0) return 0; 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]-fee); dp[i][1]=max(dp[i-1][1],dp[i-1][0]-prices[i]); } return dp[prices.size()-1][0]; } };
leetcode 887. 鸡蛋掉落
常规思路:dp[K][N]表示在存在K个好鸡蛋的情况下要测试N层楼必须的最小尝试次数,
鸡蛋在dp[K][N]处的下一个状态可能由两个状态组成:max(dp[K][N-i],dp[K-1][i-1]) 在第i层扔过鸡蛋以后没有碎,在第i层扔过鸡蛋以后碎了。i:1->N遍历取最小的一个
#define inf 999999 int minl(int a,int b) { return (a>b?b:a); } class Solution { public: int dpl(vector<vector<int> >&dp,int K, int N) { int lable=inf; if(N==0) return 0; if(K==1) return N; if(dp[K][N]!=0) return dp[K][N]; for(int i=1;i<=N&&K-1>=0;i++) { lable=minl(lable,max(dpl(dp,K-1,i-1),dpl(dp,K,N-i))+1); } dp[K][N]=lable; //cout<<K<<"->"<<N<<"->"<<lable<<" "; return lable; } int superEggDrop(int K, int N) { vector<vector<int> >dp(K+1,vector<int>(N+1,0)); return dpl(dp,K,N); } };
面试题62. 圆圈中最后剩下的数字
从最后状态向前推,最后只有一个数字时,也就是0号位置是安全位置,倒着推到最初状态这个0号位置的所在。
1个人:0
2个人:(0+m)%2
3个人:((0+m)%2 +m)%3
n个人:.......................................%n
class Solution { public: int lastRemaining(int n, int m) { int out=0; for(int i=2;i<=n;i++) { out=(out+m)%i; } return out; } };
leetcode 32. 最长有效括号
本题需要找好dp的定义。
dp[i]表示以s[i]为结尾的串有效长度,如图:
class Solution { public: int longestValidParentheses(string s) { if(s.size()<=1) return 0; vector<int>dp(s.size(),0); int maxl=0; if(s[0]=='('&&s[1]==')') dp[1]=2; for(int i=2;i<s.size();i++) { if(s[i]=='('){}//A else { if(s[i-1]=='(')//B { if(i-2<0)//D { dp[i]=2; } else//E { dp[i]=dp[i-2]+2; } } else if(s[i-1]==')')//C { if(i-1-dp[i-1]<0 || s[i-1-dp[i-1]]==')'){}//F else if(s[i-1-dp[i-1]]=='(')//G { if(i-2-dp[i-1]<0)//H { dp[i]=dp[i-1]+2; } else//I { dp[i]=dp[i-1]+2+dp[i-2-dp[i-1]]; } } } } } for(int i=0;i<s.size();i++) maxl=(dp[i]>maxl?dp[i]:maxl); return maxl; } };