本篇博客整理了LeetCode hot100和剑指offer里面的动态规划的题目,做一个总结。
首先明确两个概念:
1.子数组/子串 一定是数组/字符串中下标连续的部分
2.子序列 下标只要满足是递增的即可,不需要连续
LeetCode53.最大子数组的和
思路:若f(i)表示以nums[i]结尾的子数组中的最大和,则有 f(i+1)=max(f(i)+nums[i+1],nums[i+1]);
int maxSubArray(vector<int>& nums) {
int tempans=nums[0];//保存的是以nums[i]结尾的最长子数组中最大和
int ans=nums[0];//保存的是tempans的最大值
for(int i=1;i<nums.size();i++){
tempans=max(tempans+nums[i],nums[i]);
ans=max(ans,tempans);
}
return ans;
}
若是数组是环形的情况下,求子数组的最大和?
Leetcode918.环形子数组的最大和
思路非常巧妙:
分两种情况考虑:
(1)不超过边界的子数组的最大和
(2)超过边界的子数组的最大和,用整个数组的和减去最小数组和中最小的值,即为跨越边界的子数组最大和。
int maxSubarraySumCircular(vector<int>& nums) {
int tempans1=nums[0],tempans2=nums[0],ans1=nums[0],ans2=nums[0],sum=nums[0];
for(int i=1;i<nums.size();i++){
tempans1=max(tempans1+nums[i],nums[i]);
ans1=max(ans1,tempans1);
tempans2=min(tempans2+nums[i],nums[i]);
ans2=min(ans2,tempans2);
sum+=nums[i];
}
return ans2==sum?ans1:max(ans1,sum-ans2);
}
LeetCode152.乘积最大子数组
思路:若max(i)表示以nums[i]结尾的子数组中的最大乘积,min(i)表示以i结尾的子数组中的最小乘积,考虑到乘积符号不同,正负相乘结果是负数,则有:max(i+1)=max(max(i)*nums[i+1],min(i)*nums[i+1],nums[i+1])
int maxProduct(vector<int>& nums) {
int tempmax=nums[0];
int tempmin=nums[0];
int ansmax=nums[0];
for(int i=1;i<nums.size();i++){
int a=tempmax;
int b=tempmin;
tempmax=max3(a*nums[i],max(b*nums[i],nums[i]));
tempmin=min3(a*nums[i],min(b*nums[i],nums[i]));
ansmax=max(ansmax,tempmax);
}
return ansmax;
}
LeetCode300.最长递增子序列
思路:若数组ans中的ans[i]表示以nums[i]结尾的最长递增子序列的长度,则有ans[i+1]=max(ans[j])+1,其中j
int lengthOfLIS(vector<int>& nums) {
vector<int>ans(nums.size(),1);
int res=1;
for(int i=1;i<nums.size();i++){
int j=i-1;
while(j>=0){
if(nums[j]<nums[i]){
ans[i]=max(ans[j]+1,ans[i]);
}
j=j-1;
}
res=max(res,ans[i]);}
return res;
}
上面的思路简洁易于理解,时间复杂度为o(n^2),空间复杂度为o(n),但是还有一种时间复杂度为o(nlogn)更佳的思路。
二分法:
1.维护一个tail[n]数组,一开始为空,舒适化tail[0]=nums[0]
2.从i=1开始遍历nums[i],
如果nums[i]>tail数组的最后一个元素,则tail末尾加上nums[i]
否则找出tial[i]中第一个比nums[i]小的元素tail[k],并将tail[k+1]更新为nums[i]
3,最后tail数组就是一个最长的递增子序列,它的长度就是答案
二维数组dp[i][j]表示以s1[i-1]结尾和以s2[j-1]的子串的最长公共子串的长度,则有:
dp[i][j] = dp[i-1][j-1]+1 若s1[i]=s2[j]
dp[i][j] = 0 若s1[i]!=s2[j]
结果就是dp[i][j]的最大值。
int longestcommonstring(string s1,string s2){
int size1=s1.size();
int size2=s2.size();
vector<vector<int> >dp(size1+1,vector<int>(size2+1,0));
int ans=0;
for(int i=1;i<=size1;i++){
for(int j=1;j<=size2;j++){
if(s1[i-1]==s2[j-1])dp[i][j]=dp[i-1][j-1]+1;
else dp[i][j]=0;
ans=max(ans,dp[i][j]);
}
}
return ans;
}
Offer2.95.最长公共子序列
思路和上一题很像,只不过状态转移方程需要改变一下。
dp[i][j] = dp[i-1][j-1]+1 若s1[i]=s2[j]
dp[i][j] = max(dp[i-1][j]+dp[i][j-1]) 若s1[i]!=s2[j]
int longestcommonsequence(string s1,string s2){
int size1=s1.size();
int size2=s2.size();
vector<vector<int> >dp(size1+1,vector<int>(size2+1,0));
int ans=0;
for(int i=1;i<=size1;i++){
for(int j=1;j<=size2;j++){
if(s1[i-1]==s2[j-1])dp[i][j]=dp[i-1][j-1]+1;
else dp[i][j]=max(dp[i-1][j],dp[i][j-1]);
ans=max(ans,dp[i][j]);
}
}
return ans;
}
LeetCode5.最长回文子串
方法1:动态规划
若dp[i][j]表示s[i]–s[j]的子串是否为回文子串
dp[i][j]=dp[i+1][j-1]&&s[i]==s[j];
string longestPalindrome(string s) {
vector<vector<int>>dp(s.size(),vector<int>(s.size(),1));//dp[i][j]表示s[i]--s[j]是否为回文子串
int l=0,r=0;
for(int size=1;size<s.size();size++){
for(int i=0;i+size<s.size();i++){
int j=i+size;
if(size==1)dp[i][j]=s[i]==s[j]?1:0;
else dp[i][j]=(s[i]==s[j])*dp[i+1][j-1];
if(dp[i][j]==1){
l=i;
r=j;
}
}
}
return s.substr(l,r-l+1);
}
方法2:中心扩展法
分为两种情况,一种是奇数个字符的子串,另一个是偶数个字符的子串,比较最终的最长回文子串
string longestPalindrome(string s) {
int m=1;//当一个字符为中间时
int maxhalf1=0,ansm=0;
for(int m=0;m<s.size();m++){
int half=0;
while(m-half>=0&&m+half<s.size()){
if(s[m-half]==s[m+half])half++;
else break;
}
if(half>maxhalf1){
maxhalf1=half;
ansm=m;
}
}
string s1=s.substr(ansm-maxhalf1+1,2*maxhalf1-1);
int l=0,maxhalf2=0,ansl=0;//以两个字符为中间时
for(int l=0;l+1<s.size();l++){
int half=0;
while(l-half>=0&&l+1+half<s.size()){
if(s[l-half]==s[l+1+half])half++;
else break;
}
if(half>maxhalf2){
maxhalf2=half;
ansl=l;
}
}
string s2=s.substr(ansl-maxhalf2+1,2*maxhalf2);
return s1.size()>s2.size()?s1:s2;
}
方法3:Manacher 算法
待补充。
这种问题的实质还是对给定数组的处理。
LeetCode121.买卖股票(1次交易)
思路:若minp表示表示前i-1天的股票最低价格,则当第i天抛售股票时,获得的最大利润为prices[i]-minp;
int maxProfit(vector<int>& prices) {
int minp=prices[0];//minp表示前i-1天股票最低价格
int ans=0;
for(int i=1;i<prices.size();i++){
if(prices[i-1]<minp)minp=prices[i-1];
ans=max(ans,prices[i]-minp);
}
return ans;
}
LeetCode122.买卖股票(多次交易)
思路:尽可能多得进行股票交易,则有最大利润为所有递增子数组产生的利润之和。
int maxProfit(vector<int>& prices) {
int ans=0;
for(int i=0;i+1<prices.size();i++){
if(prices[i]<prices[i+1])ans+=prices[i+1]-prices[i];
}
return ans;
}
LeetCode123.买卖股票(最多2次交易)
思路:这个可以和上一题区分起来,上一题可以理解为所有的递增子数组产生的利润之和;
int maxProfit(vector<int>& prices) {
int n = prices.size();
int buy1 = -prices[0], sell1 = 0;
int buy2 = -prices[0], sell2 = 0;
for (int i = 1; i < n; ++i) {
buy1 = max(buy1, -prices[i]);
sell1 = max(sell1, buy1 + prices[i]);
buy2 = max(buy2, sell1 - prices[i]);
sell2 = max(sell2, buy2 + prices[i]);
}
return sell2;
}
LeetCode188.买卖股票(最多k次交易)
LeetCode309.买卖股票(多次交易且含冷冻期)
思路:
我们目前持有一支股票,对应的「累计最大收益」记为 f[i][0];我们目前不持有任何股票,并且处于冷冻期中,对应的「累计最大收益」记为 f[i][1];我们目前不持有任何股票,并且不处于冷冻期中,对应的「累计最大收益」记为 f[i][2]。
LeetCode198.打家劫舍_线
思路:若dp[i]表示打劫前i家获得的最大金额,则有dp[i]=max(dp[i-1],dp[i-2]+nums[i]);
int rob(vector<int>& nums) {
vector<int>dp(nums.size(),0);
if(nums.size()==1)return nums[0];
dp[0]=nums[0];
dp[1]=max(nums[1],nums[0]);
for(int i=2;i<nums.size();i++){
dp[i]=max(dp[i-1],dp[i-2]+nums[i]);
}
return dp.back();
}
代码可以用滚动数组优化:
int rob(vector<int>& nums) {
if(nums.size()==1)return nums[0];
int a=nums[0];
int b=max(nums[1],nums[0]);
if(nums.size()==2)return b;
int c=max(b,a+nums[2]);
if(nums.size()==2)return c;
for(int i=3;i<nums.size();i++){
a=b;
b=c;
c=max(a+nums[i],b);
}
return c;
}
LeetCode213.打家劫舍_圈
去第一家不去最后一家:dp[0] dp[i-2]
不去第一家去最后一家:dp[1] dp[i-1]
两者取较大值
滚动数组优化
int rob(vector<int>& nums) {
if(nums.size()==1)return nums[0];
if(nums.size()==2)return max(nums[0],nums[1]);
if(nums.size()==3)return max(nums[0],max(nums[1],nums[2]));
int a1=nums[0];
int b1=max(nums[0],nums[1]);
int a2=nums[1];
int b2=max(nums[1],nums[2]);
int c1=0;
int c2=0;
for(int i=2;i+1<nums.size();i++){
c1=max(a1+nums[i],b1);
a1=b1;
b1=c1;
c2=max(a2+nums[i+1],b2);
a2=b2;
b2=c2;
}
return max(c2,c1);
}
LeetCode337.打家劫舍3
70.爬楼梯问题
题目1:每次只能爬1级台阶或者2级台阶
思路:f(n)表示爬n级台阶的方案数,则最后一次可以爬2级或者1级,有:f(n)=f(n-1)+f(n-2),f(1)=1,f(2)=2;
按照递归的思路,代码如下:
int f(int n){
if(n<=0)return 0;
if(n<=2)return n;
return f(n-1)+f(n-2);
}
上述代码的缺点在于,当n很大的时候,递归的深度太深了,代码的空间复杂度太高了,可以用滚动数组的方式进行如下方式的优化。
int f(int n){
if(n<=0)return 0;
if(n==1)return 1;
int a=1;
int b=2;
int c;
for(int i=1;i<n;i++){
c=a+b;
a=b;
b=c;
}
return a;
}
还可以用深度优先搜索:
有:f(n)=f(n-1)+f(n-2)+f(n-3);
int f(int n){
if(n<=0)return 0;
if(n=1) return 1;
int a=1;
int b=2;
int c=4;
int d;
for(int i=0;i<n-1;i++){
d=a+b+c;
a=b;
b=c;
c=d;
}
return a;
}
f(n)=2^(n-1)
这个可以通过数学归纳法归纳下,也可以通过找规律的方法得出
满足要求有两种情况:
1.最后一次走一步 ;
2.最后一次走两步,倒数第二次只能走一步;
因此有:f(n)=f(n-1)+f(n-3)
利用滚动数组优化后有:
int solution(int m){
if(m<=4)return m;
int a=1;
int b=2;
int c=3;
int d=4;
for(int i=0;i<m-3;i++){
d=c+a;
a=b;
b=c;
c=d;
}
return d;
}
如果用深度优先搜索的话
void dfs(int m,int tempsum,int& ans,int laststep){
if(tempsum==m){
ans++;
return;
}
if(tempsum>m)return;
if(laststep==2) dfs(m,tempsum+1,ans,1);
if(laststep==1){
dfs(m,tempsum+2,ans,2);
dfs(m,tempsum+1,ans,1);
}
}
int solution(int m){
int laststep=1;
int tempsum=laststep;
int ans=0;
dfs(m,tempsum,ans,laststep);
laststep=2;
tempsum=laststep;
dfs(m,tempsum,ans,laststep);
return ans;
}
01背包问题最为常见,已知物品i的重量为w[i],价值为v[i],每件物品均只有一件,背包的最大承重为wmax,求背包能装物品的最大价值。
二维数组dp[i][j]表示前i件物品在背包承重为j的情况下所能装的最大价值。则有 dp[i+1][j]=max(dp[i][j]+v[i+1] , v[i+1]);
LeetCode62.不同路径
int uniquePaths(int m, int n){
vector<vector<int>>dp(m,vector<int>(n,1));
for(int i=1;i<m;i++){
for(int j=1;j<n;j++){
dp[i][j]=dp[i-1][j]+dp[i][j-1];
}
}
return dp[m-1][n-1];
}
LeetCode64.最小路径和
int minPathSum(vector<vector<int>>& grid) {
int len1=grid.size();
int len2=grid[0].size();
vector<vector<int>>dp(len1,vector<int>(len2,0));
dp[0][0]=grid[0][0];
for(int i=1;i<len1;i++)dp[i][0]=dp[i-1][0]+grid[i][0];//初始化第0列
for(int i=1;i<len2;i++)dp[0][i]=dp[0][i-1]+grid[0][i];//初始化第0行
for(int i=1;i<len1;i++){
for(int j=1;j<len2;j++){
int a=dp[i-1][j];
int b=dp[i][j-1];
int c=a<b?a:b;
dp[i][j]=c+grid[i][j];
}
}
return dp[len1-1][len2-1];
}
LeetCode279.完全平方数
思路:假设m是满足m * m<=n的最大值,则有f(n)=maxf(n-i*i) +1 ,其中0 以f(12)为例,n=12,m=3,f(12)=min(f(11),f(8),f(3)) + 1,只需维护一个数组保存f(n)即可。
int numSquares(int n) {
vector<int>dp(n+1,0);//dp[i]表i拆成完全平方数的最小个数
for(int i=1;i<=n;i++){
dp[i]=INT_MAX;
for(int j=1;j*j<=i;j++){
dp[i]=min(dp[i],dp[i-j*j]+1);
}
}
return dp[n];
}
除此之外,还有一个数学的方法四平方和定理,它证明了任意一个正整数都可以被表示为至多四个正整数的平方和且当且仅当n≠4^k(8m+7)时,n可以被表示为至多三个正整数的平方和。
Offer14.剪绳子
这一题和上一题的思路很像,假设ans[i]表示i拆成任意个数的最大积,则有ans[i]=max(ans[i-j] * j , (i-j) *j );
值得注意的是为什么还要和 (i-j) *j比较,因为 4=2 + 2 ,但是f(4)≠2 * f(2), 题目的任意个数的含义是大于1的,需考虑部件只有一个乘数的情况。
int cuttingRope(int n) {
vector<int>ans(n+1,1);
for(int i=2;i<=n;i++){
ans[i]=INT_MIN;
for(int j=1;j<i;j++){
ans[i]=max(ans[i],max(ans[i-j]*j,j*(i-j)));
}
}
return ans[n];
}
这是一个典型的零钱兑换问题。
先以一个比较简单的情况切入,若硬币有1、2、5元这三种,找零总钱数为n,求最少的硬币数。
设dp[n]为找零总数,则dp[n]=min(dp[n-1],dp[n-2],dp[n-5])+1;
322.零钱兑换
int coinChange(vector<int>& coins, int amount) {
vector<int>dp(amount+1,0);
for(int i=1;i<=amount;i++){
int tempmin=INT_MAX;
for(int j=0;j<coins.size();j++){
if(i-coins[j]>=0&&dp[i-coins[j]]!=-1)tempmin=min(tempmin,dp[i-coins[j]]);
}
dp[i]=(tempmin==INT_MAX)?-1:tempmin+1;
}
return dp[amount];
}
Offer49.丑数
方法1:动态规划
int nthUglyNumber(int n) {
if(n<7)return n;
vector<int>dp(n+1,1);
for(int i=1;i<7;i++)dp[i]=i;
int a=1;
int b=1;
int c=1;
for(int i=7;i<=n;i++){
while(2*dp[a]<=dp[i-1])a++;
while(3*dp[b]<=dp[i-1])b++;
while(5*dp[c]<=dp[i-1])c++;
dp[i]=min(2*dp[a],min(3*dp[b],5*dp[c]));
}
return dp[n];
}
方法2:最小堆
关于最小堆的详细介绍可以看我的另外一篇博客。
LeetCode338.二进制中1的个数
涉及到了位运算的一些知识:
n &(n-1)作用是消除数字 n 的二进制表示中的最后一个 1
vector<int> countBits(int n) {
vector<int> bits(n + 1);
for (int i = 1; i <= n; i++) {
bits[i] = bits[i & (i - 1)] + 1;
}
return bits;
}
Offer60.n个筛子的点数概率
把n个骰子扔在地上,所有骰子朝上一面的点数之和为s。输入n,打印出s的所有可能的值出现的概率。
你需要用一个浮点数数组返回答案,其中第 i 个元素代表这 n 个骰子所能掷出的点数集合中第 i 小的那个的概率。