DP入门之动态序列问题

DP入门中的动态序列问题

Ch1. 序列和问题

No1.1 最大子序列和问题

动态转移方程: dp[i] = Max( dp[i],dp[i-1]+a[i] ),其中dp[i]表示以i结尾的子序列的最大和。 前面的dp[j-1]用完后就没有用了,因此为了节约内存空间,可以将存放元素的数组作dp时用。
例题: hdu-1003 Max Sum
思路: 题目要求求最大子序列和以及最大子序列的左右端点。最大子序列和很好求,用mx保存子序列的最大值,最大子序列区间左右端点分别用 l 和 r 保存,当前遍历区间的左端点用 cur_l 保存,当前遍历区间的右端点就是 i ,因此不用另外保存。当a[i] < dp[i-1] + a[i]亦即 dp[i-1] < 0 时,说明上一个区间和非最优解,将当前区间左端点移至当前结点,即cur_l = i;当dp[j] > mx时,更新mx以及最大子序列的左右端点,l = cur_l,r = i。

代码实现:

#include <cstdio>
#include <cstring>
#include <iostream>
#include <algorithm>
#define ll long long
#define mem(f, x) memset(f, x, sizeof(f) )
#define INF 0x3f3f3f3f
#define pii pair<int,int>
#define mk(x,y) make_pair(x,y)
#define fi first
#define se second
#define pk push_back
using namespace std;
const int N = 1e5+5;
const int M = 1e5+5;
int a[N];
int m, n;

int main( ){
   
    int t, cs = 1;
    scanf( "%d", &t );
    while( t-- ){
   
        scanf( "%d", &n );
        int l, r, cur_l;
        l = r = cur_l = 1;
        for( int i = 1; i <= n; i++ )
            scanf( "%d", &a[i] );
        int mx = a[1];
        for( int i = 2; i <= n; i++ ){
   
            a[i] = max( a[i], a[i]+a[i-1] );
            if( a[i-1] < 0 )
                cur_l = i;
            if( a[i] > mx )
                l = cur_l, r = i, mx = a[i];
        }
        printf( "Case %d:\n", cs++ );
        printf( "%d %d %d\n", mx, l, r );
        if( t ) printf( "\n" );
    }
    return 0;
}

No1.2 最大子段和问题

基础模型: 从长度为n的序列中找出m段子序列(相互之间无交叉)使这m段子序列之和最大,求出这一最大值。
思路:
参考网址

把一个数组分成m段, sum(i1, j1) + sum(i2, j2) + sum(i3, j3) + … + sum(im, jm),求使得上述和最大,ik,jk是连续的jk和ik+1可以不连续。

动态规划,d[i][j]表示在选取第j个数字的情况下,将前j个数字分成i组的最大和,则它的值有两种可能:

①(x1,y1),(x2,y2)…(xi,yi,num[j])
②(x1,y1),(x2,y2)…(xi-1,yi-1),…,(num[j]), 其中yi-1是第k个数字

故:d[i][j]=max(d[i][j-1],d[i-1][k])+num[j],其中k=i-1,i,…,j-1
但是题目中,1 ≤ x ≤ n ≤ 1,000,000,m的范围没有给出,内存会爆掉,也会TLE

优化方法:
注意到,d[i][ * ] 只和d[i][ * ],d[i-1][*]有关,即当前状态只与前一状态有关,可以用滚动数组完成。
d[t][j]=max(d[t][j-1],d[t-1][k])+num[j],其中k=i-1,i,…,j-1,t=1
其中只需要记录两个状态,当前状态t=1,上一状态t=0
空间优化了但时间没有优化。

考虑我们也不需要j-1之前的最大和具体发生在k位置,只需要在j-1处记录最大和即可,用pre[j-1]记录即可
pre[j-1],包括num[j-1]的j-1之前的最大和
则d[t][j]=max(d[t][j-1],pre[j-1])+num[j]
此时可以看见,t这一维也可以去掉了
即最终的状态转移为
d[j]=max( d[j-1], pre[j-1] )+num[j]

其中pre[j-1]表示dp[i-1][i-1…j-1]中的最大值,这里思考如何得出pre[]数组,由代码中我们可以看出,我们可以在dp[j=i…n]的循环中用pre[]数组保存
dp[i-1][i…j]的的最大值,当经历下一次循环时,上一轮循环中保存的pre起点是 i ,即这一轮的 i-1,相对于这一轮循环上一轮中保存的就是pre[i-1][i-1…j]的最大值,由此我们便可以根据状态转移方程得出dp数组,有点值得注意的是,最终的答案并非是dp[m][n],而是dp[m][ * ]数组中的最大值,我们在循环中可以用mx来保存每一轮dp中的最大值,因为反正pre数组中需要存储上一次dp[i][i…j-1]的最大值。

代码实现:

#include <algorithm>
#define ll long long
#define mem(f, x) memset(f, x, sizeof(f) )
#define INF 0x3f3f3f3f
#define pii pair<int,int>
#define mk(x,y) make_pair(x,y)
#define fi first
#define se second
#define pk push_back
using namespace std;
const int M = 105;
const int N = 1e6+5;
int a[N], dp[N], pre[N];
int m, n;

int Max( int x, int y ){
   
    return x>y ? x:y;
}

int main( ){
   
    while( scanf( "%d %d", &m, &n ) != EOF ){
   
        for( int i = 1; i <= n; i++ ){
   
            scanf( "%d", a+i );
            dp[i] = pre[i] = 0;
        }
        dp[0] = pre[0] = 0;
        int mx = 0;
        for( int i = 1; i <= m; i++ ){
   
            mx = -INF;
            for( int j = i; j <= n; j++ ){
   
            	  //先用上一轮存的pre[j-1],即Max( dp[i-1][i-1...j-1]
                dp[j] = Max( dp[j-1], pre[j-1] ) + a[j];
                //修改成这一轮的Max( dp[i][i...j-1] ),
                //亦即为相对于下一轮的Max( dp[i-1][i-1...j-1] )
                //注意由于mx存的是上一个j留下的,因此要存在pre[j-1]中
                pre[j-1] = mx; 
                //得到当下dp[i][i..j]的最大值方便修改pre
                mx = Max( mx, dp[j] );
            }
        }
        printf( "%d\n", mx );
    }
    return 0;
}

Ch2. 序列增减问题

No2.1 LIS(最长上升子序列)

基础模型: 给定一个长度为n的序列,求这一序列中最长上升子序列的长度,所谓上升序列即一个严格递增的序列。

思路:
(1. 暴力解法: 假设dp[i]表示以a[i]结尾的LIS长度,则
    dp[i] = Max( dp[j] )+1其中 j 满足a[j] < a[i]且 1 <= j <= i,dp[i]的由来分两种情况,一种是在i之前的最大LIS中再加上a[i],即1 <= j < i;另一种是a[i]自己,即 j==i,因此dp[i]初始化为1。这里代码不作详述,时间复杂度为O(n^2)。

(2. 贪心+二分解法:新建一个数组dp保存LIS中的子序列(并非真正的LIS子序列中的数,只能解出最终的LIS的数值),用mx保存a[i]之前的LIS长度,则讨论a[i]时分两种情况,一种a[i] > dp[mx],则dp[mx]可将a[i]加入后作进一步增长;另一种情况是a[i] <= dp[mx],mx数值不变,将a[i]插入dp数组中且要保持dp数组仍然有序,插入a[i]为LIS的后续从a[i]处得到更长的LIS保留了一种可能,而保持dp数组有序则未改变原LIS中的相对大小,因此对原LIS的进一步增长无影响。综上,最终可得出的LIS长度mx,时间复杂度为O( nlogn)。
具体的证明过程可参考:https://blog.csdn.net/lxt_Lucia/article/details/81206439 中的贪心+二分解法
  最后一个很关键的点是LIS题目的拓展,例如最长下降子序列、最长不下降子序列、最长不上升子序列,这里看只要简单的改变符号以及二分查找中L、R变化时等号该加在哪里的问题。上升找第一个不小于a[i]的dp位置;不下降找第一个大于a[i]的dp位置;下降找第一个不大于a[i]的dp位置;不上升找第一个小于a[i]的位置。若在写二分时实在弄不清该返回L、R还是mid,可以令一个ret保存符合查找条件的位置,每次找到符合条件的位置时不仅修改L(或R)的值,另外用ret保存这一位置,最终返回ret即可。

#include <bits/stdc++.h>
#define ll long long
#define mem( f, x ) memset( f, x, sizeof( f ) )
#define INF 0x3f3f3f3f
#define pii pair<int, int>
#define mk( x, y ) make_pair( x, y )
#define fi first
#define se second
#define pk push_back
using namespace std;
const int M = 1e5 + 5;
const int 
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值