单调队列优化 DP

单调队列优化 DP

回忆单调队列的作用,\(O(n)\) 求出每一个大小为 \(K\) 的窗口中的最大、最小值。

以最大值为例,我们可以得到如下 DP 转移方程:

\[dp[i]=\max(val[j])+base[i],i-j\leq K \]

其中 \(base[i]\) 是一个仅与 \(i\) 有关的式子,不受 \(j\) 影响,且可以预处理得到;而 \(val[j]\) 则是一个仅与 \(j\) 以及 \(dp[j]\) 有关的式子,不受 \(i\) 影响。
因此我们可以维护一个 \(val[j]\) 的单调队列,每次从队列中选出最大值(通常是队头)更新 \(dp[i]\),这个过程就是单调队列优化 DP。

显然,单调队列能优化的线段树肯定能优化,但是单调队列的时间复杂度少一个 \(\log\)


P2627 [USACO11OPEN] Mowing the Lawn G

题目大意:

\(N\) 个数,第 \(i\) 个数的值为 \(a_i\),需要在 \(N\) 个数中选择若干个数,使得每个数的值 \(a_i\) 之和最大,但是不能连续选择 \(K\) 个数。(即对于任意编号为 \(i\) 的数,不能选择编号为 \([i-K+1, i]\) 中的所有数)


\(dp[i][0/1]\) 表示序列选到第 \(i\) 个数时,不选/选第 \(i\) 个数能获得的最大值。

显然可以得出以下转移方程:

\[dp[i][0]=\max(dp[i-1][0],dp[i-1][1]) \]

\[dp[i][1]=\max_{i-K \le j < i}\{dp[j][0]+\sum_{t=j+1}^{i}a_t\} \]

可以预处理出前缀和优化掉一重循环,转移方程变成:

\[dp[i][1]=\max_{i-K \le j < i}\{dp[j][0]+Sum[i]-Sum[j]\} \]

可以将 \(Sum[i]\) 提出,得到:

\[dp[i][1]=\max_{i-K \le j < i}\{dp[j][0]-Sum[j]\}+Sum[i] \]

这样就得到了全文开头所说的单调队列形式的 DP 式子,那么用一个单调队列来维护长度为 \(K\) 的区间中 \(dp[j][0]-Sum[j]\) 的最大值即可。

单调队列优化时存最大值的下标,DP 更新前保证队列长度不大于 \(K\),DP 更新后如果当前下标的 DP 值大于队列中尾元素下标的 DP 值,就把尾元素删除,直至队列为空或者小于尾元素下标的 DP 值(保证队列单调递减)。最后加入当前下标。


朴素前缀和优化 DP 代码:

#include<bits/stdc++.h>
using namespace std;
#define ll long long

ll sum[100005];
ll dp[100005][2]; // dp[i][0/1] 表示到第 i 头奶牛时不选/选第 i 头奶牛的最大效率

int main(){
    ios::sync_with_stdio(0); cin.tie(0); cout.tie(0);
    int n, k; cin>>n>>k;
    for(int i=1; i<=n; i++){
        ll x; cin>>x;
        sum[i] = sum[i-1]+x;
    }
    for(int i=1; i<=n; i++){
        dp[i][0] = max(dp[i-1][0], dp[i-1][1]);
        for(int j=max(0, i-k); j<i; j++){
            dp[i][1] = max(dp[i][1], dp[j][0]+sum[i]-sum[j]);
        }
    }
    cout<<max(dp[n][0], dp[n][1]);
    return 0;
}

正解加上单调队列优化 DP 后的代码:

#include<bits/stdc++.h>
using namespace std;
#define DEBUG(a) cout<<"Dline[ "<<__LINE__<<" ]: "<<(a)<<"\n";
#define ll long long
const int N = 1e5+5;

ll sum[N];
ll dp[N][2]; // dp[i][0/1] 表示到第 i 头奶牛时不选/选第 i 头奶牛的最大效率

struct qwqque{
    int q[N], sz, head, tail;
    qwqque(int k){
        memset(q, 0, sizeof(q));
        head = tail = 1; // 一开始的范围内不选可以看作从0转移而来
        sz = k;
    }
    void upd(int x){
        while(head<=tail && x-q[head]>sz)
            head++;
    }
    void push(int x){
        while(head<=tail && dp[x][0]-sum[x]>=dp[q[tail]][0]-sum[q[tail]])
            tail--;
        q[++tail] = x;
    }
    ll qmax(){
        return dp[q[head]][0]-sum[q[head]];
    }
};

int main(){
    ios::sync_with_stdio(0); cin.tie(0); cout.tie(0);
    int n, k; cin>>n>>k;
    for(int i=1; i<=n; i++){
        ll x; cin>>x;
        sum[i] = sum[i-1]+x;
    }
    qwqque Q(k);
    for(int i=1; i<=n; i++){
        dp[i][0] = max(dp[i-1][0], dp[i-1][1]);
        Q.upd(i);
        dp[i][1] = Q.qmax()+sum[i];
        Q.push(i);
    }
    cout<<max(dp[n][0], dp[n][1]);
    return 0;
}

P2569 [SCOI2010] 股票交易

题目大意:

一共有 \(T\) 天,第 \(i\) 天最多买入 \(as_i\) 股股票,卖出 \(bs_i\) 股股票,买入每股股票需要花费 \(ap_i\) 元,卖出每股股票需要花费 \(bp_i\) 元。手持股票数目不能超过 \(maxp\) 股。股票交易的天数间隔至少为 \(w\)。即如果第 \(i\) 天进行了股票交易,那么第 \([i+1, i+w]\) 天不得进行股票交易。

\(0 \le w < T \le 2000, 1 \le maxp \le 2000, 1\le bp_i \le ap_i \le 1000, 1 \le as_i \le bs_i \le maxp\)


观察到 \(T, maxp \le 2000\),可以有两维 \(dp\)。设 \(dp[i][j]\) 表示前 \(i\) 天拥有股票数为 \(j\) 可以获得的最大钱数。

每天可以不进行交易,买入股票,卖出股票。因为后两种操作受 \(w\) 限制,会漏掉“虚空买”的操作状态。所以一共有 4 种状态。注意初始值应该全部先赋 \(-\infty\)

1.虚空买(前面没有进行过股票交易操作)

\(j\) 股股票可得转移方程:

\[dp[i][j] = -ap_i \times j \quad (0 \le j \le as_i) \]

2.不进行交易

\(i\) 天拥有 \(j\) 股股票的值即为第 \(i-1\) 天拥有 \(j\) 股股票的值,因为 \(i-1\) 天前的最大值都已经转移到了第 \(i-1\) 天:

\[dp[i][j] = \max(dp[i][j], dp[i-1][j]) \quad (0 \le j \le maxp) \]

3.买入股票

\(i\) 天进行交易的操作应该从第 \(i-w-1\) 天转移而来(可以举例子理解):

\[dp[i][j] = \max(dp[i][j], dp[i-w-1][k] - (j-k) \times ap_i) \quad (j-as_i \le k < j) \]

注意 \(i-w-1\) 可能不大于 \(0\),此时判断该种情况无法转移。所以最开始第 \(i-w-1\) 天前也必定有“虚空买”这一交易,因此可以从小到大转移。

4.卖出股票

同理可得转移方程:

\[dp[i][j] = \max(dp[i][j], dp[i-w-1][k] + (k-j) \times bp_i) \quad (j < k \le j+bs_i) \]


因为第 3 种操作和第 4 种操作时间复杂度是 \(O(n^3)\) 的,考虑如何优化掉一维。

同上一题,以第 3 种操作为例,先分离 \(k\)\(j\)

\[dp[i][j] = \max\{dp[i-w-1][k]+k \times ap_i - j \times ap_i\} \]

对于每一个 \(k\) 得到的 \(dp\) 值都减去了 \(j \times ap_i\),可以将其提出得:

\[dp[i][j] = \max\{dp[i-w-1][k]+k \times ap_i \} - j \times ap_i \]

这时 \(\max\) 里的值只与 \(k\) 有关,可以单调队列优化,对于每一个 \(j\),从 \(j-q[head]\) 长度不超过 \(as_i\)\(\max\) 里的值转移而来。

因为对于第 3 种操作,单调队列里存的下标应该小于 \(j\),即 \(k < j\),所以应从小到大转移而来,对于第 4 种操作,单调队列里存的下标应该大于 \(j\),即 \(k > j\),所以应从大到小转移而来。


朴素 DP 代码:

#include<bits/stdc++.h>
using namespace std;
#define DEBUG(a) cout<<"Dline[ "<<__LINE__<<" ]: "<<(a)<<"\n";
#define ll long long
const int N = 2005;

int inp[N], outp[N], ins[N], outs[N];
int dp[N][N]; // dp[i][j] 表示前 i 天持有股票数为 j 已获得的最多钱
int main(){
    ios::sync_with_stdio(0); cin.tie(0); cout.tie(0);
    int T, maxp, w; cin>>T>>maxp>>w;
    for(int i=1; i<=T; i++)
        cin>>inp[i]>>outp[i]>>ins[i]>>outs[i];
    memset(dp, 128, sizeof(dp)); // 按位赋 128 即为 -INF
    for(int i=1; i<=T; i++){
        // 凭空买(无 w 限制)
        for(int j=0; j<=ins[i]; j++)
            dp[i][j] = -inp[i]*j;
        // 不买也不卖(从上一天转移来)
        for(int j=0; j<=maxp; j++)
            dp[i][j] = max(dp[i][j], dp[i-1][j]);
        if(i<=w)
            continue; // 可从 i-w-1 转移而来
        // 买入
        for(int j=0; j<=maxp; j++){
            for(int k=max(0, j-ins[i]); k<j; k++)
                dp[i][j] = max(dp[i][j], dp[i-w-1][k]-(j-k)*inp[i]);
        }
        // 卖出
        for(int j=0; j<=maxp; j++){
            for(int k=min(maxp, j+outs[i]); k>j; k--)
                dp[i][j] = max(dp[i][j], dp[i-w-1][k]+(k-j)*outp[i]);
        }
    }
    int ans = 0;
    for(int i=0; i<=maxp; i++)
        ans = max(ans, dp[T][i]);
    cout<<ans;
    return 0;
}

单调队列优化后代码:

#include<bits/stdc++.h>
using namespace std;
#define DEBUG(a) cout<<"Dline[ "<<__LINE__<<" ]: "<<(a)<<"\n";
#define ll long long
const int N = 2005;

int inp[N], outp[N], ins[N], outs[N];
int dp[N][N]; // dp[i][j] 表示前 i 天持有股票数为 j 已获得的最多钱

int head, tail, q[N]; // 单调队列

int main(){
    ios::sync_with_stdio(0); cin.tie(0); cout.tie(0);
    int T, maxp, w; cin>>T>>maxp>>w;
    for(int i=1; i<=T; i++)
        cin>>inp[i]>>outp[i]>>ins[i]>>outs[i];
    memset(dp, 128, sizeof(dp)); // 按位赋 128 即为 -INF
    for(int i=1; i<=T; i++){
        // 凭空买(无 w 限制)
        for(int j=0; j<=ins[i]; j++)
            dp[i][j] = -inp[i]*j;
        // 不买也不卖(从上一天转移来)
        for(int j=0; j<=maxp; j++)
            dp[i][j] = max(dp[i][j], dp[i-1][j]);
        if(i<=w)
            continue; // 需可从 i-w-1 转移而来
        // 买入
        head = 1, tail = 0;
        for(int j=0; j<=maxp; j++){
            while(head<=tail && j-q[head]>ins[i])
                head++;
            if(head<=tail)
                dp[i][j] = max(dp[i][j], dp[i-w-1][q[head]]+q[head]*inp[i]-j*inp[i]);
            while(head<=tail && dp[i-w-1][q[tail]]+q[tail]*inp[i]<=dp[i-w-1][j]+j*inp[i])
                tail--;
            q[++tail] = j;
        }
        // 卖出
        head = 1, tail = 0;
        for(int j=maxp; j>=0; j--){
            while(head<=tail && q[head]-j>outs[i])
                head++;
            if(head<=tail)
                dp[i][j] = max(dp[i][j], dp[i-w-1][q[head]]+q[head]*outp[i]-j*outp[i]);
            while(head<=tail && dp[i-w-1][q[tail]]+q[tail]*outp[i]<=dp[i-w-1][j]+j*outp[i])
                tail--;
            q[++tail] = j;
        }
    }
    int ans = 0;
    for(int i=0; i<=maxp; i++)
        ans = max(ans, dp[T][i]);
    cout<<ans;
    // 这里也可以是直接输出 dp[T][0],因为如果有剩余就不如当初少买点
    return 0;
}

CF939F Cutlet

待补。

posted @ 2024-09-11 21:37  FlyPancake  阅读(273)  评论(2)    收藏  举报
// music