0
点赞
收藏
分享

微信扫一扫

动态规划值线性DP 常见问题模板总结

回溯 2023-02-08 阅读 97


【定义】

动态规划(英语:Dynamicprogramming,DP)是一种在数学、计算机科学和经济学中使用的,通过把原问题分解为相对简单的子问题的方式求解复杂问题的方法。 动态规划常常适用于有重叠子问题和最优子结构性质的问题,动态规划方法所耗时间往往远少于朴素解法。

 

动态规划背后的基本思想非常简单。大致上,若要解一个给定问题,我们需要解其不同部分(即子问题),再合并子问题的解以得出原问题的解。 通常许多子问题非常相似,为此动态规划法试图仅仅解决每个子问题一次,从而减少计算量: 一旦某个给定子问题的解已经算出,则将其记忆化存储,以便下次需要同一个子问题解之时直接查表。 这种做法在重复子问题的数目关于输入的规模呈指数增长时特别有用。

【DP基本模型】

        在现实生活中,有一类活动的过程,由于它的特殊性,可将过程分成若干个互相联系的阶段,在它的每一阶段都需要作出决策,从而使整个过程达到最好的活动效果。当然,各个阶段决策的选取不是任意确定的,它依赖于当前面临的状态,又影响以后的发展,当各个阶段决策确定后,就组成一个决策序列,因而也就确定了整个过程的一条活动路线,这种把一个问题看作是一个前后关联具有链状结构的多阶段过程就称为多阶段决策过程,这种问题就称为多阶段决策问题。如下图所示:

动态规划值线性DP  常见问题模板总结_i++

多阶段决策过程,是指这样的一类特殊的活动过程,问题可以按时间顺序分解成若干相互联系的阶段,在每一个阶段都要做出决策,全部过程的决策是一个决策序列。

【动态规划问题满足三大重要性质】

最优子结构性质:

如果问题的最优解所包含的子问题的解也是最优的,我们就称该问题具有最优子结构性质(即满足最优化原理)。最优子结构性质为动态规划算法解决问题提供了重要线索。

子问题重叠性质:

子问题重叠性质是指在用递归算法自顶向下对问题进行求解时,每次产生的子问题并不总是新问题,有些子问题会被重复计算多次。动态规划算法正是利用了这种子问题的重叠性质,对每一个子问题只计算一次,然后将其计算结果保存在一个表格中,当再次需要计算已经计算过的子问题时,只是在表格中简单地查看一下结果,从而获得较高的效率。

无后效性:

将各阶段按照一定的次序排列好之后,对于某个给定的阶段状态,它以前各阶段的状态无法直接影响它未来的决策,而只能通过当前的这个状态。换句话说,每个状态都是过去历史的一个完整总结。这就是无后向性,又称为无后效性。

【动态规划设计方法的一般模式】

动态规划所处理的问题是一个多阶段决策问题,一般由初始状态开始,通过对中间阶段决策的选择,达到结束状态;或倒过来,从结束状态开始,通过对中间阶段决策的选择,达到初始状态。这些决策形成一个决策序列,同时确定了完成整个过程的一条活动路线,通常是求最优活动路线。

动态规划的设计都有着一定的模式,一般要经历以下几个步骤:

(1)划分阶段

按照问题的时间或空间特征,把问题划分为若干个阶段。在划分阶段时,注意划分后的阶段一定是有序的或者是可排序的,否则问题就无法求解。

(2)确定状态和状态变量

将问题发展到各个阶段时所处于的各种客观情况用不同的状态表示出来。当然,状态的选择要满足无后效性。

(3)确定决策并写出状态转移方程

因为决策和状态转移有着天然的联系,状态转移就是根据上一阶段的状态和决策来导出本阶段的状态。所以如果确定了决策,状态转移方程也就可以写出。但事实上常常是反过来做,根据相邻两段的各个状态之间的关系来确定决策。

(4)寻找边界条件

给出的状态转移方程是一个递推式,需要一个递推的终止条件或边界条件

【个人理解】

上面的话是网上或者书籍资料对动态规划的经典解释,如果你刚开始学DP,看到是云里雾里,但看完理论基础,你要做题,跟着经典例题做题,会慢慢有所感悟,基础DP(高深的我也不怎么会,后续会有补充),,其实觉得DP就是一种优化的搜索,状态还是要枚举,只不过状态之间有关系,状态转移方程是关键,边界条件是细节,做到这两点,一般DP都能解决,下面给出DP里面的经典模型,代码模板是优化过得。

【经典模型】

1)序列DP

最大最大子序列和

很简单一个问题,看这题就行,​​1003 Max Sum​​

最长上升子序列LIS

输入n及一个长度为n的数列,求出此序列的最长上升子序列长度。上升子序列指的是对于任意的i<j都满足ai<aj的子序列。(1<=n<=1000,0<=ai<=1000000)

样例输入:

5

4 2 3 1 5

样例输出:

3(最长上升子序列为2, 3, 5)

分析:

法一:O(n^2)


这是老版解释,便于理解。

但是,代码未经过优化,有些问题会超时

法二:O(nlogn)

设dp[i]表示以i为结尾的最长递增子序列的长度,则状态转移方程为:

dp[i] = max{dp[j]+1}, 1≤j<i,a[j]<a[i].

考虑两个数a[x]和a[y],x<y且a[x]<a[y],且dp[x]=dp[y],当a[t]要选择时,到底取哪一个构成最优的呢?显然选取a[x]更有潜力,因为可能存在a[x]<a[z]<a[y],这样a[t]可以获得更优的值。在这里给我们一个启示,当dp[t]一样时,尽量选择更小的a[x].

按dp[t]=k来分类,只需保留dp[t]=k的所有a[t]中的最小值,设g[k]记录这个值,g[k]=min{a[t],dp[t]=k}。

重点):

1. g[k]在计算过程中单调上升;          

2. g数组是有序的,g[1]<g[2]<..g[n]。

利用这两个性质,可以很方便的求解:

(1).设当前已求出的最长上升子序列的长度为len(初始时为1),每次读入一个新元素x:

(2).若x>g[len],则直接加入到d的末尾,且len++;(利用性质2)

   否则,在g中二分查找,找到第一个比x小的数g[k],并g[k+1]=x,在这里x≤g[k+1]一定成立(性质1,2)。(如果分析思路看不懂,直接看代码,简单)

代码实现:

#include <iostream>
#include <cstdio>
#include <algorithm>
#define maxn 1005
#define INF 99999999
using namespace std;
int n,a[maxn];
int dp[maxn]; //dp[i]:长度为i+1的上升子序列中末尾元素的最小值(不存在的话就是INF)
int main()
{
int i,j;
scanf("%d",&n);
for(i=0;i<n;i++)
scanf("%d",&a[i]);
fill(dp,dp+n,INF); //初始化dp数组为INF
for(i=0;i<n;i++) //找到更新dp[i]的位置并用a[i]更新之
{
*lower_bound(dp,dp+n,a[i])=a[i];//找到>=a[i]的第一个元素,并用a[i]替换;
/* for(j=0;j<n;j++) //观察dp数组的填充过程,dp里面保存着最长不下降子序列
printf("%d ",dp[j]);
printf("\n"); */
}
printf("%d\n",lower_bound(dp,dp+n,INF)-dp); //第一个INF出现的位置即为LIS长度
return 0;
}

方法一和方法二的不同:

方法二比方法一唯一的特点就是时间少,但方法二也有一个缺陷,方法是能打印出最长不下降子序列的,但是他不好求最长不下降子序列的最大和,举个例子:

3

1  3 2

方法二打印的最长不下降子序列dp[i]的为 1 2,为什么是2不是3,因为2在3后面。所以它不好求最长不下降子序列的最大和

2)最长公共子序列

 问题描述:

【问题描述】

字符序列的子序列是指从给定字符序列中随意地(不一定连续)去掉若干个字符(可能一个也不去掉)后所形成的字符序列。   令给定的字符序列X=“x0,x1,…,xm-1”,序列Y=“y0,y1,…,yk-1”是X的子序列,存在X的一个严格递增下标序列i0,i1,…,ik-1,使得对所有的j=0,1,…,k-1,有xij = yj。例如,X=“ABCBDAB”,Y=“BCDB”是X的一个子序列。

对给定的两个字符序列,求出他们最长的公共子序列长度,以及最长公共子序列个数。

【输入格式】

第1行为第1个字符序列,都是大写字母组成,以”.”结束。长度小于5000。

第2行为第2个字符序列,都是大写字母组成,以”.”结束,长度小于5000。

【输出格式】

第1行输出上述两个最长公共子序列的长度。

第2行输出所有可能出现的最长公共子序列个数,答案可能很大,只要将答案对100,000,000求余即可。

【输入样例1】

ABCBDAB.

BACBBD.

【输出样例1】

4

7

【时空限制】

1S

256MB

问题分析:

对与第一问,应该都会求,用基本的动态规划就可以搞定。

 问题是怎么求个数。

 令f[i][j]表示a序列到第i个数与b序列到第j个数的LCS长度。

 令g[i][j]表示a序列到第i个数与b序列到第j个数的LCS个数。

 如果a[i]==b[j]

那么f[i][j]==f[i-1][j-1]+1,g[i][j]+=g[i-1][j-1],

如果f[i][j]==f[i-1][j] 那么g[i][j]+=g[i-1][j].

如果f[i][j]==f[i][j-1] 那么g[i][j]+=g[i][j-1].

这很好理解,就是f[i][j]的个数可以是因为a[i],b[j]相同,加1推出来的;也可以是由f[i-1][j]与f[i][j-1]推来(如果长度相等的话)

 

如果a[i]!=b[j]

那么f[i][j]==max(f[i-1][j],f[i][j-1])

如果f[i][j]==f[i-1][j] 那么g[i][j]+=g[i-1][j].

如果f[i][j]==f[i][j-1] 那么g[i][j]+=g[i][j-1].

如果f[i][j]==f[i-1][j-1]那么g[i][j]-=g[i-1][j-1].

为什么要减呢?首先确定一点,如果a[i]!=b[j] ,必然f[i-1][j-1]≤max(f[i-1][j],f[i][j-1])

当取等号时,必然是f[i-1][j]==f[i][j-1]时。

 又因为g[i-1][j]由g[i-2][j]与f[i-1][j-1]推来,g[i][j-1]由g[i][j-2]与f[i-1][j-1]推来。

f[i-1][j-1]被算了两遍,所以要减去一次f[i-1][j-1]。

代码实现:

#include<bits/stdc++.h>
#include<string>
using namespace std;
const int mod=1e8;
char a[5010],b[5010];
int f[5010][5010],g[5010][5010];
int len1,len2;
int main(){
scanf("%s%s",a+1,b+1);
len1=strlen(a+1);
len2=strlen(b+1);
len1--,len2--;
for(int i=0;i<=len1;++i)
for(int j=0;j<=len2;++j)
g[i][j]=1;
for (int i = 1 ;i <=len1; i++)
for( int j = 1; j <=len2; j++) {
if (a[i]==b[j]){
f[i][j]=f[i-1][j-1] + 1;
g[i][j]=g[i-1][j-1];
if(f[i][j]==f[i-1][j]) g[i][j]=(g[i][j]+g[i-1][j])%mod;
if(f[i][j]==f[i][j-1]) g[i][j]=(g[i][j]+g[i][j-1])%mod;
}
else{
f[i][j] = max(f[i - 1][j], f[i][j - 1]);
g[i][j]=0;
if(f[i][j]==f[i-1][j]) g[i][j]=(g[i][j]+g[i-1][j])%mod;
if(f[i][j]==f[i][j-1]) g[i][j]=(g[i][j]+g[i][j-1])%mod;
if(f[i][j]==f[i-1][j-1]) g[i][j]=(g[i][j]-g[i-1][j-1]+mod)%mod;
}
}
cout<<f[len1][len2]<<"\n"<<(g[len1][len2]+mod)%mod<<"\n";
return 0;
}

3)最长公共上升子序列 LCIS

题意:

给出有 n 个元素的数组 a[] , m 个元素的数组 b[] ,求出它们的最长上升公共子序列的长度.

定义状态

 

F[i][j]表示以a串的前i个整数与b串的前j个整数且以b[j]为结尾构成的LCIS的长度。

 

状态转移方程:

 

①F[i][j] = F[i-1][j] (a[i] != b[j])

 

②F[i][j] = max(F[i-1][k]+1) (1 <= k <= j-1 && b[j] > b[k])

 

现在我们来说为什么会是这样的状态转移方程呢?

 

对于①,因为F[i][j]是以b[j]为结尾的LCIS,如果F[i][j]>0那么就说明a[1]..a[i]中必然有一个整数a[k]等于b[j],因为a[k]!=a[i],那么a[i]对F[i][j]没有贡献,于是我们不考虑它照样能得出F[i][j]的最优值。所以在a[i]!=b[j]的情况下必然有F[i][j]=F[i-1][j]。

 

对于②,前提是a[i] == b[j],我们需要去找一个最长的且能让b[j]接在其末尾的LCIS。之前最长的LCIS在哪呢?

首先我们要去找的F数组的第一维必然是i-1。因为i已经拿去和b[j]配对去了,不能用了。并且也不能是i-2,因为i-1必然比i-2更优。

第二维呢?那就需要枚举b[1]...b[j-1]了,因为你不知道这里面哪个最长且哪个小于b[j]。

这里还有一个问题,可不可能不配对呢?也就是在a[i]==b[j]的情况下,需不需要考虑F[i][j]=F[i-1][j]的决策呢?答案是不需要。因为如果b[j]不和a[i]配对,那就是和之前的a[1]...a[j-1]配对(假设F[i-1][j]>0,等于0不考虑),这样必然没有和a[i]配对优越。(为什么必然呢?因为b[j]和a[i]配对之后的转移是max(F[i-1][k])+1,而和之前的i`配对则是max(F[i`-1][k])+1。

代码实现:

#include <iostream>
#include <cstdlib>
#include <cstdio>
#include <cstring>
#include <string>
#include <algorithm>
using namespace std;

const int MAXN = 1001;

int a[MAXN], b[MAXN];
int f[MAXN][MAXN];
int n, m,ans;

void init()
{
memset(f, 0, sizeof(f));
}
void dp()
{
init();
int i, j, k;
for(i = 1; i <= n; i++)
{
for(j = 1; j <= m; j++)
{
f[i][j] = f[i-1][j]; // if(a[i] != b[j])
if(a[i] == b[j])
{
int MAX = 0;
for(k = 1; k <= j-1; k++) if(b[j] > b[k]) //枚举最大的f[i-1][k]
{
MAX = max(MAX, f[i-1][k]);
}
f[i][j] = MAX+1;
}
}
}
ans = 0;
for(int i = 1; i <= m; i++) ans = max(ans, f[n][i]);

}
int main()
{
int T;
cin>>T;
while(T--)
{
scanf("%d",&n) ;
for(int i=1;i<=n;i++)
scanf("%d",&a[i]);
scanf("%d",&m);
for(int i=1;i<=m;i++)
scanf("%d",&b[i]);
dp();

printf("%d\n", ans);
if(T)
printf("\n");
}
}

时间优化:

以上的代码的时间复杂度是O(n^3),那我们怎么去优化呢?通过思考发现,第三层循环找最大值是否可以优化呢?我们能否直接把枚举最大的f[i-1][k]值直接算出来呢?假设存在这么一个序列a[i] == b[j],我们继续看状态转移方程②,会发现b[j] > b[k],即当a[i] == b[j]时,可以推出a[i] > b[k],那么有了这个表达式我们可以做什么呢?可以发现,我们可以维护一个MAX值来储存最大的f[i-1][k]值。即只要有a[i] > a[j]的地方,那么我们就可以更新最大值,所以,当a[i] == b[j]的时候,f[i][j] = MAX+1,即可。

void dp()
{
for(int i = 1; i <= n; i++)
{
int MAX = 0; //维护最大值
for(int j = 1; j <= m; j++)
{
f[i][j] = f[i-1][j]; //a[i] != b[j]
if(a[i] > b[j]) MAX = max(MAX, f[i-1][j]);
if(a[i] == b[j]) f[i][j] = MAX+1;
}
}
int ans = 0;
for(int i = 1; i <= m; i++) ans = max(ans, f[n][i]);
printf("%d\n", ans);
}

可以发现,其实上面的代码有些地方与0/1背包很相似,即每次用到的只是上一层循环用到的值,即f[i-1][j],那么我们可以像优化0/1背包问题利用滚动数组来优化空间。

void dp()
{
init();
for(int i = 1; i <= n; i++)
{
int MAX = 0;
for(int j = 1; j <= n; j++)
{
if(a[i] > b[j]) MAX = max(MAX, f[j]);
if(a[i] == b[j]) f[j] = MAX+1;
}
}
int ans = 0;
for(int j = 1; j <= m; j++) ans = max(ans, f[j]);
printf("%d\n", ans);
}

如果是求最长公共下降子序列呢?很明显嘛,把状态定义改动一下,即f[i][j]表示以a串的前i个整数与b串的前j个整数且以b[j]为结尾构成的LCDS的长度,具体实现的时候只要把a[i] > b[j]改为a[i] < b[j]就可以啦。

还有一种打印路径的模板:

 

#include<iostream>
#include<math.h>
#include<stdio.h>
#include<algorithm>
#include<string.h>
#include<vector>
#include<map>
using namespace std;
typedef long long lld;
const int oo=0x3f3f3f3f;
const lld OO=1LL<<61;
const lld MOD=1000000007;
#define eps 1e-6
#define maxn 505
int dp[maxn][maxn];
int path[maxn][maxn];
int a[maxn],b[maxn];
int n,m;

void dfs(int x)
{
if(path[n][x]==-1)
{
printf("%d",b[x]);
return ;
}
dfs(path[n][x]);
printf(" %d",b[x]);
}

int main()
{
while(scanf("%d",&n)!=EOF)
{
for(int i=1;i<=n;i++)
scanf("%d",&a[i]);
scanf("%d",&m);
for(int i=1;i<=m;i++)
scanf("%d",&b[i]);

memset(dp,0,sizeof dp);
memset(path,-1,sizeof path);
for(int i=1;i<=n;i++)
{
int pos=-1,Max=0;
for(int j=1;j<=m;j++)
{
dp[i][j]=dp[i-1][j];
path[i][j]=path[i-1][j];
if(a[i]==b[j]&&dp[i][j]<Max+1)
{
dp[i][j]=Max+1;
path[i][j]=pos;
}
if(a[i]>b[j]&&dp[i-1][j]>Max)
{
Max=dp[i-1][j];
pos=j;
}
}
}
int ans=1;
for(int i=1;i<=m;i++)
if(dp[n][ans]<dp[n][i])
ans=i;
printf("%d\n",dp[n][ans]);
if(dp[n][ans])dfs(ans);
puts("");
}
return 0;
}
/**
*/

 

举报

相关推荐

0 条评论