动态规划之环形处理
前言
在许多环形结构的问题中,我们都能通过枚举法,选择一个位置把环断开,变成线性结构从而进行求解,但是往往这种枚举的方式时间复杂度比较大,所以在动态规划问题中往往用二次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[i−1][j][0],f[i−1][j][1])
因为第 i i i个小时没有休息,那么第 i i i个小时的状态就跟第 i − 1 i-1 i−1个小时的状态完全一样
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[i−1][j−1][0],f[i−1][j−1][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]一段上的结果,这种往往适用于断点之间没有特殊关系的,无法通过增加条件,强制连接。
如果是一条链,考虑区间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])
复制处理环上问题,然后可以发现:
就是
用单调队列维护每一段 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));
}
其他方法:取模法