动态规划之环形处理

动态规划之环形处理

前言

在许多环形结构的问题中,我们都能通过枚举法,选择一个位置把环断开,变成线性结构从而进行求解,但是往往这种枚举的方式时间复杂度比较大,所以在动态规划问题中往往用二次DP法断环为链法来避免枚举

二次DP法

二次DP法一般适用于断点位置固定的题目,第一次DP在任意位置将环断开,第二次DP通过适当的条件和赋值,保证计算的状态等价于把断开的位置强制相连。下面我们用几道题目去感受一下。

通俗的讲这类题目环形比线形多了一种状态。

例题一:

休息时间

在这里插入图片描述

分析:

很显然这是一个环形问题,并且断点的位置是固定的,即前一天的夜里第 N N N点(因为这个星球只有 N N N个小时)和这一天的早上零点是相连的。那我们不妨将问题简化一下,首先只考虑不相连的情况即每天的第一个小时都是熟睡的状态,那么这个星球这一天就是线性的,从第 1 1 1个小时开始到第 N N N个小时结束。接下来我们就来分析DP三要素:

1.首先阶段:因为简化后的问题是线性的,并且时间是不断递增的,所以可虑时间作为阶段,用 d p [ i ] dp[i] dp[i]表示在前 i i i个小时内恢复的体力是多少。

2.考虑状态:因为恢复的体力还与休息的时间有关,即需要休息够 B B B小时,那么就可以再增加一个维度表示 d p [ i ] [ j ] dp[i][j] dp[i][j] i i i个小时休息了 j j j个小时的恢复最大精力。但因为题目上说每段的第一个小时不能恢复体力,所以我们像下一个阶段转移的时候也需要判断上一个阶段它到底是休息了没休息,那么就需要增加一个维度 d p [ i ] [ j ] [ 1 ] dp[i][j][1] dp[i][j][1]表示前 i i i个小时休息了 j j j个小时并且第 i i i个小时正在休息累计恢复体力的最大值,而 d p [ i ] [ j ] [ 0 ] dp[i][j][0] dp[i][j][0]表示前 i i i个小时休息了 j j j个小时并且第 i i i个小时没有休息的最大值。

3.决策(状态转移):

第一次dp(第一个小时在熟睡但未获得体力):

通过上述分析,转移方程应该比较好写:

f [ i , j , 0 ] = m a x ( f [ i − 1 ] [ j ] [ 0 ] , f [ i − 1 ] [ j ] [ 1 ] ) f[i,j,0]=max(f[i-1][j][0],f[i-1][j][1]) f[i,j,0]=max(f[i1][j][0],f[i1][j][1])

因为第 i i i个小时没有休息,那么第 i i i个小时的状态就跟第 i − 1 i-1 i1个小时的状态完全一样

f [ i , j , 1 ] = m a x ( f [ i − 1 ] [ j − 1 ] [ 0 ] , f [ i − 1 ] [ j − 1 ] [ 1 ] + u [ i ] ) f[i,j,1]=max(f[i-1][j-1][0],f[i-1][j-1][1]+u[i]) f[i,j,1]=max(f[i1][j1][0],f[i1][j1][1]+u[i])

因为第 i i i个小时休息了,所以就要考虑它是否是这一段的第一个小时,就有了上述的转移方程。

上面的情况考虑的就是线性的情况,即第一个小时一定是在休息的情况,那么我们的目标为 m a x ( f [ N , B , 0 ] , f [ N , B , 1 ] ) max(f[N,B,0],f[N,B,1]) max(f[N,B,0],f[N,B,1])

第二次dp(第一个小时在熟睡但已获得体力)

到目前为止我们解决的线性问题仅比环形问题少一种情况,即第一个小时在熟睡并且获得了体力。那该怎么解决呢,我们就可以通过附加条件将其强制连接起来。如果第一个小时想获得体力,那么前一天的第 N N N个小时一定在熟睡,这就是我们所说的附加条件。那么我们就只需要将初值修改一下就可以了

初值: f [ 1 , 1 , 1 ] = u 1 f[1,1,1]=u_1 f[1,1,1]=u1,其余为负无穷

目标: f [ N , B , 1 ] f[N,B,1] f[N,B,1]

本题的解法本质上是把问题拆成了两部分,无论是哪一个部分因为第 N N N个小时和第一个小时之间有特殊的关系,所以我们可以把环拆开,用线性DP计算。

代码:

注意这道题数据范围较大,三维数组会爆内存,所以这种情况一般可以用滚动数组进行优化。

#include <iostream>
#include <cstring>
#include <algorithm>

using namespace std;
using LL = long long;

const int N = 4000;
LL f[2][N][2], w[N];
int n, m;

int main()
{
    cin >> n >> m;
    for(int i = 1; i <= n; i ++) scanf("%lld", &w[i]);

    //第n小时不睡觉
    memset(f, -0x3f, sizeof f);//初始化为负无穷
    f[1][0][0] = f[1][1][1] = 0;
    for(int i = 2; i <= n; i ++)
        for(int j = 0; j <= min(i, m); j ++)
        {
            f[i & 1][j][0] = max(f[i-1 & 1][j][1], f[i-1 & 1][j][0]);
            if(j >= 1) f[i & 1][j][1] = max(f[i-1 & 1][j-1][1] + w[i], f[i-1 & 1][j-1][0]);
        }
    LL ans = f[n & 1][m][0];

    //第n小时睡觉
    memset(f, -0x3f, sizeof f);//初始化为负无穷
    f[1][0][0] = 0; f[1][1][1] = w[1];
    for(int i = 2; i <= n; i ++)
        for(int j = 0; j <= min(i, m); j ++)
        {
            f[i & 1][j][0] = max(f[i-1 & 1][j][1], f[i-1 & 1][j][0]);
            if(j >= 1) f[i & 1][j][1] = max(f[i-1 & 1][j-1][1] + w[i], f[i-1 & 1][j-1][0]);
        }
    ans = max(ans, f[n & 1][m][1]);

    cout << ans << endl;

    return 0;
}


类似题目(重点感受思路)

断环为链法(来源:hungry1234)

思路:将一个环复制一倍成为一条链,最终答案为 w [ i ]   w [ i + n ] w[i]~w[i+n] w[i] w[i+n]一段上的结果,这种往往适用于断点之间没有特殊关系的,无法通过增加条件,强制连接。

P1880 [NOI1995] 石子合并

如果是一条链,考虑区间dp, d p [ i ] [ j ] dp[i][j] dp[i][j]指i~j一段的值,每次枚举中间点来得到答案

因为是一个环,将w复制到原来的后面做dp,最后答案是 m a x ( d p [ i ] [ i + n ] ) max(dp[i][i+n]) max(dp[i][i+n])

环路运输

复制处理环上问题,然后可以发现:

dp[i]=max_{j=i-len}^{j<i}(w[j]+i-j)

就是

dp[i]=max_{j=i-len}^{j<i}(w[j]-j)+i

用单调队列维护每一段 w [ j ] − j w[j]-j w[j]j的值,最后答案就是 队 首 的 值 + w [ i ] + i 队首的值+w[i]+i +w[i]+i

q.push(make_pair(1,w[i]));
for(int i=2;i<=a*2;i++){
		while(!q.empty()&&q.front().first+len<=i) q.pop_front();
		dp[i]=q.front().second+i+w[i];
		ans=max(ans,dp[i]);
		while(!q.empty()&&q.back().second<=w[i]-i) q.pop_back();
		q.push_back(make_pair(i,w[i]-i));
 	}

其他方法:取模法

435. 传球游戏

  • 4
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值