文章目录
概述
线性动态规划,是较常见的一类动态规划问题,其是在线性结构上进行状态转移,这类问题不像背包问题、区间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(x−1),也可以为f(x−2),那么f(x)=f(x−1)+f(x−2),很容易就推出状态转移方程
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只跟x−1和x−2有关,因此我们只需要考虑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(x−1)的值赋值为f(x−2),f(x)的值赋值给f(x−1),到此压缩完毕
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[i−j](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是集合里面的元素
这里的2和3代表j,j是集合里面的元素
显然只有集合里的
j
j
j小于
i
i
i,
d
p
[
i
]
dp[i]
dp[i]就能加上
d
p
[
i
−
j
]
dp[i-j]
dp[i−j]的值
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大于等于zero和one时,加上他们的个数,即dp[i]+=dp[i−zero],dp[i]+=dp[i−one]
最后记得取模即可
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[i−1],dp[i−2]+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(x−1)和f(x−2)有关,因此我们一样可以进行状态压缩
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[i−1],dp[i−2]+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[i−1]和dp[i−2],因此我们只能返回dp[i−3]
这时我们可以考虑用双指针来维护新数组,
指针
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)x∗y)(x∗y代表数值∗数量)
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[i−1]+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);
}
};