动态规划之线性DP

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 1iN),石头 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| hihj的代价,其中 j j j是要降落的石块。

求青蛙到达石块 N N N之前可能产生的最小总成本。

限制因素
  • 所有输入值均为整数。
  • 2 ≤ N ≤ 1 0 5 2 \leq N \leq 10^5 2N105
  • 1 ≤ h i ≤ 1 0 4 1 \leq h_i \leq 10^4 1hi104

样例

4
10 30 40 20
30

可以先考虑状态,青蛙每到达第 i i i 块石头的成本由第 i − 1 i-1 i1 块石头和第 i − 2 i-2 i2 块石头的状态决定(第一块和第二块除外)

所以这里可以先初始化第一块和第二块: 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[i1])+dp[i1],abs(a[i]a[i2])+dp[i2])

以下为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 1iN),石头 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| hihj的代价,其中 j j j是要降落的石头。

求青蛙到达石块 N N N之前可能产生的最小总成本。

限制因素
  • 所有输入值均为整数。
  • 2 ≤ N ≤ 1 0 5 2 \leq N \leq 10^5 2N105
  • 1 ≤ K ≤ 100 1 \leq K \leq 100 1K100
  • 1 ≤ h i ≤ 1 0 4 1 \leq h_i \leq 10^4 1hi104

样例

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 iK 块到第 i i i 块的距离加上到达第 i − K i-K iK 块的成本

预处理前 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 ii+K1项,并处理第 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 1iN)天,太郎将从下面的活动中选择一项,并在 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 1N105
  • 1 ≤ a i , b i , c i ≤ 1 0 4 1 \leq a_i, b_i, c_i \leq 10^4 1ai,bi,ci104

样例

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][i1],a[3][i1]);
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][i1],a[3][i1]);
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][i1],a[1][i1]);

以下为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 1iN),项目 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 1N100
  • 1 ≤ W ≤ 1 0 5 1 \leq W \leq 10^5 1W105
  • 1 ≤ w i ≤ W 1 \leq w_i \leq W 1wiW
  • 1 ≤ v i ≤ 1 0 9 1 \leq v_i \leq 10^9 1vi109

样例

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[jw[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(n5000) 个不超过 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 1n,小循环 1 到 i − 1 1 到 i-1 1i1 ,每次判断该项是否比第 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 1n103 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

题目描述

PDF

题面翻译

输入正整数 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} res0(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涉及的点不多,大体就是找状态转移方程,找初始状态即可。

觉得博主写的不错的可以三连一下!!!

  • 15
    点赞
  • 22
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
动态规划线性规划和非线性规划是三种不同的优化方法,它们的思想和应用场景不同,具体区别如下: 1. 动态规划动态规划是一种在有重叠子问题和最优子结构的情况下可以采用的算法思想。动态规划通常用于解决多阶段决策问题,每个阶段的决策依赖于前面各个阶段的决策。常见的动态规划问题有最长公共子序列、背包问题等。 2. 线性规划:线性规划是一种优化问题,它的目标是在一组线性约束条件下最大化或最小化线性目标函数的值。线性规划问题通常可以用线性规划算法求解,这种算法的核心是单纯形法。常见的线性规划问题有生产计划、运输问题等。 3. 非线性规划:非线性规划是一类目标函数或约束条件中包含非线性项的优化问题。非线性规划问题通常比线性规划问题更难求解,因为它们的解空间通常是非凸的。常见的非线性规划问题有最小二乘法、无约束优化问题等。 举例来说: 1. 动态规划:最长公共子序列问题是一个经典的动态规划问题。给定两个字符串,求它们的最长公共子序列的长度。这个问题可以用动态规划算法求解,其中状态转移方程为: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])。 2. 线性规划:假设一个工厂有 2 种机器可以用于生产产品 A 和 B,每种机器的使用时间和成本如下表所示。现在需要制定一个生产计划,使得生产的产品 A 和 B 的总成本最小,同时满足以下约束条件:每种机器的使用时间不得超过 40 小时,产品 A 和 B 的总生产量分别不得少于 100 和 200。 | 机器 | 生产 A 的时间 | 生产 B 的时间 | A 的成本 | B 的成本 | |------|-------------|-------------|---------|---------| | 1 | 10 | 20 | 2 | 3 | | 2 | 20 | 10 | 3 | 2 | 这个问题可以用线性规划算法求解,其中目标函数为:2A + 3B + 3A + 2B = 5A + 5B,约束条件为:10A + 20B ≤ 400,20A + 10B ≤ 400,A ≥ 100,B ≥ 200。 3. 非线性规划:假设有一组数据点 {(x1, y1), (x2, y2), ..., (xn, yn)},现在要求在所有二次函数 y = ax^2 + bx + c 中找到一个最优的拟合函数,使得实际数据点与拟合函数之间的误差最小。这个问题可以用非线性规划算法求解,其中目标函数为误差平方和,即 min Σ(yi - axi^2 - bxi - c)^2。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值