单调队列优化DP详解

单调队列,即单调的队列。有时用于优化1D/1D方程。


例题 Tyvj1305

时间: 1000ms / 空间: 131072KiB / Java类名: Main

描述

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

1,-3,5,1,-2,3
当m=4时,S=5+1-2+3=7
当m=2或m=3时,S=5+1=6  
 
 
  • 1
  • 2
  • 3
  • 1
  • 2
  • 3
输入格式
  • 第一行两个数n,m
  • 第二行有n个数,要求在n个数找到最大子序和
输出格式
  • 一个数,数出他们的最大子序和
测试样例
输入
6 4 
1 -3 5 1 -2 3
 
 
  • 1
  • 2
  • 1
  • 2
输出
7
 
 
  • 1
  • 1
备注

数据范围: 
100%满足n,m<=300000

分析

这是一个典型的动态规划题目,不难得出一个1D/1D方程:

f(i) = sum[i]-min{sum[k]|i-M≤k≤i} 
 
 
  • 1
  • 1

如果不明白这个方程的来历,戳这里

由于方程是1D/1D的,所以我们不想只得出简单的Θ(n^2)算法。不难发现,此优化的难点是计算min{sum[i-M]..sum[i-1]}。在上面的链接中,我们成功的用Θ(nlgn)的算法解决了这个问题。但如果数据范围进一步扩大,运用st表解决就力不从心了。所以我们需要一种更高效的方法,即可以在Θ(n)的摊还时间内解决问题的单调队列。 
单调队列(Monotone queue)是一种特殊的优先队列,提供了两个操作:插入,查询最小值(最大值)。它的特殊之处在于它插入的不是值,而是一个指针(key)(wiki原文:imposes the restriction that a key (item) may only be inserted if its priority is greater than that of the last key extracted from the queue)。所谓单调,指当一组数据的指针1..n(优先级为A1..An)插入单调队列Q时,队列中的指针是单调递增的,队列中指针的优先级也是单调的。因为这里要维护优先级的最小值,那么队列是单调减的,也说队列是单调减的。

查询最小值

由于优先级是单调减的,所以最小值一定是队尾元素。直接取队尾即可。

插入操作

当一个数据指针i(优先级为Ai)插入单调队列Q时,方法如下:

  1. 如果队列已空或队头的优先级比Ai大,删除队头元素。
  2. 否则将i插入队头

比如说,一个优先队列已经有优先级分别为 {5,3,-2} 的三个元素,插入一个新元素,优先级为2,操作如下:

  1. 因为2 < 5,删除队头,{3,-2}
  2. 因为2 < 3,删除队头,{-2}
  3. 因为2 > -2,插入队头,{2,-2}
证明性质可以得到维护

证明指针的单调减 :由于插入指针i一定比已经在队列中所有元素大,所以指针是单调减的。 
证明优先级的单调减:由于每次将优先级比Ai大的删除,只要原队列优先级是单调的,新队列一定是单调的。用循环不变式易证正确性。 
为什么删除队头:直观的,指针比i小(靠左)而优先级比Ai大的数据没有希望成为任何一个需要的子序列中的最小值。这一点是我们使用优先队列的根本原因。

维护区间大小

当一串数据A1..Ak插入时,得到的最小值是A1..Ak的最小值。反观dp方程:

f(i) = sum[i]-min{sum[k]|i-M≤k≤i} 
 
 
  • 1
  • 1

在这里,A = sum。对于f(i),我们需要的其实是Ai-M .. Ai的最小值,而不是所有已插入数据的最小值(A1..Ai-1)。所以必须维护区间大小,使队列中的元素严格处于Ai-M..Ai-1这一区间,或者说删去哪些A中过于靠前而违反题目条件的值。由于队列中指针是单调的,也就是靠左的指针大于靠右的,或者说在优先队列中靠左的值,在A中一定靠后;优先队列中靠右的值,在A中一定靠前。我们想要删除过于靠前的,只需要在优先队列中从右一直删除,直到最右边(队尾)的值符合条件。具体地:当队头指针p满足i-m≤p时。 
形象地说,就是忍痛割爱删去哪些较好但是不符合题目限制的数据

解决问题

这里用std::list表示队列,直接按照上面的方法查询最小值,然后根据方程,f(k) = s[i] - s[queue.back()]。直接给出代码:

#include <iostream>
#include <list>
#include <cstdio>
using namespace std;

int n, m;
long long s[300005];
// 前缀和

list<int> queue;
// 链表做单调队列

int main() {
    cin >> n >> m;
    s[0] = 0;
    for (int i=1; i<=n; i++) {
        cin >> s[i];
        s[i] += s[i-1];
    }
    long long maxx = 0;
    for (int i=1; i<=n; i++) {
        while (!queue.empty() and s[queue.front()] > s[i])
            queue.pop_front();
        // 保持单调性
        queue.push_front(i);
        // 插入当前数据
        while (!queue.empty() and i-m > queue.back())
            queue.pop_back();
        // 维护区间大小,使i-m >= queue.back()
        if (i > 1)
            maxx = max(maxx, s[i] - s[queue.back()]);
        else
            maxx = max(maxx, s[i]);
        // 更新最值
    }
    cout << maxx << endl;
    return 0;
}
 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38

分析时间复杂度

运用聚合分析,由于一个元素最多进队一次,出队一次,while循环总共最多运行Θ(2n)=Θ(n)次,所以算法的摊还效率是Θ(n)。通过单调队列,实现了在线性复杂度内解决形如:

f[x] = max or min{g(k) | b[x] <= k < x} + w[x]
其中b[x]随x单调不降,即b[1]<=b[2]<=b[3]<=...<=b[n]
g[k]表示一个和k或f[k]有关的函数,w[x]表示一个和x有关的函数
 
 
  • 1
  • 2
  • 3
  • 1
  • 2
  • 3

的动态规划问题。

参考资料:百度百科,wiki 
感谢这两位优美的一问一答: 
http://zhidao.baidu.com/link?url=uQuBcPkzFeA_xoxxzKwNCXbdlmihh4ema-RUQwlcdhZ7oDzR9awb2Ec4tjudlYzyyOOpYlaQTGYntLVDDwe5-q



  • 2
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
单调队列优化DP是一种常用的优化方法,可以将时间复杂度从 $O(n^2)$ 降低到 $O(n)$ 或者 $O(n \log n)$。以下是一道利用单调队列优化DP的典型题目: 题目描述: 给定一个长度为 $n$ 的序列 $a_i$,定义 $f(i)$ 为 $a_i$ 到 $a_n$ 中的最小值,即 $f(i) = \min\limits_{j=i}^n a_j$。现在定义 $g(i)$ 为满足 $f(j) \ge a_i$ 的最小下标 $j$,即 $g(i) = \min\{j \mid j > i, f(j) \ge a_i\}$。如果不存在这样的下标 $j$,则 $g(i) = n+1$。 现在请你计算出 $1 \le i \le n$ 的所有 $g(i)$ 的值。 输入格式: 第一行包含一个整数 $n$。 第二行包含 $n$ 个整数 $a_1,a_2,\cdots,a_n$。 输出格式: 输出 $n$ 行,第 $i$ 行输出 $g(i)$ 的值。 输入样例: 5 3 1 2 4 5 输出样例: 2 5 5 5 6 解题思路: 设 $dp(i)$ 表示 $g(i)$,那么 $dp(i)$ 与 $dp(i+1)$ 的转移关系可以表示为: $$dp(i)=\begin{cases}i+1, &\text{if}\ f(i+1)\ge a_i \\dp(i+1), &\text{else}\end{cases}$$ 这个转移方程可以使用暴力 DP 解决,时间复杂度为 $O(n^2)$。但是,我们可以使用单调队列优化 DP,将时间复杂度降为 $O(n)$。 我们定义一个单调队列 $q$,存储下标。队列 $q$ 中的元素满足: - 队列中的元素是单调递减的,即 $q_1 < q_2 < \cdots < q_k$; - 对于任意的 $i\in [1,k]$,有 $f(q_i) \ge f(q_{i+1})$。 队列 $q$ 的作用是维护一个长度为 $k$ 的区间 $[i+1,q_k]$,满足这个区间中的所有 $j$ 都满足 $f(j) < f(i+1)$。 根据定义,当我们要求 $dp(i)$ 时,只需要查找队列 $q$ 中第一个满足 $f(q_j) \ge a_i$ 的位置 $q_j$,那么 $g(i) = q_j$,如果队列 $q$ 中不存在这样的位置,则 $g(i) = n+1$。 那么如何维护单调队列 $q$ 呢?我们可以在每次 DP 的过程中,将 $i$ 加入队尾。然后判断队首元素 $q_1$ 是否满足 $f(q_1) \ge a_i$,如果满足则弹出队首元素,直到队首元素不满足条件为止。 由于每个元素最多被加入队列一次,并且最多被弹出一次,因此时间复杂度为 $O(n)$。具体实现细节可以参考下面的代码实现:
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值