1D/1D的动态规划/单调队列优化

我仅一届平凡人
对于某些dp的状态转移方程,我们可以写成一种形式:
F [ i ] = min ⁡ l ( i ) ≤ j ≤ r ( i ) { F [ j ] + v a l ( i , j ) } F[i] = \min_{l(i)\leq j \leq r(i)} \{F[j] + val(i, j)\} F[i]=minl(i)jr(i){F[j]+val(i,j)}
在这种模型中,j取值的范围都是关于i的一次单调函数 v a l ( i , j ) val(i, j) val(i,j)关于i,j的多项式函数。我们将其称为1D/1D的动态规划。在上述模型中,如果多项式 v a l ( x , y ) val(x, y) val(x,y)的每一项仅与i或j中的一项有关,我们可以使用单调队列进行优化。
李煜东蓝皮书上,给出三个例子。
例1:

给定一个N长度的序列(有负数),选择一个长度不超过M的子段,使子段的所有数的和最大。

我处理出前缀和 S [ i ] S[i] S[i]答案可以表述成:
a n s = max ⁡ 1 ≤ i ≤ N { S [ i ] − min ⁡ i − M ≤ j ≤ i − 1 { S [ j ] } } ans = \max_{1 \le i \le N}\{S[i] - \min_{i-M\le j \le i-1} \{ S[j]\} \} ans=max1iN{S[i]miniMji1{S[j]}}
此时我们发现它符合上述模型。那么,我们是怎么通过单调队列进行优化的呢?我们不妨设两个位置j,k并满足:k < j < i。并且 S [ k ] ≥ S [ j ] S[k]\ge S[j] S[k]S[j]这时,我们可以知道:k绝对不可能是最优解。这不仅取决于j的前缀和比k小而j更优。还因为j的位置比k靠前(长度更短),我们在后期有更多的空间选择。此时我们将所有决策按照遍历顺序,维护成一个单调递增的队列。可以知道:最优子结构由且仅由队列中的决策产生。
蓝皮书给出的代码:

int l = 1, r = 1;
q[i] = 0;
for (int i = 1; i <= n; i++)
{
	while(l <= r && q[l] < i - m) l++;//弹出过时决策
	ans = max(ans, sum[i] - sum[q[l]]);//当前最优决策即位队头的值
	while(l <= r && sum[q[r]] >= sum[i]) r—-;//删除队尾决策保证单调性
	q[r++] = i;
}

这个例子其实仅是一个单调队列的题目,不怎么涉及dp。
例2:POJ 1821 Fence
我们设 F [ i , j ] F[i, j] F[i,j]为考虑前i个工匠,刷前j个木板,能获得的最大报酬。我们可以写成转移方程:
F [ i , j ] = max ⁡ j − L i ≤ k ≤ S i − 1 { F [ i − 1 , k ] , P i ∗ ( j − k ) } F[i, j] = \max_{j-L_i \le k \le S_i - 1} \{F[i-1, k], P_i *(j - k)\} F[i,j]=maxjLikSi1{F[i1,k],Pi(jk)}
这里注意摒弃i的干扰,这里的i只是表示阶段,不参与当前阶段的决策。这里的k是模型中的j,这里的j是模型中的i。
对与每一个阶段,我们发现,随着j的递增, j − L i j-L_i jLi线性递增,且val函数符合模型中的定义,可以使用单调队列优化!这时,我们随意找出两个位置 k 1 , k 2 ( k 1 &lt; k 2 &lt; S i − 1 ) k_1,k_2(k_1 &lt; k_2 &lt; S_i-1) k1,k2(k1<k2<Si1) k 2 k_2 k2的决策优于 k 1 k_1 k1此时 k 1 k_1 k1为完全无用的决策。所以我们依靠转移方程维护一个递减的单调队列。
在这个题中,我们可以看到,首先对于每一个i阶段,我们初始化单调队列:

 int l = 1, r= 0;
for (int k = max(0, a[i].s - a[i].l); k <= a[i].s - 1; k++)
{
	while(l <= r && calc(i, q[r]) <= calc(i, k)) r--;
	q[++r] = k;
}

我们在 j − L i ≤ k ≤ S i − 1 j-L_i \le k \le S_i - 1 jLikSi1的范围内初始化单调队列。因为右端点是不变的。我们只有缩左端点就行了。

for (int j = 1; j <= n; j++)
{
	f[i][j] = max(f[i-1][j], f[i][j-1]);
	if (j >= a[i].s)
	{
		while(l <= r && q[l] < j - a[i].l) l++;
		if (l <= r) f[i][j] = max(f[i][j], calc(i, q[l]) + a[i].p * j);
	}
}

下面是完整ac代码:

#include <iostream>
#include <cstring>
#include <string>
#include <queue>
#include <vector>
#include <algorithm>
#include <cmath>
#include <cstdio>
using namespace std;
struct Node
{
    int l, p, s;
}a[110];
int f[110][16005], q[16005];
bool cmp(Node a, Node b)
{
    return a.s < b.s;
}
int calc(int i, int k)
{
    return f[i-1][k] - a[i].p *k;
}
int main()
{
    int n, m;
    cin >> n >> m;
    for (int i = 1; i <= m; i++)
        scanf("%d%d%d", &a[i].l, &a[i].p, &a[i].s);
    sort(a+1, a+m+1, cmp);
    for (int i = 1; i <= m; i++)
    {
        int l = 1, r= 0;
        for (int k = max(0, a[i].s - a[i].l); k <= a[i].s - 1; k++)
        {
            while(l <= r && calc(i, q[r]) <= calc(i, k)) r--;
            q[++r] = k;
        }
        for (int j = 1; j <= n; j++)
        {
            f[i][j] = max(f[i-1][j], f[i][j-1]);
            if (j >= a[i].s)
            {
                while(l <= r && q[l] < j - a[i].l) l++;
                if (l <= r) f[i][j] = max(f[i][j], calc(i, q[l]) + a[i].p * j);
            }
        }
    }
    printf("%d\n", f[m][n]);
    return 0;
}

例3:O(nm)解多重背包。
对于多重背包,我们处理朴素的计数,还可以通过二进制压缩进行优化,但是使用单调队列优化,我们可以讲复杂度衰减为O(nm)。
我们通过模拟朴素多重背包发现:在每个阶段,每次状态的转移的位置都是成倍的。例如。对于 p p p点,他只能转移到 p + V i , p + 2 ∗ V i , p + 3 ∗ V i p+V_i,p+2 * V_i, p+3*V_i p+Vi,p+2Vi,p+3Vi等,所以我们所有状态,按照% V i V_i Vi的余数分为等价类。对于每一个余数 u u u有状态转移方程:
F [ u + p ∗ V i ] = max ⁡ p − C i ≤ k ≤ p − 1 { F [ u + k ∗ V i ] + ( p − k ) ∗ W i } F[u+p*V_i]=\max_{p-C_i \le k \le p-1}\{ F[u +k*V_i] + (p-k)*W_i\} F[u+pVi]=maxpCikp1{F[u+kVi]+(pk)Wi}
同理符合上述模型。代码不想打了。。。直接看书吧。
回头学一波斜率优化就ok了。。。。。。。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值