首页 > 其他分享 >单调队列优化DP

单调队列优化DP

时间:2024-02-06 17:13:05浏览次数:32  
标签:le DP 队列 sum d% int 单调 include dp

单调队列在 DP 中的基本应用,是对这样一类 DP 状态转移方程进行优化:\(dp[i] = \min \{ dp[j] + a[i] + b[j] \}, L(i) \le j \le R(i)\)。方程中的 \(\min\) 也可以是 \(\max\),方程的特点是其中关于 \(i\) 的项 \(a[i]\) 和关于 \(j\) 的项 \(b[j]\) 是独立的,而 \(j\) 被限制在窗口 \([L(i),R(i)]\) 内,常见的如给定一个窗口值 \(k\),即 \(i-k \le j \le i\)。这个 DP 状态转移方程的编程实现,如果简单地对 \(i\) 做外层循环,对 \(j\) 做内层循环,时间复杂度为 \(O(n^2)\),而如果用单调队列来优化,时间复杂度可以变为 \(O(n)\)。

其本质原因是外层 \(i\) 变化时,不同的 \(i\) 所对应的内层 \(j\) 的窗口有重叠。

image

如图,\(i=i_1\) 时,对应的 \(j_1\) 的滑动窗口范围是上方的阴影部分;\(i=i_2\) 时,对应的 \(j_2\) 处理的滑动窗口范围是下方的阴影部分;两部分有重叠。当 \(i\) 从 \(i_1\) 增加到 \(i_2\) 时,这些重叠部分被重复计算,如果减少这些重复,就得到了优化。如果把所有重叠的部分都优化掉,那么所有 \(j\) 加起来只从头到尾遍历了一次,此时 \(j\) 的遍历实际上就是 \(i\) 的遍历。

例:P1725 琪露诺

设 \(dp[i]\) 为到达位置 \(i\) 时最大的冰冻指数,则状态转移方程是 \(dp[i] = \min \{ dp[j] \} + a[i]\),其中 \(i-R \le j \le i-L\),显然 \(j\) 的选择范围是一个滑动窗口,可以使用单调队列优化。

#include <cstdio>
#include <deque>
using namespace std;
const int N = 2e5 + 5;
const int INF = 2e9;
int a[N], dp[N];
int main()
{
    int n, l, r;
    scanf("%d%d%d", &n, &l, &r);
    for (int i = 0; i <= n; i++) scanf("%d", &a[i]);
    for (int i = 1; i <= n; i++) dp[i] = -INF;
    deque<int> q;
    for (int i = l; i <= n; i++) {
        while (!q.empty() && q.front() < i - r) q.pop_front();
        while (!q.empty() && dp[q.back()] < dp[i - l]) q.pop_back();
        q.push_back(i - l);
        if (dp[q.front()] == -INF) continue;
        dp[i] = dp[q.front()] + a[i];
    }
    int ans = -INF;
    for (int i = n - r + 1; i <= n; i++) ans = max(ans, dp[i]);
    printf("%d\n", ans);
    return 0;
}

例:P3957 [NOIP2017 普及组] 跳房子

首先注意到金币花得更多的情况下,得分不可能减少,因此满足二分答案的条件,所以考虑二分花的金币数。注意二分答案的范围是 \([0, \max (d, x_n)]\),初始弹跳距离 \(d\) 有可能比最后一个格子到起点的距离还要大。

设 \(dp[i]\) 表示调到第 \(i\) 个格子时所能得到的最大分数,注意有的格子可能不可到达,需要令其值为 \(-\infty\),而 \(dp[0]=0\),其状态转移方程类似于上一题“琪露诺”,根据当前判定的金币数可以得到升级后的弹跳能力,若单步弹跳范围为 \([L,R]\),则 \(dp[i] = \min \{ dp[j] \} + a[i]\),其中 \(i-R \le j \le i-L\),类似地,用单调队列优化 DP。

#include <cstdio>
#include <deque>
#include <algorithm>
using namespace std;
typedef long long LL;
const int N = 500005;
const LL INF = 1e12;
int x[N], s[N], n, d, k;
LL dp[N];
bool check(int g) {
    for (int i = 1; i <= n; i++) dp[i] = -INF;
    deque<int> q;
    int minstep = max(d - g, 1);
    int j = 0;
    for (int i = 1; i <= n; i++) {
        while (!q.empty() && x[q.front()] < x[i] - d - g) q.pop_front();
        while (j < i && x[j] <= x[i] - minstep) {
            if (x[j] < x[i] - d - g) {
                j++;
                continue;
            }
            while (!q.empty() && dp[q.back()] < dp[j]) q.pop_back();
            q.push_back(j);
            j++;
        }
        if (q.empty() || dp[q.front()] == -INF) continue;
        dp[i] = dp[q.front()] + s[i];
        if (dp[i] >= k) return true;
    }
    return false;
} 
int main()
{
    scanf("%d%d%d", &n, &d, &k);
    LL pos = 0;
    for (int i = 1; i <= n; i++) {
        scanf("%d%d", &x[i], &s[i]); 
        if (s[i] > 0) pos += s[i];
    }
    if (pos >= k) {
        int l = 0, r = max(d, x[n]), ans;
        while (l <= r) {
            int mid = (l + r) / 2;
            if (check(mid)) {
                r = mid - 1; ans = mid;
            } else {
                l = mid + 1;
            }
        }
        printf("%d\n", ans);
    } else printf("-1\n");
    return 0;
}

例:P3800 Power收集

给定一个 \(N\) 行 \(M\) 列的棋盘,有 \(K\) 个格子上的值为非零。要求在每一行选择一个格子,并且相邻行选的格子列标号差不超过 \(T\)。最大化选取的格子取值和。
数据范围:\(1 \le N,M,T,K \le 4000\)

分析:记 \(a_{i,j}\) 为格子的权值,设 \(dp_{i,j}\) 表示走到第 \(i\) 行,第 \(j\) 列的最大权值和,显然 \(dp_{i,j} = \max \limits_{1 \le k \le m, |k-j| \le T} \{ dp_{i-1,k}\} + a_{i,j}\)

这个转移实际上就是滑动窗口问题,即区间查询的左端点和右端点都是单调的。每一行 \(dp\) 值计算完成后,可以使用单调队列计算该行 \(dp\) 值中每个窗口的最大值供下一行 \(dp\) 值的计算使用。

#include <cstdio>
#include <deque>
#include <algorithm>
using namespace std; 
const int N = 4005;
int a[N][N], maxv[N][N], dp[N][N];
int main()
{
    int n, m, k, t; scanf("%d%d%d%d", &n, &m, &k, &t);
    while (k--) {
        int x, y, v; scanf("%d%d%d", &x, &y, &v); a[x][y] = v;
    }
    for (int i = 1; i <= n; i++) {
        for (int j = 1; j <= m; j++) dp[i][j] = maxv[i - 1][j] + a[i][j];
        deque<int> dq;
        int l = 1, r = 1;
        for (int j = 1; j <= m; j++) {
            while (!dq.empty() && dq.front() < j - t) dq.pop_front();
            while (r <= j + t && r <= m) {
                while (!dq.empty() && dp[i][dq.back()] <= dp[i][r]) dq.pop_back();
                dq.push_back(r); r++;
            }
            maxv[i][j] = dp[i][dq.front()];
        }
    }
    int ans = 0;
    for (int i = 1; i <= m; i++) ans = max(ans, dp[n][i]);
    printf("%d\n", ans);
    return 0;
}

例:P2627 [USACO11OPEN] Mowing the Lawn G

问题描述:有一个包括 \(n\) 个正整数的序列,第 \(i\) 个整数为 \(E_i\),给定一个整数 \(k\),找这样的子序列,子序列中的数在原序列连续不能超过 \(k\) 个。对子序列求和,问所有子序列中最大的和是多少?\(1 \le n \le 10^5, 0 \le E_i \le 10^9, 1 \le k \le n\)。
例如:\(n=5\),原序列为 \([7,2,3,4,5]\),\(k=2\),选择 \([7,2]\) 和 \([4,5]\),其最大和为 \(18\),其中每一段连续长度都不超过 \(k=2\)。

分析:设 \(dp[i]\) 为考虑到前 \(i\) 个整数的答案,状态转移方程为 \(dp[i] = \max \{ dp[j-1] + sum[i] - sum[j] \}\),其中 \(i-k \le j \le i\),\(sum[i]\) 为前缀和,即 \(E_1\) 加到 \(E_i\)。也就是说前面有一个位置 \(j\),该位置上的数不选,因此 \(j\) 之间部分的答案是 \(dp[j-1]\),而 \(j+1\) 到 \(i\) 这一段全选,这样的考虑对于 \(j\) 之前的部分和之后的部分都没有打破连续长度的限制,注意第 \(i\) 个数自己也可以不选,因此 \(j\) 的考虑范围是 \(i-k\) 到 \(i\)。

在计算 \(dp[i]\) 时 \(i\) 是一个定值,上述方程等价于 \(dp[i] = \max \{ dp[j-1]-sum[j] \} + sum[i]\),其中 \(i-k \le j \le i\),因此求 \(dp[i]\) 就是找到做个决策 \(j\) 使得 \(dp[j-1]-sum[j]\) 最大。这个决策范围可视作一个左端点和右端点都单调递增的滑动窗口,因此可以使用单调队列优化。

#include <cstdio>
#include <algorithm>
#include <deque>
using namespace std;
typedef long long LL;
const int N = 100005;
LL dp[N], sum[N];
int e[N];
LL calc(int i) {
    // 技巧:队列中只记录下标,需要比较实际的大小时再代入计算
    return i == 0 ? 0 : dp[i - 1] - sum[i];
}
int main()
{
    int n, k; scanf("%d%d", &n, &k);
    for (int i = 1; i <= n; i++) {
        scanf("%d", &e[i]); sum[i] = sum[i - 1] + e[i];
    }
    deque<int> dq; dq.push_back(0);
    for (int i = 1; i <= n; i++) {
        // dp[i] = max(dp[j-1]-sum[j])+sum[i]   j in [i-k,i]
        while (!dq.empty() && dq.front() < i - k) dq.pop_front();
        while (!dq.empty() && calc(dq.back()) <= calc(i)) dq.pop_back();
        dq.push_back(i);
        dp[i] = calc(dq.front()) + sum[i];
    }
    printf("%lld\n", dp[n]);
    return 0;
}

标签:le,DP,队列,sum,d%,int,单调,include,dp
From: https://www.cnblogs.com/ronchen/p/18009458

相关文章

  • 如何通过主机cPanel面板进入通过Softaculous已安装的WordPress后台
    近期接到一位用户的反馈,表示自己有一台Hostease的Linux虚拟主机,并且在cPanel面板上面通过Softaculous中的WordPress软件进行了一键安装操作,但是由于有段时间没使用,不记得WordPress后台的用户名和密码。因此想要知道如何从Hostease的Linux虚拟主机cPanel面板后台直接登陆我他的word......
  • 单调队列及单调队列优化DP
    首先是单调队列: 其实单调队列就是一种队列内的元素有单调性(单调递增或者单调递减)的队列,答案(也就是最优解)就存在队首,而队尾则是最后进队的元素。因为其单调性所以经常会被用来维护区间最值或者降低$DP$的维数已达到降维来减少空间及时间的目的。类似于滑动窗口等,单调队列具有......
  • <学习笔记> DP套DP
    游园会考虑直接设\(F(i,j)\)表示对于兑奖串前\(i\)位匹配奖章串的\(\mathrm{LCS}\)位\(j\)的方案数,但是发现没有办法直接转移,因为对于匹配到奖章串不同位置新加一个字符情况是不一样的。考虑\(\mathrm{LCS}\)的转移,设\(dp(i,j)\)表示兑奖串前\(i\)位与奖章串前\(......
  • 数位DP的一般方法
    数位DP?数位DFS!P2657[SCOI2009]windy数-洛谷|计算机科学教育新生态(luogu.com.cn)不含前导零且相邻两个数字之差至少为2的正整数被称为windy数。windy想知道,在a和b之间,包括a和b,总共有多少个windy数?我们使用DFS解决。数位DFS要设计好状态,考虑好哪些条件会......
  • Nginx配置TCP/UDP流量转发
    #usernobody;worker_processes1;#error_loglogs/error.log;#error_loglogs/error.lognotice;#error_loglogs/error.loginfo;#pidlogs/nginx.pid;events{worker_connections1024;}stream{log_formatmain'$remote_addr[$tim......
  • leetcode--5. 最长回文子串(dp)
    记录23:292024-2-5https://leetcode.cn/problems/longest-palindromic-substring/dp[i][j]s[i,j]之间能否构成回文子串[i,j]之间是否能够构成需要考虑[i+1,j-1]是否构成回文子串且s[i]==s[j]当j-1>=i+1时候说明正好是俩个相邻的字符,此时如果s[i]==s[j]就肯定可......
  • kafka系列(一)【消息队列、Kafka的基本概念、Kafka的工作机制、Kafka可满足的需求、Kafk
    (kafka系列一)一、消息队列1.消息队列的来源在高并发的应用场景中,由于来不及同步处理请求,接收到的请求往往会发生阻塞。例如,大量的插入、更新请求同时到达数据库,这会导致行或表被锁住,最后会因为请求堆积过多而触发“连接数过多的异常”(TooManyConnections)错误。因此,在高......
  • 如何实现一个延迟队列?
    延迟队列是我们日常开发过程中,经常接触并需要使用到的一种技术方案。前些时间在开发业务需求时,我也遇到了一个需要使用到延迟消息队列的需求场景,因此我也在网上调研了一系列不同的延迟队列的实现方案,在此进行了一个总结并且给大家进行分享。延迟队列定义首先,队列这种数据结构相信大......
  • DPDK-22.11.2 [六] RSS receive side scaling 网卡分流机制
    这个的作用就是为了提高性能。当分析网络数据时,可以为网口提供多个接收队列,每个cpu处理一个队列。如果每条队列是独立的,那么就可以很好的并发。这里有两个问题,一个是数据需要平均的分配到每个队列;二是同一组数据需要分配到同一个队列。rss就是这个作用,可以设定以ip进行区分,或......
  • 单调队列
    单调队列是一种内部元素具有单调性的队列,可以解决求“区间内最值”的问题。例:P1886滑动窗口/【模板】单调队列分析:暴力枚举的方式是进行\(n-k\)次循环,每次查找长度为\(k\)区间的最值,这样的算法时间复杂度是\(O(nk)\)的,无法通过这个题目。以下分析以最大值为例,最小值同......