【动态规划】线性动态规划
吐槽:动态规划这个东西,只要推不出状态转移方程,一切都白搭
基础知识
一. 动态规划
动态规划中最重要的三个概念:最优子结构,重复子问题,无后效性。
- 最优子结构:如果问题的最优解所包含的子问题的解也是最优的,就称该问题具有最优子结构。
- 无后效性:即某阶段状态一旦确定,就不受这个状态以后决策的影响。也就是说,某状态以后的过程不会影响以前的状态,只与当前状态有关。即“现在决定未来,未来与过去无关。”
- 重复子问题:即子问题之间是不独立的,一个子问题在下一阶段决策中可能被多次使用到。虽然该性质并不是动态规划适用的必要条件,但是如果没有这条性质,动态规划算法同其他算法相比就不具备优势
二 . 线性动态规划
线性动态规划是各种动态规划的基础,是动态规划中变化最多的一类。
线性动态规划的主要特点是状态的推导是按照问题规模 i 从小到大或从大到小依次推过去的,较大规模的问题的解依赖较小规模的问题的解。
线性动态规划针对的问题是最常见的数组,字符串,矩阵等,这三种数据结构本身就是线性的。因此出现这些类型的输入的时候,如果要用到动态规划,首先考虑线性动态规划就很合理了,因此很多问题不论最后正解是不是线性动态规划,都会首先想一下线性动态规划是否可行。
单个数组或字符串上设计一维状态,两个数组或字符串上设计两维状态,以及矩阵上设计两维状态等等,同时以上三种情况的状态设计都有可能再加上额外的判断标准的状态,这里面变化就很多了,有的题目可能会在新增的这一维上使用二分,贪心的策略,有的题目需要 DP 状态与数据结构配合来解决问题。
除此之外,还有一类问题没有显式的数组,字符串,但是在求解的时候依然满足前面提到的动态规划三条基本概念,可以用动态规划求解,这种问题通常也是线性动态规划。
背包问题也属于线性动态规划,但是由于变化较多,会拿出来专门介绍。
一 . 求最长上升子序列Longest Increasing Subsequence(LIS问题)
例题:洛谷 B3637 最长上升子序列
题目描述
给出一个由 个不超过 的正整数组成的序列。请输出这个序列的最长上升子序列的长度。
最长上升子序列是指,从原序列中按顺序取出一些数字排在一起,这些数字是逐渐增大的。
输入格式
第一行,一个整数 n,表示序列长度。
第二行有 n 个整数,表示这个序列。
输出格式
一个整数表示答案。
输入输出样例
输入 #1
6 1 2 4 1 3 4
输出 #1
4
说明/提示
分别取出 1、2、3、4 即可。
思路:
先从大循环开始,从 1 到 n,计算 ,记得初始化的值是 1;接下来从 1 到 i-1,即用 j 控制 i 之前的数值,如果 小于 的话,说明这个数可以和 组成上升子序列,则 取,最后统计在这段序列中最长上升子序列中长度的最大值。
时间复杂度为 的做法:
#include <iostream>
#include <cstdio>
#include <cstring>
#include <algorithm>
using namespace std;
int n;
const int maxn=1e6+5,INF=0x3f3f3f3f;
int a[maxn],f[maxn];
inline int read()
{int x=0,f=1;char c=getchar();while(c<'0'||c>'9'){if(c=='-') f=-1;c=getchar();}while(c>='0'&&c<='9'){x=(x<<3)+(x<<1)+(c^48);c=getchar();}return x*f;
}int main()
{n=read();for(int i=1;i<=n;++i){a[i]=read();f[i]=1; //记得初始化为1;}for(int i=1;i<=n;++i){for(int j=1;j<i;++j){if(a[i]>a[j]){f[i]=max(f[i],f[j]+1);}}}int Max=-INF;for(int i=1;i<=n;++i){if(f[i]>Max) Max=f[i]; //记录所有数据中能保证得到的最大的子序列的长度}printf("%d",Max);return 0;
}
二 . 求最长公共子序列 The longest common subsequence (LCS问题)
例题: 洛谷 P1439 【模板】最长公共子序列
题目描述
给出 的两个排列 和 ,求它们的最长公共子序列。
输入格式
第一行是一个数 n 。
接下来两行,每行为 n 个数,为自然数 的一个排列。
输出格式
一个数,即最长公共子序列的长度。
输入输出样例
输入 #1
5 3 2 1 4 5 1 2 3 4 5
输出 #1
3
说明/提示
- 对于50%的数据, ;
- 对于100%的数据, 。
思路:
设dp[i][j]表示两个串从1到第一个串的第 i 位,求与第二个串的第 j 位最多有多少个公共子元素
时间复杂度为 的做法:
#include <iostream>
#include <cstdio>
#include <cstring>
#include <algorithm>
using namespace std;
int n;
const int maxn=1e5+5;
int a[maxn],b[maxn],dp[3005][3005];inline int read()
{int x=0,f=1;char c=getchar();while(c<'0'||c>'9'){if(c=='-') f=-1;c=getchar();}while(c>='0'&&c<='9'){x=(x<<1)+(x<<3)+(c^48);c=getchar();}return x*f;
}int main()
{n=read();for(int i=1;i<=n;++i){a[i]=read();}for(int i=1;i<=n;++i){b[i]=read();}dp[0][0]=1; //还没进行比较的时候,每一个位置只有一个数for(int i=1;i<=n;++i){for(int j=1;j<=n;++j){if(a[i] == b[j]){dp[i][j]=max(dp[i][j],dp[i-1][j-1]+1); //与还没有进行a[i]与b[j]匹配之前的子序列数量进行比较}else{dp[i][j]=max(dp[i-1][j],dp[i][j-1]); //继承未匹配前的最大值}}}printf("%d",dp[n][n]);return 0;
}
因为数据范围很大,开不了1e5*1e5的二维数组,而且相当于每一个串的每一个位置都需要比较,因此需要优化程序。
(待填坑)优化后的程序时间复杂度为 。
需要注意的是:最长公共子序列是按位向后比对的,所以a序列每个元素在b序列中的位置如果递增,就说明b中的这个数在a中的这个数整体位置偏后。
#include <iostream>
#include <cstdio>
#include <cstring>
#include <algorithm>
using namespace std;
int n;
const int maxn=1e5+5,INF=0x3f3f3f3f;
int a[maxn],b[maxn],dp[maxn],change[maxn];inline int read()
{int x=0,f=1;char c=getchar();while(c<'0'||c>'9'){if(c=='-') f=-1;c=getchar();}while(c>='0'&&c<='9'){x=(x<<1)+(x<<3)+(c^48);c=getchar();}return x*f;
}int main()
{n=read();for(int i=1;i<=n;++i){a[i]=read();change[a[i]]=i; //记录下标}for(int i=1;i<=n;++i){b[i]=read();dp[maxn]=INF;}int len=0; //最开始的最长公共子序列的长度为0dp[0]=0; for(int i=1;i<=n;++i){int l=0,r=len,mid;if(change[b[i]]>dp[len]) //如果新加入的数比子序列的最大值还要大 {dp[++len]=change[b[i]]; //直接加入到子序列中}else //利用二分查找降低复杂度{while(l<r){mid=(l+r)>>1;if(dp[mid]>change[b[i]]) //缩小区间{r=mid;}else l=mid+1;}}dp[l]=min(dp[l],change[b[i]]); }printf("%d",len);return 0;
}
三 . DAG中的最长路与最短路
例题:洛谷 P2583 地铁间谍 / UVA1025 城市里的间谍 A Spy in the Metro
思路:因为在时间轴上,能影响决策的只有所在时间点和当前所在的车站。
在进行决策时可以有三种选择:1 . 在当前车站等待一分钟;2.在当前车站搭上向左行驶的列车;3.在当前车站搭上向右行驶的列车;
设dp[i][j],下标表示时间点 i 和当前所在的车站 j ,求需要等待的最少时间;设train[i][j][0/1] ,表示在时刻 i 下,在车站 j 有一趟向右或者向左出发的列车。
#include <iostream>
#include <cstdio>
#include <cstring>
#include <algorithm>
using namespace std;
int n,T,num=0;
const int INF=0x3f3f3f3f;
int t[205];
int dp[2005][55];
bool train[2005][55][3];
inline int read()
{int x=0,f=1;char c=getchar();while(c<'0'||c>'9'){if(c=='-') f=-1;c=getchar();}while(c>='0'&&c<='9'){x=(x<<1)+(x<<3)+(c^48);c=getchar();}return x*f;
}int main()
{while(1){n=read();if(n==0){break; return 0;}memset(train,0,sizeof(train));memset(t,0,sizeof(t));T=read();for(int i=1;i<=n-1;++i){t[i]=read();}int M1=read();for(int i=0;i<M1;++i){int a=read();for(int j=1;j<=n;++j) //记录从左往右开的列车{train[a][j][0]=1;a+=t[j]; //记录这一段的时间和} }int M2=read(); for(int i=0;i<M2;++i) //记录从右往左开的列车{int b=read();for(int j=n;j>=1;--j){train[b][j][1]=1;b+=t[j-1]; //记录时间前缀和}}for(int i=1;i<n;++i){dp[T][i]=INF; //寻找最小值需要初始化为最大值}dp[T][n]=0;for(int i=T-1;i>=0;--i){for(int j=1;j<=n;++j){dp[i][j]=dp[i+1][j]+1; //停在原地if(j<n && train[i][j][0] && i+t[j]<=T) dp[i][j]=min(dp[i][j],dp[i+t[j]][j+1]);
//搭上向右行驶的列车(条件解释:右边还存在站台,存在向右的列车,并且在规定时间范围内)if(j>1 && train[i][j][1] && i+t[j-1]<=T) dp[i][j]=min(dp[i][j],dp[i+t[j-1]][j-1]); //搭上向左行驶的列车}}cout<<"Case Number "<<++num<<": ";if(dp[0][1] >= INF) cout<<"impossible"<<endl;else cout<<dp[0][1]<<endl;}return 0;
}
其他无统称线性动态规划汇总
一 . 入门类线性动态规划
1 . 洛谷 P1799 数列
思路:设dp[i][j]为:在前 i 个数中删去 j 个数后,剩余最多的符合题意的数的个数。
可以分为当前位置的数是否删去:如果删去当前位置的数,那么:dp[i][j]=dp[i−1][j−1];若不删当前位置的数,依然分两种情况:如果这个数不符合题意:dp[i][j]=dp[i-1][j]dp[i][j]=dp[i−1][j];如果这个数符合题意:dp[i][j]=dp[i-1][j]+1dp[i][j]=dp[i−1][j]+1。
判断条件为:当前的数值与位置下标的值是否相等即(a[i] == i - j),当前位置在操作前可能已经删减了 i 个数,也可能没有。
#include <iostream>
#include <cstdio>
#include <cstring>
#include <algorithm>
using namespace std;
int n;
bool flag;
const int maxn=1e7+5,INF=0x3f3f3f3f;
int a[maxn],dp[1005][1005],Max=-INF;
inline int read()
{int x=0,f=1;char c=getchar();while(c<'0'||c>'9'){if(c=='-') f=-1;c=getchar();}while(c>='0'&&c<='9'){x=(x<<1)+(x<<3)+(c^48);c=getchar();}return x*f;
}int main()
{n=read();for(int i=1;i<=n;++i){a[i]=read();}dp[0][0]=0;for(int i=1;i<=n;++i){for(int j=0;j<=i;++j){if(j>0) dp[i][j]=dp[i-1][j-1]; //要注意一下j不能等于0,因为j-1前面没有数啦!if(a[i]==i-j) dp[i][j]=max(dp[i][j],dp[i-1][j]+1);else dp[i][j]=max(dp[i][j],dp[i-1][j]);}}for(int i=1;i<=n;++i){for(int j=0;j<=n;++j){Max=max(Max,dp[i][j]);}}printf("%d",Max); //查找最大值return 0;
}
二 . 根据某种条件选取两个数,求总情况
1 . 洛谷 P2513 [HAOI2009]逆序对数列
题目描述
对于一个数列 ,如果有 i<j 且 ,那么我们称 与 为一对逆序对数。若对于任意一个由 1∼n 自然数组成的数列,可以很容易求出有多少个逆序对数。那么逆序对数为 k 的这样自然数数列到底有多少个?
输入格式
第一行为两个整数n,k。
输出格式
写入一个整数,表示符合条件的数列个数,由于这个数可能很大,你只需输出该数对10000求余数后的结果。
输入输出样例
输入 #1
4 1
输出 #1
3
说明/提示
样例说明:
下列3个数列逆序对数都为1;分别是1 2 4 3 ;1 3 2 4 ;2 1 3 4;
测试数据范围
30%的数据
100%的数据 ,
思路:
设 f [i][j] 表示从1~ i 的数列,有 j 个逆序对。
在1 ~ i-1 的数列中插入 i 这个数,可以产生0 ~ i-1 个逆序对
所以,可以推出式子:f [i][j]=∑ f [i-1][j-k] (&& )
#include <iostream>
#include <cstdio>
#include <cstring>
#include <algorithm>
using namespace std;
int n,k;
const int maxn=1005,mod=10000;
int f[maxn][maxn];
inline int read()
{int x=0,f=1;char c=getchar();while(c<'0'||c>'9'){if(c=='-') f=-1;c=getchar();}while(c>='0'&&c<='9'){x=(x<<1)+(x<<3)+(c^48);c=getchar();}return x*f;
}int main()
{n=read(); k=read();f[0][0]=f[1][0]=1; //初始化for(int i=2;i<=n;++i){int sum=0;for(int j=0;j<=k;++j){(sum+=f[i-1][j])%mod;f[i][j]=sum%mod;if(j-i+1>=0) sum=(sum-f[i-1][j-i+1]+mod)%mod;}}printf("%d\n",f[n][k]);return 0;
}
三. 线段覆盖问题
在这类问题中,通常是:在一维坐标系中,给出左右节点,问线段不重合的情况下,最长能覆盖多少,一般会通过建图的思想存储线段的左右节点。
例题:洛谷 P1868 饥饿的奶牛
思路:在一些线性结构中,可以将两个端点看做图的一条边,从大的端点向小的端点连线,两点之间的距离即y-x+1看做可修改的路径长度。在这道题中即对当前稻草的向前的边所指向的稻草 的最大价值加这条边的价值取最大值,就是当前稻草时的最大价值。
#include <iostream>
#include <cstdio>
#include <cstring>
#include <algorithm>
using namespace std;
long long int n,sum=0;
const int maxn=1e7+5,INF=0x3f3f3f3f;
struct node{int to,next;
}edge[maxn<<1];
int head[maxn],num=0;
int dp[maxn];
inline int read()
{int x=0,f=1;char c=getchar();while(c<'0'||c>'9'){if(c=='-') f=-1;c=getchar();}while(c>='0'&&c<='9'){x=(x<<3)+(x<<1)+(c^48);c=getchar();}return x*f;
}inline void add(int u,int v)
{edge[++num].to=v;edge[num].next=head[u];head[u]=num;
}int main()
{n=read();int Max=-INF;memset(head,-1,sizeof(head));for(int i=1;i<=n;++i){int x,y;x=read(); y=read();add(y,x); if(y>Max) Max=y; //找到最大的点}for(int i=0;i<=Max;++i) //范围从0到当前所有数据的最大值{dp[i]=dp[i-1];for(int j=head[i];j!=-1;j=edge[j].next){int v=edge[j].to;dp[i]=max(dp[i],dp[v-1]+(i-v+1)); //当前节点的最大值:与当前节点相连的节点 的最大值 与这两个节点之间的距离之和}}printf("%d",dp[Max]);return 0;
}
变式1:已知线段的其中一个端点及线段的长度
例题:洛谷 P1280 尼克的任务
思路:在某个时间点上,尼克存在两种状态,即空闲状态和正在完成任务的状态。因为“如果在同一时刻有多个任务需要完成,尼克可以任选其中的一个来做,而其余的则由他的同事完成,反之如果只有一个任务,则该任务必需由尼克去完成,假如某些任务开始时刻尼克正在工作,则这些任务也由尼克的同事完成。”,所以第 i 时刻的最大空闲时间与后面的选择任务的持续时间有关系,那么从最后选的事情进行倒推。
#include <iostream>
#include <cstdio>
#include <cstring>
#include <algorithm>
using namespace std;
int n,k;
const int maxn=1e7+5;
struct node{int p,t;
}edge[maxn<<1];
int f[maxn],dp[maxn];
inline int read()
{int x=0,f=1;char c=getchar();while(c<'0'||c>'9'){if(c=='-') f=-1;c=getchar();}while(c>='0'&&c<='9'){x=(x<<1)+(x<<3)+(c^48);c=getchar();}return x*f;
}inline bool cmp(node x,node y)
{return x.p>y.p;
}int main()
{n=read(); k=read();memset(f,0,sizeof(f));for(int i=1;i<=k;++i){edge[i].p=read();edge[i].t=read();f[edge[i].p]++; //标记这个点是否存在任务}sort(edge+1,edge+k+1,cmp);for(int i=n;i>=1;--i){if(f[i]==0) //若该点不存在任务{dp[i]=dp[i+1]+1; //比前一个空闲状态下多一个空闲时间}else {for(int j=1;j<=k;++j){if(edge[j].p==i) //该点存在任务dp[i]=max(dp[i],dp[i+edge[j].t]); //选出空闲时间最长的点,从p开始t秒后全部休息不了}}}printf("%d",dp[1]);return 0;
}
变式2:已知线段的两个端点,区间范围为左闭右开
例题:洛谷 P2439 [SDOI2005]阶梯教室设备利用
题目中要求“假设在某一演讲结束的瞬间我们就可以立即开始另一个演讲”,那么说明到达 k 时刻的时候已经算下一场演讲的范围了,所以在建图的时候,应该从 k-1 这个节点开始建立,意识到这一点其他的都跟原来的处理方式一样。
#include <iostream>
#include <cstdio>
#include <cstring>
#include <algorithm>
using namespace std;
int n,p,k;
const int maxn=1e7+5,INF=0x3f3f3f3f;
int dp[maxn];
struct node{int to,next;
}edge[maxn<<1];
int head[maxn],num=0;
inline int read()
{int x=0,f=1;char c=getchar();while(c<'0'||c>'9'){if(c=='-') f=-1;c=getchar();}while(c>='0'&&c<='9'){x=(x<<1)+(x<<3)+(c^48);c=getchar();}return x*f;
}inline void add(int u,int v)
{edge[++num].to=v;edge[num].next=head[u];head[u]=num;
}int main()
{n=read();memset(head,-1,sizeof(head));int Max=-INF;for(int i=1;i<=n;++i){p=read(); k=read();add(k-1,p); //!!这里建图要注意Max=max(Max,k);}for(int i=1;i<=Max;++i){dp[i]=dp[i-1];for(int j=head[i];j!=-1;j=edge[j].next){int v=edge[j].to;dp[i]=max(dp[i],dp[v-1]+i-v+1);}}printf("%d",dp[Max]);return 0;
}
变式3:已知线段的两个端点,存在区间扩大的情况
例题:洛谷 P2889 [USACO07NOV]Milking Time S
思路:由题得:“每次 FJ 给 Bessie 挤奶之后,Bessie 都要休息 R 个小时,才能开始下一次挤奶”,那么,我们可以将休息的时间与挤奶的时间看成同一段时间,最后统计总时间段能够获得多少奶。
#include <iostream>
#include <cstring>
#include <cstring>
#include <algorithm>
using namespace std;
int n,m,r;
int start,End,ef;
const int maxn=1e7+5;
struct node{int to,next,w;
}edge[maxn<<1];
int head[maxn],num=0,dp[maxn];
inline int read()
{int x=0,f=1;char c=getchar();while(c<'0'||c>'9'){if(c=='-') f=-1;c=getchar();}while(c>='0'&&c<='9'){x=(x<<1)+(x<<3)+(c^48);c=getchar();}return x*f;
}inline void add(int u,int v,int w)
{edge[++num].to=v;edge[num].w=w;edge[num].next=head[u];head[u]=num;
}int main()
{n=read(); m=read(); r=read();memset(head,-1,sizeof(head));for(int i=1;i<=m;++i){start=read(); End=read(); ef=read();add(End,start,ef);}for(int i=0;i<=n;++i){dp[i]=dp[i-1];for(int j=head[i];j!=-1;j=edge[j].next){int v=edge[j].to;dp[i]=max(dp[i],dp[max(0,v-r)]+edge[j].w); //注意start节点向前推长度r时,可能出现超过时间0的情况,这里需要特判}}printf("%d",dp[n]);return 0;
}
【动态规划】线性动态规划相关推荐
- 0x51.动态规划 - 线性DP(习题详解 × 10)
目录 0x51.动态规划 - 线性DP 0x51.1 LIS问题 Problem A. 登山 (最长下降子序列) Problem B. 友好城市(思维) Problem C. 最大上升子序列和 0x5 ...
- 动态规划 —— 线性 DP
[概述] 线性动态规划,是较常见的一类动态规划问题,其是在线性结构上进行状态转移,这类问题不像背包问题.区间DP等有固定的模板. 线性动态规划的目标函数为特定变量的线性函数,约束是这些变量的线性不等式 ...
- 动态规划 —— 线性 DP —— 字符串编辑距离
[概述] 字符串编辑距离,即 Levenshtein 距离,是俄国科学家 Vladimir Levenshtein 提出的概念,是指从一个字符串修改到另一个字符串时,编辑单个字符所需的最少次数,编辑单 ...
- 第十七章:线性动态规划
通过上一章对01背包问题的学习,我相信同学们都动态规划都有了一个新的认识.在我们利用动态规划解决问题的过程通常会有以下几个常见的专业术语,分别是:状态定义,状态的转移,初始化和边界条件.下面我们对这几 ...
- 桐爷开车 线性动态规划
题目描述 桐爷人称计科老司机,有天要从深圳跑长途去帝都.桐爷的塞恩车每跑一千米耗油一升.塞恩车的油箱容量为200升.桐爷出发时有100升油,为了造成少耗油的假象到达帝都时至少有100升油.沿途有不少加 ...
- 矩阵连乘 动态规划_Java动态规划
1. 介绍 动态规划典型的被用于优化递归算法,因为它们倾向于以指数的方式进行扩展.动态规划主要思想是将复杂问题(带有许多递归调用)分解为更小的子问题,然后将它们保存到内存中,这样我们就不必在每次使用它 ...
- 动态规划之线性动态规划
一.动态规划的定义以及术语 1.定义:将一个问题分解为子问题递归求解,并且将中间结果保存以避免重复计算的办法,就叫做"动态规划" 2.阶段:把求解问题的过程恰当地分成若干个相互联系 ...
- P2196 [NOIP1996 提高组] 挖地雷 线性动态规划DP 题解
原题链接:[P2196 NOIP1996 提高组] 挖地雷 - 洛谷) 题目分析 看到这道题,首先感觉是个搜索,如果数据范围不大应该没问题.但是,,,我没找到数据范围啊喂~ 那就动态规划一下,先用二维 ...
- 动态规划 —— 线性 DP —— 最大和问题
[最大子序列和] 问题定义:对于给定序列 a1,a2,a3--an 寻找它的连续的最大和子数组. 用数组 dp[i] 来保存当前最大的连续子数组,循环遍历每个数,然后每次检验 dp[i-1] 是否大于 ...
最新文章
- MonoCon:使用辅助学习的单目3D目标检测框架(AAAI 2022)
- php 使用css乱码,分享CSS字符编码引起乱码快速解决的方法
- 在Wireshark中查找数据包
- 如何在局域网内查找病毒主机
- 数学教师计算机能力提升,深度融合信息技术,提升数学课堂魅力
- 我和ABP vNext 的故事
- Dw序号列表如何通过html语言加,使用DW软件实现html编码转换的详细步骤
- 转:Deep learning系列(十五)有监督和无监督训练
- js最简单的几个特效_腊八蒜最简单做法,掌握这几个诀窍快速变绿,又脆又香,真过瘾...
- install-newton部署安装--------计算节点部署安装
- POJ1637 Sightseeing tour(判定混合图欧拉回路)
- KeyRaider:迄今最大规模的苹果账号泄露事件
- 计划学Linux,老男孩Linux怎么样?真实的学员评价!
- SSM项目实战之博客系统
- 常用的免费好用的DNS有哪些?
- Chrome HackBar工具下载
- 网站安全之域名被劫持、域名被劫持后该怎么办!!!
- 算法:通过克鲁斯卡尔(Kruskal)算法,求出图的最小生成树
- Element表单验证规则
- 适合学生学计算机专业的电脑,什么电脑比较适合用于学计算机专业的学生用