[动态规划]单调队列优化DP

单调队列优化DP

最大子序列和

来源: https://www.acwing.com/problem/content/137/

题目描述

输入一个长度为 n n n 的整数序列,从中找出一段长度不超过 m m m 的连续子序列,使得子序列中所有数的和最大。

注意: 子序列的长度至少是 1 1 1

输入格式

第一行输入两个整数 n , m n,m n,m

第二行输入 n n n 个数,代表长度为 n n n 的整数序列。

同一行数之间用空格隔开。

输出格式

输出一个整数,代表该序列的最大子序和。

数据范围

1 ≤ n , m ≤ 300000 1≤n,m≤300000 1n,m300000

输入样例:
6 4
1 -3 5 1 -2 3
输出样例:
7

思路分析

使用滑动窗口优化DP

该题让求长度不超过 m 的最大子序列和,我们知道一段子序列的和可以用前缀和来快速求解,如下图所示

在这里插入图片描述

表达式包含两个变量,双重循环时间复杂度为 n 2 n^2 n2 ,显然不可接受。

求区间和最大值的常用线性方法是滑动窗口,可以固定区间左端点或右端点,然后另一个端点通过滑动窗口从区间内求得,枚举固定端点即可遍历整个序列

做法1

我们可以固定右端点 j ,因为 ji 的右边,因此枚举到 j 的时候,s[i-1] 的最值已经计算出,正序遍历即可

在这里插入图片描述

然后明确滑动窗口的一些关键点:

  • 区间长度: 不超过 m m m , 即[i-1, j-1]
  • 滑动窗口边界:队首下标 q[hh] < j - m
  • 滑动窗口求最小值
  • 边界情况:当枚举到第一个元素时,i-1 为 0,应该提前加入到队列中
#include <iostream>
#include <cstring>
#include <algorithm>

using namespace std;
typedef long long ll;
const int N = 300010;
int a[N];
ll s[N];
ll q[N];
int n, m;
ll res = -2e9;
int main() {
    scanf("%d%d", &n, &m);
    for (int i = 1; i <= n; i ++) scanf("%d", &a[i]), s[i] = s[i-1] + a[i];
    int hh = 0, tt = -1;
    // i 表示上面分析中的 j
    for (int i = 0; i <= n; i ++)
    {
        if (hh <= tt && q[hh] < i - m) hh ++;
        if (i) res = max(res, s[i] - s[q[hh]]); // 处理边界,第一次枚举时将 s[0] 加入队列中
        while(hh <= tt && s[q[tt]] > s[i]) tt --;
        q[++ tt] = i;
    }
    printf("%d\n", res);
    return 0;
}
做法2

固定左端点 i ,因为 ij 的右边,所以枚举 i-1 时,需要用到 s[j],但 s[j] 并未求出,因此倒序枚举就比较合适

在这里插入图片描述

然后明确滑动窗口的一些关键点:

  • 区间长度: 不超过 m m m , 即[i-1, j-1]
  • 滑动窗口边界:队首下标 q[hh] > j
  • 滑动窗口求最小值
  • 边界情况:实际枚举时 i 会从 n-1 开始, 但此时 j 没有被添加进队列,所以需要从 n 开始,但这一步不计算答案,也可以在队列初始化时进行
#include <iostream>
#include <cstring>
#include <algorithm>

using namespace std;
typedef long long ll;
const int N = 300010;
int a[N];
ll s[N];
ll q[N];
int n, m;
ll res = -2e9;

int main() {
    scanf("%d%d", &n, &m);
    for (int i = 1; i <= n; i ++) scanf("%d", &a[i]), s[i] = s[i-1] + a[i];
    int hh = 0, tt = -1;
    // 枚举时,用 i 表示上面分析的 i - 1
    for (int i = n; i >= 0; i --)
    {
        if (hh <= tt && q[hh] > i + m) hh ++;
        
        if (i < n) res = max(s[q[hh]] - s[i], res);
        while(hh <= tt && s[q[tt]] < s[i]) tt --;
        q[++ tt] = i;
        
    }
    printf("%d\n", res);
    return 0;
}

旅行问题

来源: https://www.acwing.com/problem/content/description/1090/

题目描述

John 打算驾驶一辆汽车周游一个环形公路。

公路上总共有 n n n 个车站,每站都有若干升汽油(有的站可能油量为零),每升油可以让汽车行驶一千米。

John 必须从某个车站出发,一直按顺时针(或逆时针)方向走遍所有的车站,并回到起点。

在一开始的时候,汽车内油量为零,John 每到一个车站就把该站所有的油都带上(起点站亦是如此),行驶过程中不能出现没有油的情况。

任务:判断以每个车站为起点能否按条件成功周游一周。

输入格式

第一行是一个整数 n n n,表示环形公路上的车站数;

接下来 n n n 行,每行两个整数 pi,dipi,di,分别表示表示第 i i i 号车站的存油量和第 i i i 号车站到 顺时针方向 下一站的距离。

输出格式

输出共 n n n 行,如果从第 i i i 号车站出发,一直按顺时针(或逆时针)方向行驶,能够成功周游一圈,则在第 ii 行输出 TAK,否则输出 NIE。

数据范围

3 ≤ n ≤ 1 0 6 3≤n≤10^6 3n106,
0 ≤ p i ≤ 2 × 1 0 9 0≤pi≤2×10^9 0pi2×109,
0 ≤ d i ≤ 2 × 1 0 9 0≤di≤2×10^9 0di2×109

输入样例:
5
3 1
1 2
5 2
0 1
5 4
输出样例:
TAK
NIE
TAK
NIE
TAK

思路分析

通过分析题意,需要对进行拆换成链。

在这里插入图片描述

然后先考虑顺时针,从 1 1 1 号点出发, 1 1 1 号点是否到达 2 2 2 号点等价于 oil[1] - dist[1] >= 0,到达 3 3 3 号点等价于 1 能到达 2 且 oil[1] + oil[2] - (dist[1] + dist[2]),我们能够发现需要统计 oil[i]-dist[i]的前缀和。复制两遍 oil[i]-dist[i],展成链状后求前缀和。依次类推,从1号点走一圈就等价于 s[1] >= s[0], s[2] >= s[0], ..., s[1+n]

在这里插入图片描述

等价于 min{s[j] - s[k]} >= 0 k = i-1, i, …, j-1

和上一题基本类似,但也稍有不同,固定值应选择 s[k] ,即枚举左端点,使用滑动窗口求得 s[j] 的最小值,此时可以选择倒序枚举。

为什么倒序呢?

按照上题的解释,正序枚举由于 j 在右边,在遍历 i − 1 i-1 i1 的时候,得不到右边区间内 j j j 的最小值,所以需要倒序遍历。但是对于本体而言,可以正序也可以倒序。但是此题中正序和倒序在本质上是相同的,因为多复制了一遍,正序遍历的前 n n n 个元素计算区间 j j j 的最值,后面在统计答案

为什么选择枚举 s[i-1] 呢?

因为如果选择枚举 s[j] 后,s[i-1] 需要通过滑动窗口求最大值,但是我们的区间定义的是从 i 开始到 j ,即起点是 i , min{s[j] - s[k]} >= 0 k = i-1, i, …, j-1, 固定左端点没有意义,但是对于逆时针来说需要这样枚举,二者是正好相反的

考虑逆时针,先令 dist[n] = dist[0] , 求前缀和或后缀和都可以,本质上和上一题基本一致,只不过需要反向考虑

在这里插入图片描述

在这里插入图片描述

以第二段为起点,向左延伸,即可得到反向的情况,适合枚举右端点,此时可采用正序遍历

做法一:

第一遍:

  • 区间长度: 不超过 m m m , 即[i-1, j-1]
  • 滑动窗口边界:队首下标 q[hh] < j - m
  • 滑动窗口求最小值
  • 边界情况:当枚举到第一个元素时,i-1 为 0,应该提前加入到队列中

第二遍:

  • 区间长度: 不超过 m m m , 即[i-1, j-1]
  • 滑动窗口边界:队首下标 q[hh] > j
  • 滑动窗口求最小值
  • 边界情况:实际枚举时 i 会从 n-1 开始, 但此时 j 没有被添加进队列,所以需要从 n 开始,但这一步不计算答案,也可以在队列初始化时进行

顺时针正序遍历:

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

using namespace std;
typedef long long ll;
const int N = 1e6 + 10, M = 2 * N;
int n;
int w[N], o[N];
ll s[M];
ll q[M];
bool res[N];
int hh, tt;
int read() {
    int t;
    scanf("%d", &t);
    return t;
}

int main()
{
    scanf("%d", &n);
    for (int i = 1; i <= n; i ++) w[i] = read(), o[i] = read(), s[i] = s[i+n] = w[i] - o[i];
    for (int i = 1; i <= 2*n; i ++) s[i] += s[i-1];
    
    hh = 0, tt = -1;
    for (int i = 0; i <= 2*n; i ++) // 和倒序本质上是相同的
    {
        if (hh <= tt && q[hh] < i - n) hh ++;
        while(hh <= tt && s[q[tt]] > s[i]) tt --;
        q[++ tt] = i;
        if (i > n)
        {
            if (s[q[hh]] >= s[i-n-1]) res[i-n] = 1;
        }
        
    }

    o[0] = o[n];
    for (int i = 1; i <= n; i ++) s[i] = s[i+n] = w[i] - o[i-1];
    for (int i = 1; i <= 2*n; i ++) s[i] += s[i-1];
    
    
    hh = 0, tt = -1;
    for (int i = 0; i <= 2*n; i ++)
    {
        if (hh <= tt && q[hh] < i - n) hh ++;
        if (i > n)
        {
            if (s[i] - s[q[hh]] >= 0) res[i-n] = true;
        }
        while(hh <= tt && s[q[hh]] < s[i]) tt --;
        q[++ tt] = i;
    }
    
    for (int i = 1; i <= n; i ++)
        if (res[i]) puts("TAK");
        else puts("NIE");
    return 0;
}

顺时针倒序遍历:

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

using namespace std;
typedef long long ll;
const int N = 1e6 + 10, M = 2 * N;
int n;
int w[N], o[N];
ll s[M];
ll q[M];
bool res[N];
int hh, tt;
int read() {
    int t;
    scanf("%d", &t);
    return t;
}

int main()
{
    scanf("%d", &n);
    for (int i = 1; i <= n; i ++) w[i] = read(), o[i] = read(), s[i] = s[i+n] = w[i] - o[i];
    for (int i = 1; i <= 2*n; i ++) s[i] += s[i-1];
    
    hh = 0, tt = -1;
    for (int i = 2*n; i >= 0; i --)
    {
        if (hh <= tt && q[hh] > i + n) hh ++;
        if (i < n)
        {
            if (s[q[hh]] - s[i] >= 0) res[i+1] = true; 
        }
        while(hh <= tt && s[q[tt]] > s[i]) tt --;
        q[++ tt] = i;
    }

    o[0] = o[n];
    for (int i = 1; i <= n; i ++) s[i] = s[i+n] = w[i] - o[i-1];
    for (int i = 1; i <= 2*n; i ++) s[i] += s[i-1];
    
    
    hh = 0, tt = -1;
    for (int i = 0; i <= 2*n; i ++)
    {
        if (hh <= tt && q[hh] < i - n) hh ++;
        if (i > n)
        {
            if (s[i] - s[q[hh]] >= 0) res[i-n] = true;
        }
        while(hh <= tt && s[q[hh]] < s[i]) tt --;
        q[++ tt] = i;
    }
    
    for (int i = 1; i <= n; i ++)
        if (res[i]) puts("TAK");
        else puts("NIE");
    return 0;
}

烽火传递

来源:https://www.acwing.com/problem/content/1091/

题目描述

烽火台是重要的军事防御设施,一般建在交通要道或险要处。

一旦有军情发生,则白天用浓烟,晚上有火光传递军情。

在某两个城市之间有 n n n 座烽火台,每个烽火台发出信号都有一定的代价。

为了使情报准确传递,在连续 m m m 个烽火台中至少要有一个发出信号。

现在输入 n , m n,m n,m 和每个烽火台的代价,请计算在两城市之间准确传递情报所需花费的总代价最少为多少。

输入格式

第一行是两个整数 n , m n,m n,m,具体含义见题目描述;

第二行 n n n 个整数表示每个烽火台的代价 a i a_i ai

输出格式

输出仅一个整数,表示最小代价。

数据范围

1 ≤ m ≤ n ≤ 2 × 1 0 5 1≤m≤n≤2×10^5 1mn2×105,
0 ≤ a i ≤ 1000 0≤ai≤1000 0ai1000

输入样例:
5 3
1 2 5 6 2
输出样例:
4

思路分析

典型的使用滑动窗口优化DP问题的案例

状态表示:f[i]表示从 1 ∼ n 1\sim n 1n 符合要求且第 i i i 个点燃的所有方案

属性:f[i] 存储的是 1 ∼ n 1\sim n 1n 符合要求的所有方案中代价的最小值

集合划分:题目要求最长连续不点燃的区间长度必须小于 m m m 因此对于 f[i] 来说,考虑区间内的即可 [i-m+1, i-1]f[i] = w[i] + min{f[k]} (k=i-m+1, ..., i-1)f[i] 由区间内最小的 f[k] 转移过来。

答案:我们只需要在最后一个区间长度不超过 m-1 的区间内寻找最小值即可。

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

using namespace std;

const int N = 2e5+10;

int a[N], f[N];
int n, m;
int q[N];
int main() {
    scanf("%d%d", &n, &m);
    for (int i = 1; i <= n; i ++) scanf("%d", &a[i]);
    int hh = 0, tt = 0;
    
    for (int i = 1; i <= n; i ++)
    {
        if (hh <= tt && 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 = 2e9;
    for (int i = n; i >= n-m+1; i --) res = min(res, f[i]);
    printf("%d\n", res);
    return 0;
}

修剪草坪

来源: https://www.acwing.com/problem/content/1089/

题目描述

在一年前赢得了小镇的最佳草坪比赛后,FJ 变得很懒,再也没有修剪过草坪。

现在,新一轮的最佳草坪比赛又开始了,FJ 希望能够再次夺冠。

然而,FJ 的草坪非常脏乱,因此,FJ 只能够让他的奶牛来完成这项工作。

FJ 有 N N N 只排成一排的奶牛,编号为 1 1 1 N N N

每只奶牛的效率是不同的,奶牛 i i i 的效率为 E i E_i Ei

编号相邻的奶牛们很熟悉,如果 FJ 安排超过 K K K 只编号连续的奶牛,那么这些奶牛就会罢工去开派对。

因此,现在 FJ 需要你的帮助,找到最合理的安排方案并计算 FJ 可以得到的最大效率。

注意,方案需满足不能包含超过 KK 只编号连续的奶牛。

输入格式

第一行:空格隔开的两个整数 N N N K K K

第二到 N + 1 N+1 N+1 行:第 i + 1 i+1 i+1 行有一个整数 E i E_i Ei

输出格式

共一行,包含一个数值,表示 FJ 可以得到的最大的效率值。

数据范围

1 ≤ N ≤ 1 0 5 1≤N≤10^5 1N105,
0 ≤ E i ≤ 1 0 9 0≤E_i≤10^9 0Ei109

输入样例:
5 2
1
2
3
4
5
输出样例:
12
样例解释

FJ 有 5 只奶牛,效率分别为 1、2、3、4、5。

FJ 希望选取的奶牛效率总和最大,但是他不能选取超过 2 只连续的奶牛。

因此可以选择第三只以外的其他奶牛,总的效率为 1 + 2 + 4 + 5 = 12。

思路分析

状态表示

f[i] 表示考虑前 i 头牛的所有合法方案的最大效率

状态计算

对应集合划分,对第 i 头牛可以划分为选或不选

  • 不选第 i 头牛,f[i] = f[i-1]
  • 选第 i 头牛,依照前边选了多少连续的牛,设为 j ,第 i-j头牛一定不选,以此划分成固定部分和变动部分:前 i-j-1 头牛的划分情况是变动的,后面 i-j+1i 的牛是必选的。 f[i] = max{f[i-j-1] + s[i] - s[i-j]} (1 <= j <= k)
#include <iostream>
#include <cstring>
#include <algorithm>

using namespace std;
typedef long long ll;
const int N = 1e5+10;

int n, m;

ll s[N];
int q[N];
ll f[N];

ll g(int i)
{
    if (!i) return 0;
    return f[i-1] - s[i];
}

int main() 
{
    scanf("%d%d", &n, &m);
    for (int i = 1; i <= n; i ++) scanf("%lld", &s[i]), s[i] += s[i-1];
    int hh = 0, tt = 0;
    for (int i = 1; i <= n; i ++)
    {
        if (hh <= tt && q[hh] < i-m) hh ++;
        f[i] = max(f[i-1], g(q[hh]) + s[i]);
        while(hh <= tt && g(q[tt]) < g(i)) -- tt;
        q[++ tt] = i;
    }
    printf("%lld\n", f[n]);
    return 0;
}


理想的正方形

来源: https://www.acwing.com/problem/content/1093/

思路分析

横着做一遍求区间内的最值,再竖着做一遍求区间内的最值,遍历最后的小区间即可得到答案

有点类似与神经网络中的池化操作,得到小尺寸的图片

#include <iostream>
#include <algorithm>
#include <cstring>
using namespace std;
const int N = 1010;

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

void get_max(int a[], int b[], int len)
{
    int hh = 0, tt = -1;
    for (int i = 1; i <= len; 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_min(int a[], int b[], int len)
{
    int hh = 0, tt = -1;
    for (int i = 1; i <= len; 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]];
    }
}

int main()
{
    scanf("%d%d%d", &n, &m, &k);
    for (int i = 1; i <= n; i ++)
        for (int j = 1; j <= m; j ++) 
            scanf("%d", &w[i][j]);
            
    for (int i = 1; i <= n; i ++)
    {
        get_max(w[i], row_max[i], m);
        get_min(w[i], row_min[i], m);
    }
    int a[N], b[N], c[N];
    int res = 2e9;
    for (int i = k; i <= m; i ++)
    {
        for (int j = 1; j <= n; j ++) a[j] = row_max[j][i];
        get_max(a, b, n);
        
        for (int j = 1; j <= n; j ++) a[j] = row_min[j][i];
        get_min(a, c, n);
        
        for (int j = k; j <= n; j ++)
            res = min(res, b[j] - c[j]);
    }
    printf("%d\n", res);
    return 0;
}
  • 2
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值