动态规划之——线性DP(入门篇)

概述

线性动态规划,是较常见的一类动态规划问题,其是在线性结构上进行状态转移,这类问题不像背包问题、区间DP等有固定的模板。
线性动态规划的目标函数为特定变量的线性函数,约束是这些变量的线性不等式或等式,目的是求目标函数的最大值或最小值。

例题(leetcode)

70.爬楼梯

点击这里

思路

线性dp入门题目, 假设当前状态为 x ,那么 x 的阶层数可以为 f ( x − 1 ) ,也可以为 f ( x − 2 ) , 那么 f ( x ) = f ( x − 1 ) + f ( x − 2 ) 假设当前状态为x,那么x的阶层数可以为f(x-1),也可以为f(x-2),那么f(x)=f(x-1)+f(x-2) 假设当前状态为x,那么x的阶层数可以为f(x1),也可以为f(x2),那么f(x)=f(x1)+f(x2),很容易就推出状态转移方程

code1

class Solution {
public:
    int climbStairs(int n) {
     int dp[50];
     dp[1]=1,dp[2]=2;
     for(int i=3;i<=n;++i){
     	dp[i]=dp[i-2]+dp[i-1]; 
	 }
	 return dp[n];
    }
};

我们通过观察可以发现, x 只跟 x − 1 和 x − 2 有关 x只跟x-1和x-2有关 x只跟x1x2有关,因此我们只需要考虑3个数就可以了,将dp进行状态压缩,转换成 f = f 1 + f 2 , f 2 = f 1 , f 1 = f f=f1+f2,f2=f1,f1=f f=f1+f2,f2=f1,f1=f
f ( x − 1 ) 的值赋值为 f ( x − 2 ) , f ( x ) 的值赋值给 f ( x − 1 ) f(x-1)的值赋值为f(x-2),f(x)的值赋值给f(x-1) f(x1)的值赋值为f(x2),f(x)的值赋值给f(x1),到此压缩完毕

code2

class Solution {
public:
    int climbStairs(int n) {
     int f1=1,f2=2;
     if(n==1) return 1;
     else if(n==2) return 2;
     for(int i=3;i<=n;++i){
     	int f=f1+f2;
        f1=f2,f2=f;
	 }
	 return f2;
    }
};

进阶1 377. 组合总和 Ⅳ

点击这里

思路

跟爬楼梯的套路一样, d p [ i ] = d p [ i − j ] ( j < = i ) dp[i]=dp[i-j](j<=i) dp[i]=dp[ij](j<=i)
怎么来的呢, 首先我们令 x 等于 4 , 那么 d p [ 4 ] 可以由 d p [ 1 ] + 3 由来, d p [ 4 ] = d p [ 2 ] + 2 由来 首先我们令x等于4,那么dp[4]可以由dp[1]+3由来,dp[4]=dp[2]+2由来 首先我们令x等于4,那么dp[4]可以由dp[1]+3由来,dp[4]=dp[2]+2由来
这里的 2 和 3 代表 j , j 是集合里面的元素 这里的2和3代表j,j是集合里面的元素 这里的23代表jj是集合里面的元素
显然只有集合里的 j j j小于 i i i, d p [ i ] dp[i] dp[i]就能加上 d p [ i − j ] dp[i-j] dp[ij]的值

code

class Solution {
public:
    int combinationSum4(vector<int>& nums, int target) {
      unsigned long long dp[1010];
      dp[0]=1;
      for(int i=1;i<=target;++i){
        for(auto j : nums){
            if(i>=j){
                dp[i]+=dp[i-j];
            }
        }
      }
      return dp[target];
    }
};

进阶2 2266. 统计打字方案数

点击这里

思路

和上题思路一样,当 i 大于等于 z e r o 和 o n e 时,加上他们的个数,即 d p [ i ] + = d p [ i − z e r o ] , d p [ i ] + = d p [ i − o n e ] i大于等于zero和one时,加上他们的个数,即dp[i]+=dp[i-zero],dp[i]+=dp[i-one] i大于等于zeroone时,加上他们的个数,即dp[i]+=dp[izero]dp[i]+=dp[ione]
最后记得取模即可

code

class Solution {
public:
    int dp[100010];
    int countGoodStrings(int low, int high, int zero, int one) {
        int mod=1e9+7; 
        dp[0]=1;
        int ans=0;
        for(int i=1;i<=high;++i){
           if(i>=zero) dp[i]=(dp[i]+dp[i-zero])%mod;
           if(i>=one) dp[i]=(dp[i]+dp[i-one])%mod;
           if(i>=low) ans=(ans+dp[i])%mod;
        }
        return ans;
    }
};

198. 打家劫舍

点击这里

思路

我们只会有两种选择,偷和不偷,不偷的话返回上一个状态,偷的话返回上上个状态,其他情况都不是最优
因此,它的状态转移方程为 d p [ i ] = m a x ( d p [ i − 1 ] , d p [ i − 2 ] + a [ i ] ) dp[i]=max(dp[i-1],dp[i-2]+a[i]) dp[i]=max(dp[i1],dp[i2]+a[i])

code1

class Solution {
public:
    int rob(vector<int>& nums) {
      int n=nums.size();
      int dp[105]={0};
      for(int i=0;i<n;++i){
        dp[i+2]=max(dp[i+1],dp[i]+nums[i]);//防止数组越界
      }
      return dp[n+1];
    }
};

与爬楼梯同理, f ( x ) 只跟 f ( x − 1 ) 和 f ( x − 2 ) 有关,因此我们一样可以进行状态压缩 f(x)只跟f(x-1)和f(x-2)有关,因此我们一样可以进行状态压缩 f(x)只跟f(x1)f(x2)有关,因此我们一样可以进行状态压缩

code2

class Solution {
public:
    int rob(vector<int>& nums) {
     int f1=0,f2=0;
     for(int i=0;i<nums.size();++i){
        int f=max(f1,f2+nums[i]);
        f2=f1,f1=f;
     }
     return f1;
    }
};

进阶1 2320. 统计放置房子的方式数

点击这里

思路

由于道路两侧的情况不影响,因此我们只需要考虑一侧的情况,另一侧的情况与之相同,最后进行相乘即可
考虑不放房子的情况, d p [ 0 ] = 1 , 放一个房子的情况 d p [ 1 ] = 2 , 接着就与上题一样了 考虑不放房子的情况,dp[0]=1,放一个房子的情况dp[1]=2,接着就与上题一样了 考虑不放房子的情况,dp[0]=1,放一个房子的情况dp[1]=2,接着就与上题一样了
d p [ i ] = m a x ( d p [ i − 1 ] , d p [ i − 2 ] + a [ i ] ) , 同样我们可以进行状态压缩 dp[i]=max(dp[i-1],dp[i-2]+a[i]),同样我们可以进行状态压缩 dp[i]=max(dp[i1],dp[i2]+a[i]),同样我们可以进行状态压缩
最后答案乘上另一侧的情况即可

code

class Solution {
public:
    int countHousePlacements(int n) {
        long long f2=1,f1=2;
        int mod=1e9+7;
        for(int i=2;i<=n;++i){
         int f=(f1+f2)%mod;
         f2=f1,f1=f;
        }
        return (f1*f1)%mod;
    }
};

进阶2 3186. 施咒的最大总伤害

点击这里

思路

这题是一道综合题,首先我们需要统计数列里不同数的个数,用哈希表来存
接着将它存入到数组里进行升序排序,由于 d p [ i ] 不能返回 d p [ i − 1 ] 和 d p [ i − 2 ] ,因此我们只能返回 d p [ i − 3 ] dp[i]不能返回dp[i-1]和dp[i-2],因此我们只能返回dp[i-3] dp[i]不能返回dp[i1]dp[i2],因此我们只能返回dp[i3]
这时我们可以考虑用双指针来维护新数组, 指针 j 必须满足 a [ j ] > = a [ i ] − 2 指针j必须满足a[j]>=a[i]-2 指针j必须满足a[j]>=a[i]2
那么状态转移方程就为 d p [ i + 1 ] = m a x ( d p [ i ] , d p [ j ] + ( l o n g l o n g ) x ∗ y ) ( x ∗ y 代表数值 ∗ 数量 ) 那么状态转移方程就为dp[i+1]=max(dp[i],dp[j]+(long long)x * y)(x*y代表数值*数量) 那么状态转移方程就为dp[i+1]=max(dp[i],dp[j]+(longlong)xy)(xy代表数值数量)

code

class Solution {
public:
    long long maximumTotalDamage(vector<int>& power) {
        unordered_map<int,int> m;
        for(auto i : power){
            m[i]++;
        }
        vector<pair<int,int>> a(m.begin(),m.end());
        sort(a.begin(),a.end());
        int n=a.size();
        vector<long long> dp(n+1);
        for(int i=0,j=0;i<n;++i){
            int x=a[i].first,y=a[i].second;
            while(a[j].first<x-2) j++;
            dp[i+1]=max(dp[i],dp[j]+(long long)x * y);
        }
        return dp[n];
    }
};

53. 最大子数组和

点击这里

思路

假设x为序列中的一个数,那么x需要考虑2种情况:

  • 加上前面的序列
  • 不加上前面的序列,序列更新为x
    因此,我们就可以得出状态转移方程为 d p [ i ] = m a x ( d p [ i − 1 ] + n u m s [ i ] , n u m s [ i ] ) ; dp[i]=max(dp[i-1]+nums[i],nums[i]); dp[i]=max(dp[i1]+nums[i],nums[i]);

code1

class Solution {
public:
    int maxSubArray(vector<int>& nums) {
     int n=nums.size();
     vector<int> dp(n+1);
     int maxn=-1e9;
     for(int i=0;i<nums.size();++i){
        dp[i+1]=max(dp[i]+nums[i],nums[i]);//防止数组越界
        maxn=max(dp[i+1],maxn);
     }
     return maxn;
    }
};

同样这题也可以进行状态压缩,我们每次只需要考虑2个数:

  • 前面整段序列的值
  • 当前数组的值

因此,我们可以用一个整数来模拟整段序列的值

code2

class Solution {
public:
    int maxSubArray(vector<int>& nums) {
     int b=nums[0];
     int maxn=b;
     for(int i=1;i<nums.size();++i){
        if(nums[i]+b>nums[i]) b=nums[i]+b;
        else b=nums[i];
        maxn=max(maxn,b);
     }
     return maxn;
    }
};

进阶1 1749. 任意子数组和的绝对值的最大值

点击这里

思路

开2个dp数组,一个存正值,一个存负值,它的状态转移方程为 m a x n = m a x ( d p 1 [ i + 1 ] , a b s ( d p 2 [ i + 1 ] ) , m a x n ) maxn=max({dp1[i+1],abs(dp2[i+1]),maxn}) maxn=max(dp1[i+1],abs(dp2[i+1]),maxn)

code

class Solution {
public:
    int maxAbsoluteSum(vector<int>& nums) {
      int n=nums.size();
      vector<int> dp(n+1);
      vector<int> v(n+1);
      int maxn=0;
      for(int i=0;i<n;++i){
          dp[i+1]=max(dp[i],0)+nums[i];
          v[i+1]=min(v[i],0)+nums[i];
          maxn=max({dp[i+1],abs(v[i+1]),maxn});
      }
      return maxn;
    }
};

进阶2 1191. K 次串联后最大子数组之和

点击这里

思路

这题需要分3种情况:

  • 当k=1时,与 53. 最大子数组和 53. 最大子数组和 53.最大子数组和 的状态转移方程是一致的
  • k>=2时,若数组总和相加<=0,那么我们只需要2倍数组长度的状态转移方程(在往后下去最大值不变)
  • 若数组总和相加大于0,那可以看成再第一段结尾第二段开头插入k-2个正数(也是进行状态转移,然后在该基础上相加数组的总和个数减去2乘以总和的值)
    为什么可以这么看呢,因为我们找出前2倍数组长度的最大值的基础上,在这后面我们可以看成(k-2)次循环,因此我们总和就加上(k-2)乘上总值即可

code

class Solution {
public:
    int kConcatenationMaxSum(vector<int>& arr, int k) {
      int mod=1e9+7;
      int maxn=0;
      if(k==1){
        int dp=0;
        for(auto i : arr){
         dp=max(dp,0)+i;
         maxn=max(maxn,dp);
        }
        return maxn;
      }
      else{
        int n=arr.size();
        int dp=0;
        for(int i=0;i<2*n;++i){
          dp=max(dp,0)+arr[i%n];
          maxn=max(maxn,dp);
        }
        long long sum=0;
        for(auto i : arr) sum+=i;
        if(sum>0){
            maxn=(maxn+(k-2)*sum)%mod;
        }
        return maxn;
      }
    }
};

进阶3 918. 环形子数组的最大和

点击这里

思路

这题考虑2种情况:

  • 不考虑环形,那么和前几题的状态转移方程是一样的
  • 考虑环形,我们可以求数组中最小数组的和,那么可能的最大值为数组总和减去最小数组的和
  • 将这两者进行大小比较
    单单考虑这还不够,若最小数组的和=数组总和,那么我们还是考虑情况1(即不考虑环形)

code

class Solution {
public:
    int maxSubarraySumCircular(vector<int>& nums) {
     int n=nums.size();
     int maxn=-1e9;
     int dp=0,f=0;
     int minn=1e9;
     int sum=0;
     for(auto i : nums){
       sum+=i;
       dp=max(dp,0)+i;
       maxn=max(maxn,dp);

       f=min(f,0)+i;
       minn=min(minn,f);
     }
     if(sum==minn){
        return maxn;
     }
     else return max(maxn,sum-minn);
    }
};
  • 19
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值