DP定义:
动态规划是分治思想的延伸,通俗一点来说就是大事化小,小事化无的艺术
在将大问题化解为小问题的分治过程中,保存对着些小问题已经处理好的结果,并供后面处理更大规模的问题时直接使用这些结果
动态规划具备了以下三个特点
1.把原来的问题分解成了几个相似的子问题
2.所有的子问题都只需解决一次
3.存储子问题的解
动态规划的本质,是对问题状态的定义和状态转移方程的定义(状态以及状态之间的递推关系)
动态规划问题一般从以下四个角度考虑:
1.状态定义
2.状态间的转移方程定义
3.状态的初始化
4.返回结果
状态定义的要求:定义的状态一定要形成递推关系
适用场景:最大值/最小值 ,可不可行, 是不是,方案个数
下面根据一些例题来解释线性DP的使用:
Frog 1
问题陈述
有 N N N块石头,编号为 1 , 2 , … , N 1, 2, \ldots, N 1,2,…,N。每块 i i i( 1 ≤ i ≤ N 1 \leq i \leq N 1≤i≤N),石头 i i i的高度为 h i h_i hi。
有一只青蛙,它最初在石块 1 1 1 上。它会重复下面的动作若干次以到达石块 N N N:
- 如果青蛙目前在石块 i i i上,则跳到石块 i + 1 i + 1 i+1或石块 i + 2 i + 2 i+2上。这里需要付出 ∣ h i − h j ∣ |h_i - h_j| ∣hi−hj∣的代价,其中 j j j是要降落的石块。
求青蛙到达石块 N N N之前可能产生的最小总成本。
限制因素
- 所有输入值均为整数。
- 2 ≤ N ≤ 1 0 5 2 \leq N \leq 10^5 2≤N≤105
- 1 ≤ h i ≤ 1 0 4 1 \leq h_i \leq 10^4 1≤hi≤104
样例
4
10 30 40 20
30
可以先考虑状态,青蛙每到达第 i i i 块石头的成本由第 i − 1 i-1 i−1 块石头和第 i − 2 i-2 i−2 块石头的状态决定(第一块和第二块除外)
所以这里可以先初始化第一块和第二块: d p [ 1 ] = 0 , d p [ 2 ] = a b s ( a [ 2 ] − a [ 1 ] ) dp[1]=0,dp[2]=abs(a[2]-a[1]) dp[1]=0,dp[2]=abs(a[2]−a[1])
第 2 2 2 块以后的状态可以由前面推导而来: d p [ i ] = m i n ( a b s ( a [ i ] − a [ i − 1 ] ) + d p [ i − 1 ] , a b s ( a [ i ] − a [ i − 2 ] ) + d p [ i − 2 ] ) dp[i]=min(abs(a[i]-a[i-1])+dp[i-1],abs(a[i]-a[i-2])+dp[i-2]) dp[i]=min(abs(a[i]−a[i−1])+dp[i−1],abs(a[i]−a[i−2])+dp[i−2])
以下为AC代码:
#include<bits/stdc++.h>
using namespace std;
int a[200100],dp[200100];
int main(){
int n,sum=0;
cin>>n;
for(int i=1;i<=n;i++){
cin>>a[i];
}
dp[1]=0;
dp[2]=abs(a[2]-a[1]);
for(int i=3;i<=n;i++){
dp[i]=min(abs(a[i]-a[i-1])+dp[i-1],abs(a[i]-a[i-2])+dp[i-2]);
}
cout<<dp[n]<<endl;
}
Frog 2
问题陈述
有 N N N块石头,编号为 1 , 2 , … , N 1, 2, \ldots, N 1,2,…,N。每块 i i i( 1 ≤ i ≤ N 1 \leq i \leq N 1≤i≤N),石头 i i i的高度为 h i h_i hi。
有一只青蛙,它最初在石块 1 1 1 上。它会重复下面的动作若干次以到达石块 N N N:
- 如果青蛙目前在石块 i i i上,请跳到以下其中一个位置:石块 i + 1 , i + 2 , … , i + K i + 1, i + 2, \ldots, i + K i+1,i+2,…,i+K。这里会产生 ∣ h i − h j ∣ |h_i - h_j| ∣hi−hj∣的代价,其中 j j j是要降落的石头。
求青蛙到达石块 N N N之前可能产生的最小总成本。
限制因素
- 所有输入值均为整数。
- 2 ≤ N ≤ 1 0 5 2 \leq N \leq 10^5 2≤N≤105
- 1 ≤ K ≤ 100 1 \leq K \leq 100 1≤K≤100
- 1 ≤ h i ≤ 1 0 4 1 \leq h_i \leq 10^4 1≤hi≤104
样例
5 3
10 30 40 50 20
30
该题目为上面一道题目的升级版,跳跃距离变化为 i + 1 i+1 i+1 到 i + K i+K i+K,因此本题的状态应该是:第 i i i 块的成本为第 i − K i-K i−K 块到第 i i i 块的距离加上到达第 i − K i-K i−K 块的成本
预处理前 K + 1 K+1 K+1 项的成本: d p [ i ] = a b s ( a [ i ] − a [ 1 ] ) ; dp[i]=abs(a[i]-a[1]); dp[i]=abs(a[i]−a[1]);
然后之后每次遍历 i − i + K − 1 i-i+K-1 i−i+K−1项,并处理第 i + K i+K i+K 项,因为第 i + K i+K i+K 项没有初始值
之后每次的状态转移为: d p [ j ] = m i n ( d p [ i ] + a b s ( a [ j ] − a [ i ] ) , d p [ j ] ) ; dp[j]=min(dp[i]+abs(a[j]-a[i]),dp[j]); dp[j]=min(dp[i]+abs(a[j]−a[i]),dp[j]);
d
p
[
i
+
k
]
=
d
p
[
i
]
+
a
b
s
(
a
[
i
+
k
]
−
a
[
i
]
)
;
dp[i+k]=dp[i]+abs(a[i+k]-a[i]);
dp[i+k]=dp[i]+abs(a[i+k]−a[i]);
以下为AC代码:
#include<bits/stdc++.h>
using namespace std;
int a[200100],dp[200100];
int main(){
int n,sum=0,k;
cin>>n>>k;
for(int i=1;i<=n;i++){
cin>>a[i];
}
for(int i=1;i<=k+1;i++){
dp[i]=abs(a[i]-a[1]);
}
for(int i=1;i<=n;i++){
for(int j=i+1;j<i+k&&j<=n;j++){
dp[j]=min(dp[i]+abs(a[j]-a[i]),dp[j]);
}
dp[i+k]=dp[i]+abs(a[i+k]-a[i]);
}
cout<<dp[n]<<endl;
}
Vacation
问题陈述
太郎的暑假明天就开始了,他决定现在就制定计划。
假期有 N N N 天。每 i i i ( 1 ≤ i ≤ N 1 \leq i \leq N 1≤i≤N)天,太郎将从下面的活动中选择一项,并在 i i i/th天进行:
- A: 在海里游泳。获得 a i a_i ai点快乐值。
- B: 在山上捉虫子。获得 b i b_i bi点快乐值。
- C: 在家做作业。获得 c i c_i ci点快乐。
由于太郎很容易感到无聊,所以他不能连续两天或两天以上做同样的活动。
求太郎可能获得的幸福总分的最大值。
限制因素
- 所有输入值均为整数。
- 1 ≤ N ≤ 1 0 5 1 \leq N \leq 10^5 1≤N≤105
- 1 ≤ a i , b i , c i ≤ 1 0 4 1 \leq a_i, b_i, c_i \leq 10^4 1≤ai,bi,ci≤104
样例
3
10 40 70
20 50 80
30 60 90
210
由题意可知,每次选择应与上一次选择不同,因为一共三种选择,所以可以从第二项开始遍历,每次选择前一项中另两项的最大值即可
状态转移方程:
a
[
1
]
[
i
]
+
=
m
a
x
(
a
[
2
]
[
i
−
1
]
,
a
[
3
]
[
i
−
1
]
)
;
a[1][i]+=max(a[2][i-1],a[3][i-1]);
a[1][i]+=max(a[2][i−1],a[3][i−1]);
a
[
2
]
[
i
]
+
=
m
a
x
(
a
[
1
]
[
i
−
1
]
,
a
[
3
]
[
i
−
1
]
)
;
a[2][i]+=max(a[1][i-1],a[3][i-1]);
a[2][i]+=max(a[1][i−1],a[3][i−1]);
a
[
3
]
[
i
]
+
=
m
a
x
(
a
[
2
]
[
i
−
1
]
,
a
[
1
]
[
i
−
1
]
)
;
a[3][i]+=max(a[2][i-1],a[1][i-1]);
a[3][i]+=max(a[2][i−1],a[1][i−1]);
以下为AC代码:
#include<bits/stdc++.h>
using namespace std;
int a[4][200100];
int main(){
int n;
cin>>n;
for(int i=1;i<=n;i++){
cin>>a[1][i]>>a[2][i]>>a[3][i];
if(i>1){
a[1][i]+=max(a[2][i-1],a[3][i-1]);
a[2][i]+=max(a[1][i-1],a[3][i-1]);
a[3][i]+=max(a[2][i-1],a[1][i-1]);
}
}
cout<<max({a[1][n],a[2][n],a[3][n]})<<endl;
}
还有一道大家熟悉的背包问题
Knapsack 1
问题陈述
有 N N N 个项目,编号为 1 , 2 , … , N 1, 2, \ldots, N 1,2,…,N。对于每个 i i i( 1 ≤ i ≤ N 1 \leq i \leq N 1≤i≤N),项目 i i i的权重为 w i w_i wi,值为 v i v_i vi。
太郎决定从 N N N件物品中选择一些装进背包里带回家。背包的容量为 W W W,这意味着所取物品的权重之和最多为 W W W。
求太郎带回家的物品价值的最大可能和。
限制因素
- 所有输入值均为整数。
- 1 ≤ N ≤ 100 1 \leq N \leq 100 1≤N≤100
- 1 ≤ W ≤ 1 0 5 1 \leq W \leq 10^5 1≤W≤105
- 1 ≤ w i ≤ W 1 \leq w_i \leq W 1≤wi≤W
- 1 ≤ v i ≤ 1 0 9 1 \leq v_i \leq 10^9 1≤vi≤109
样例
3 8
3 30
4 50
5 60
90
该题为
0
/
1
0/1
0/1背包问题,每次从
N
N
N 件物品中拿出一件,遍历背包的容量,当背包的容量大于该物品体积时,选择装与不装的最大值即可:
d
p
[
j
]
=
m
a
x
(
d
p
[
j
]
,
d
p
[
j
−
w
[
i
]
]
+
v
[
i
]
)
;
dp[j]=max(dp[j],dp[j-w[i]]+v[i]);
dp[j]=max(dp[j],dp[j−w[i]]+v[i]);
以下为AC代码:
#include<bits/stdc++.h>
using namespace std;
#define int long long
int dp[200100],w[200],v[200];
signed main(){
int n,s;
cin>>n>>s;
for(int i=1;i<=n;i++){
cin>>w[i]>>v[i];
}
for(int i=1;i<=n;i++){
for(int j=s;j>=w[i];j--){
dp[j]=max(dp[j],dp[j-w[i]]+v[i]);
}
}
cout<<dp[s]<<endl;
}
最长上升子序列
题目描述
这是一个简单的动规板子题。
给出一个由 n ( n ≤ 5000 ) n(n\le 5000) n(n≤5000) 个不超过 1 0 6 10^6 106 的正整数组成的序列。请输出这个序列的最长上升子序列的长度。
最长上升子序列是指,从原序列中按顺序取出一些数字排在一起,这些数字是逐渐增大的。
输入格式
第一行,一个整数 n n n,表示序列长度。
第二行有 n n n 个整数,表示这个序列。
输出格式
一个整数表示答案。
样例输入
6
1 2 4 1 3 4
样例输出
4
提示
分别取出 1 1 1、 2 2 2、 3 3 3、 4 4 4 即可。
该题比较简单,直接两个for循环,大循环 1 到 n 1到 n 1到n,小循环 1 到 i − 1 1 到 i-1 1到i−1 ,每次判断该项是否比第 i i i 项小,是则判断在第 j j j 项基础上 + 1 +1 +1还是取本身 。
状态方程: i f ( a [ j ] < a [ i ] ) d p [ i ] = m a x ( d p [ i ] , d p [ j ] + 1 ) ; if(a[j]<a[i]){ dp[i]=max(dp[i],dp[j]+1); } if(a[j]<a[i])dp[i]=max(dp[i],dp[j]+1);
以下为AC代码:
#include<bits/stdc++.h>
#define int long long
using namespace std;
int a[200100],dp[200100];
signed main()
{
int n;
cin>>n;
for(int i=1;i<=n;i++){
cin>>a[i];
dp[i]=1;
}
for(int i=1;i<=n;i++){
for(int j=1;j<i;j++){
if(a[j]<a[i]){
dp[i]=max(dp[i],dp[j]+1);
}
}
}
int sum=-1;
for(int i=1;i<=n;i++){
sum=max(dp[i],sum);
}
cout<<sum<<endl;
return 0;
}
导弹拦截III
这个题目时导弹拦截的升级版,导弹拦截是一个简单的不上升序列,就不再举例。
题目描述
很多年以前,A 国发明了一种导弹系统用来拦截敌对势力发射的导弹。
这个系统可以发射一颗导弹拦截从由到远、高度不增加的多个导弹。
但是现在,科学家们发现这个防御系统还不够强大,所以他们发明了另外一套导弹系统。
这个新系统可以发射一颗导弹由近到远的拦截更多的导弹。
当这个系统启动,首先选择一颗敌人的导弹进行拦截,然后拦截一颗更远的高度更低的导弹进行拦截,然后拦截比第二颗更远的但高度更高的导弹……以此类推,拦截的第奇数颗导弹比前一颗导弹更远、更高,拦截的第偶数颗导弹比前一个更远、更低。
现在,给你一个从近到远的导弹高度列表,计算新系统发射一颗导弹可以拦截的最多的导弹数目。
输入格式
第一行是一个整数 n n n,表示敌人发射的导弹数目。接下来的一行有 n n n 个整数,表示由近到远的。
输出格式
仅一个整数,表示拦截的最多导弹的数量。
样例 #1
样例输入 #1
4
5 3 2 4
样例输出 #1
3
提示
1 ≤ n ≤ 1 0 3 1\leq n\leq 10^3 1≤n≤103, 1 ≤ 1\leq 1≤ 导弹高度 ≤ 1 0 9 \leq 10^9 ≤109。
该题将上升与下降子序列相结合,状态为每次更新奇偶dp数组时,需要用相应的偶奇数组进行更新
状态转移方程: i f ( a [ j ] < a [ i ] ) d p [ i ] [ 1 ] = m a x ( d p [ i ] [ 1 ] , d p [ j ] [ 0 ] + 1 ) ; if(a[j]<a[i]){ dp[i][1]=max(dp[i][1],dp[j][0]+1); } if(a[j]<a[i])dp[i][1]=max(dp[i][1],dp[j][0]+1); i f ( a [ j ] > a [ i ] ) d p [ i ] [ 0 ] = m a x ( d p [ i ] [ 0 ] , d p [ j ] [ 1 ] + 1 ) ; if(a[j]>a[i]){ dp[i][0]=max(dp[i][0],dp[j][1]+1); } if(a[j]>a[i])dp[i][0]=max(dp[i][0],dp[j][1]+1);
另外,关于每一颗导弹,我们都可以对其进行拦截与不拦截,每一颗导弹都可以成为第一颗拦截的导弹。
f
o
r
(
i
n
t
i
=
1
;
i
<
=
n
;
i
+
+
)
d
p
[
i
]
[
1
]
=
1
;
for(int~i =1;i<=n;i++)~{ dp[i][1]=1; }
for(int i=1;i<=n;i++) dp[i][1]=1;
以下为AC代码:
#include<bits/stdc++.h>
using namespace std;
#define int long long
int dp[200100][2],a[200100];
signed main(){
int n;
cin>>n;
for(int i=1;i<=n;i++){
cin>>a[i];
dp[i][1]=1;
}
for(int i=2;i<=n;i++){
for(int j=1;j<i;j++){
if(a[j]<a[i]){
dp[i][1]=max(dp[i][1],dp[j][0]+1);
}
if(a[j]>a[i]){
dp[i][0]=max(dp[i][0],dp[j][1]+1);
}
}
}
int sum=0;
for(int i=1;i<=n;i++){
sum=max({sum,dp[i][0],dp[i][1]});
}
cout<<sum<<endl;
return 0;
}
接下来是本文的最后一道题,这道题学会了,线性dp基本上就掌握一大半了
Divisibility
题目描述
题面翻译
输入正整数 t t t,表示数据组数。
对于每组数据,输入 2 2 2 个整数 n n n, k k k,接下来一行 n n n 个数。
这 n n n 个数不能调换顺序。第 1 1 1 个数之前必须是加号,其它的数可加可减。设这个算式的结果为 r e s res res。
问题:存不存在一条这样的算式,使得
r
e
s
≡
0
(
m
o
d
k
)
res \equiv 0 \pmod{k}
res≡0(modk)?如果有,输出 Divisible
;否则输出 Not divisible
。
输入格式
输出格式
样例输入
2
4 7
17 5 -21 15
4 5
17 5 -21 15
样例输出
Divisible
Not divisible
大意就是让你在 n n n 个数之间加 + / − +/- +/− 号,来判断结果是否能整除 k k k .
换种思路来说,判断模 k k k 是否为零,那么,可以通过每次分别对 + / − +/- +/− 号操作之后的数进行取模并标记,在下个阶段用其取模过的数再对下个阶段的数进行 + / − +/- +/− 号操作并标记,最后,判断第 n n n 个数的 0 0 0 下标是否被标记即可.
状态转移方程:
for(int j=0;j<k;j++){
if(dp[i-1][j]){
dp[i][((j-a[i])%k+k)%k]=1;
dp[i][((j+a[i])%k+k)%k]=1;
}
}
以下是 AC代码:
#include<bits/stdc++.h>
int dp[11000][110],a[200100];
using namespace std;
void slove(){
int n,k;
cin>>n>>k;
memset(dp,0,sizeof(dp));
dp[0][0]=1;
for(int i=1;i<=n;i++){
cin>>a[i];
for(int j=0;j<k;j++){
if(dp[i-1][j]){
dp[i][((j-a[i])%k+k)%k]=1;
dp[i][((j+a[i])%k+k)%k]=1;
}
}
}
if(dp[n][0]){
cout<<"Divisible"<<endl;
}else{
cout<<"Not divisible"<<endl;
}
return ;
}
int main()
{
int t;
cin>>t;
while(t--)
slove();
return 0;
}
以上就是我个人对线性dp的总结(姑且算是吧),线性dp涉及的点不多,大体就是找状态转移方程,找初始状态即可。
觉得博主写的不错的可以三连一下!!!