单调队列优化DP


单调队列优化DP

单调队列优化的经典问题是滑动窗口,而滑动窗口本质上解决的问题都是移动区间内的最值问题,所以碰到此类问题我们都可以用单调队列进行优化。

纯滑动窗口问题

题目链接:154. 滑动窗口 - AcWing题库

#include <bits/stdc++.h>

using namespace std;

const int N = 1000010;
int a[N], q[N]; // q为单调队列,需要注意,队列中存放的是数组下标。

int main()
{
    int n, k;
    cin >> n >> k;

    for (int i = 0; i < n; i ++ ) cin >> a[i];

    int hh = 0, tt = -1; // hh为队列头,tt为队列尾。tt < hh的原因是防止在刚进入循环时判断出错,所以要将tt小于hh,从而达到先入一个数的情况。
    for (int i = 0; i < n; i ++ ){ // 模拟队尾碰到新数a[i]的过程
        if (hh <= tt && i - k + 1 > q[hh]) hh ++ ; // 单调队列长度大于k,队头出队,加上hh <= tt的原因主要是防止初始状态时tt < hh。

        while (hh <= tt && a[q[tt]] >= a[i]) tt --; // 碰到新数a[i]后,若当前队尾大于a[i],则放入后不满足单调递减
        q[ ++ tt] = i; // 新元素入队

        if (i >= k - 1) printf("%d ", a[q[hh]]); // 用单调队列维护一个单调递减的区间,故每次窗口内最大值一定是队头
    }

    printf("\n");
	
    // 以下同上,改为维护单调递增即可。
    hh = 0, tt = -1;
    for (int i = 0; i < n; i ++ ){
        if (hh <= tt && i - k + 1 > q[hh]) hh ++ ;

        while (hh <= tt && a[q[tt]] <= a[i]) tt --;
        q[ ++ tt] = i;

        if (i >= k - 1) printf("%d ", a[q[hh]]);
    }
    printf("\n");

    return 0;
}

135. 最大子序和 - AcWing题库

考虑维护一个单调递增的前缀和的滑动窗口。

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

const int maxn = 300010;

int n, m;
int s[maxn], q[maxn];

int main () {
    ios::sync_with_stdio(false);
    cin.tie(nullptr);
    
    cin >> n >> m;
    for (int i = 1; i <= n; i ++ ) {
        cin >> s[i];
        s[i] += s[i - 1]; // 前缀和
    }
    
    int res = -0x3f3f3f3f; // 长度至少为1
    int hh = 0, tt = 0;
    for (int i = 1; i <= n; i ++ ) { // 枚举右端点
        if (q[hh] < i - m) hh ++ ; // 区间长度如果大于m,就不可以
        res = max (res, s[i] - s[q[hh]]); // 更新最值
        while (hh <= tt && s[i] <= s[q[tt]]) tt -- ; // 滑动窗口开滑
        q[ ++ tt ] = i; // 入队
    }
    
    cout << res << endl;
}

1088. 旅行问题 - AcWing题库

首先我们要考虑将环形问题化为一条区间。

顺时针情况

用p[i] 表示

如图,对于顺时针遍历,如果我们可以满足条件,从3走到3,那么对于所有的k,都存在s[k] - s[i] >= 0,即对图中情况,都满足s[k] - s[2] >= 0,即s[k] >= s[2]。所以我们只需要找到一个长度为n的滑动窗口,满足窗口内的最小值满足s[min] >= s[2],所以该题就转化为了一个区间最值问题。

同理逆时针

逆时针情况

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

#define int long long
const int maxn = 2e6 + 10;

int n;
int p[maxn], d[maxn];
int s[maxn];
int q[maxn];
bool st[maxn];

signed main () {
    cin >> n;
    for (int i = 1; i <= n; i ++ ) {
        cin >> p[i] >> d[i];
    }
    
    for (int i = 1; i <= n; i ++ ) s[i] = s[i + n] = p[i] - d[i];
    for (int i = 1; i <= 2 * n; i ++ ) s[i] += s[i - 1];

    int hh = 0, tt = -1;
    for (int i = 2 * n; i >= 1; i -- ) {
         if (hh <= tt && q[hh] > i + n - 1) hh ++ ;
         while (hh <= tt && s[q[tt]] >= s[i]) tt -- ;
         q[ ++ tt] = i;
         if (i <= n && s[q[hh]] >= s[i - 1]) st[i] = 1;
    }
    
    // 逆时针
    hh = 0, tt = -1;
    d[0] = d[n];
    for (int i = 1; i <= n; i ++ ) s[i] = s[i + n] = p[i] - d[i - 1];
    for (int i = 2 * n; i >= 0; i -- ) s[i] += s[i + 1];
    for (int i = 1; i <= 2 * n; i ++ ) {
         if (hh <= tt && q[hh] < i - n + 1) hh ++ ;
         while (hh <= tt && s[q[tt]] >= s[i]) tt -- ;
         q[ ++ tt] = i;
         if (i > n && s[q[hh]] >= s[i + 1]) st[i - n] = 1;
    }
    
    for (int i = 1; i <= n; i ++ ) {
        if (st[i]) cout << "TAK" << endl;
        else cout << "NIE" << endl;
    }
}

滑动窗口+DP

1089. 烽火传递 - AcWing题库

y总的状态计算

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

const int maxn = 1e6 + 10, inf = -0x3f3f3f3f;
int n, m, l, r;
int a[maxn], f[maxn], q[maxn << 1];

int main () {
    cin >> n >> m;
    for (int i = 1; i <= n; i ++ ) cin >> a[i];
    int hh = 0, tt = 0;
    for (int i = 1; i <= n; i ++ ) {
        if (q[hh] < i - m) hh ++ ;
        f[i] = f[q[hh]] + a[i];
        while (hh <= tt && f[q[tt]] >= f[i]) tt -- ;
        q[ ++ tt] = i;
    }
    
    int res = 1e9;
    for (int i = n - m + 1; i <= n; i ++ ) { // 答案一定在最后一段区间内选出
        res = min(res, f[i]);
    }
    cout << res << endl;
}

1090. 绿色通道 - AcWing题库

加入二分的做法。

二分

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

const int maxn = 500100;

int n, m;
int w[maxn];
int q[maxn], f[maxn];

bool check (int x) {
    int hh = 0, tt = 0;
    for (int i = 1; i <= n; i ++ ) {
        if (q[hh] < i - x - 1) hh ++ ;
        f[i] = f[q[hh]] + w[i];
        while (hh <= tt && f[q[tt]] >= f[i]) tt -- ;
        q[ ++ tt ] = i;
    }
    for (int i = n - x; i <= n; i ++ ) {
        if (f[i] <= m) return true;
    }
    return false;
}

int main () {
    cin >> n >> m;
    for (int i = 1; i <= n; i ++ ) cin >> w[i];
    
    int l = 0, r = n;
    while (l < r) {
        int mid = l + r >> 1;
        if (check(mid)) r = mid;
        else l = mid + 1;
    }
    
    cout << l << endl;
}

1087. 修剪草坪 - AcWing题库

经典问题修剪草坪,我们可以把问题转化为每k+1头牛里至少选择一头,每次选择k+1内的最小值,这样就转化成了滑动窗口的裸题。

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

#define int long long

const int maxn = 1000010;
int q[maxn], n, m;
int ans, f[maxn];

signed main () {
    cin >> n >> m;
    int alls = 0;
    for (int i = 1; i <= n; i ++ ) cin >> f[i], alls += f[i];
    
    int hh = 0, tt = 0;
    for (int i = 1; i <= n; i ++ ) {
        if (hh <= tt && i - q[hh] > m + 1) hh ++ ;
        f[i] += f[q[hh]];
        while (hh <= tt && f[q[tt]] >= f[i]) tt -- ;
        q[ ++ tt] = i;
    }
    
    for (int i = n - m; i <= n; i ++ ) ans = max(ans, alls - f[i]);
    cout << ans << endl;
}

二维滑动窗口

1091. 理想的正方形 - AcWing题库

二位滑动窗口

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

using namespace std;
//#define int long long

const int maxn = 1010, inf = 1e9;

int n, m, k;
int w[maxn][maxn];
int row_min[maxn][maxn], row_max[maxn][maxn];
int q[maxn];

void get_min(int a[], int b[], int tot) {
    int hh = 0, tt = -1;
    for (int i = 1; i <= tot; i ++ ) {
        if (hh <= tt && q[hh] <= i - k) hh ++ ;
        while (hh <= tt && a[q[tt]] >= a[i]) tt -- ;
        q[ ++ tt] = i;
        b[i] = a[q[hh]];
    }
}

void get_max(int a[], int b[], int tot) {
    int hh = 0, tt = -1;
    for (int i = 1; i <= tot; i ++ ) {
        if (hh <= tt && q[hh] <= i - k) hh ++ ;
        while (hh <= tt && a[q[tt]] <= a[i]) tt -- ;
        q[ ++ tt] = i;
        b[i] = a[q[hh]];
    }
}


signed main () {
    ios::sync_with_stdio(false);
    cin.tie(0);
    
    cin >> n >> m >> k;
    for (int i = 1; i <= n; i ++ ) {
        for (int j = 1; j <= m; j ++ ) {
            cin >> w[i][j];
        }
    }
    
    for (int i = 1; i <= n; i ++ ) {
        get_min(w[i], row_min[i], m);
        get_max(w[i], row_max[i], m);
    }
    
    int res = inf;
    int a[maxn], b[maxn], c[maxn];
    for (int i = k; i <= m; i ++ ) {
        for (int j = 1; j <= n; j ++ ) a[j] = row_min[j][i];
        get_min(a, b, n);
        
        for (int j = 1; j <= n; j ++ ) a[j] = row_max[j][i];
        get_max(a, c, n);
        
        for (int j = k; j <= n; j ++ ) res = min(res, c[j] - b[j]);
    }
    
    cout << res << endl;
}

文章作者: Cedeat
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 Cedeat !
  目录