单调队列优化DP

单调队列优化DP

1089. 烽火传递


一年半之前写过的题,非常简单的状态转移方程,现在不会想了。

要善于微调状态定义。

一开始想的是 f [ i ] f[i] f[i] 表示 1 ∼ i 1\sim i 1i 的最小花费,没有定义 第 i i i 个烽火台的状态。

就会分为点燃还是不点燃。

如果是点燃的话: f [ i ] = m i n ( f [ j ] + a [ i ] ) , i − m ≤ j < i f[i] = min(f[j]+a[i]),i-m \le j<i f[i]=min(f[j]+a[i]),imj<i ;

如果不点燃:第 i i i 个烽火台可能是空段的第一个位置,第二个位置,然后就影响到后边的元素了。

然后我就不会了


看了 y 总的,直接定义 f [ i ] f[i] f[i] 表示 1 ∼ i 1\sim i 1i 的最小花费,且点燃 i i i 个烽火台。

那么状态转移方程直接就是 : f [ i ] = m i n ( f [ j ] + a [ i ] ) , i − m ≤ j < i f[i] = min(f[j]+a[i]),i-m \le j<i f[i]=min(f[j]+a[i]),imj<i ;

枚举答案就是: a n s = f [ n − m + 1 ∼ n ] m i n ans = f[n-m+1 \sim n]_{min} ans=f[nm+1n]min

#include<iostream>
#include<algorithm>
#include<cstring>
using namespace std;

const int N=2e5+10;

int f[N],a[N],q[N];
int n,m,hh=0,tt=-1;

int main()
{
    cin>>n>>m;
    for(int i=1;i<=n;i++)scanf("%d",&a[i]);
    memset(f,0x3f,sizeof f);
    f[0] = 0;
    //f[i]=min(f[j]+w[i]), i-m<=j<i;
    for(int i=1;i<=n;i++){
        for(int j=max(0,i-m);j<i;j++){
            f[i] = min(f[i],f[j]+a[i]);
        }
    } 
    int res = 0x3f3f3f3f;
    for(int i=n;i>=n-m+1;i--){
        res = min(res,f[i]);
    }
    cout<<res;
    return 0;
}

用单调队列优化固定区间最小值。

这里有一个非常大的坑,以前都没有注意到

        if(hh<=tt && i-q[hh]+1>m+1)
            hh++; (1)
        f[i] = f[q[hh]] + a[i]; (2)
        while(hh<=tt && f[i] <= f[q[tt]]) (3)
            tt--;
        q[++tt] = i; (4)

考虑这四句的顺序是什么。实验证明不是这个顺序就会wa

(3)句表明 a [ i ] a[i] a[i] 打算进入单调队列中,所以 (3)之前,滑动窗口是 [ i − m , i − 1 ] [i-m,i-1] [im,i1]

(4)句,一般都是最后一句。(1)句在滑动窗口板子里边可以和(3)句交换。

所以问题就是 (2)句一般放在哪里。

滑动窗口的左边界由(1)句定义。有两种理解方式:

  1. i − i 对应的左端点 + 1 对应区间长度 i - i对应的左端点 + 1 对应区间长度 ii对应的左端点+1对应区间长度 ,如果区间长度比 要求的大 要求的大 要求的大 出队。
  2. q[hh] < i - m ;队头所在位置比 i 对应的最左边的位置还要小,表明队头在窗口外边了。

问题:如果滑动窗口是 [i-m , i - k] 那么应该怎么写?

#include<iostream>
#include<algorithm>
#include<cstring>
using namespace std;

const int N=2e5+10;

int f[N],a[N],q[N];
int n,m,hh=0,tt=-1;

int main()
{
    cin>>n>>m;
    for(int i=1;i<=n;i++)scanf("%d",&a[i]);
    memset(f,0x3f,sizeof f);
    f[0] = 0;
    q[++tt] = 0;
    //f[i]=min(f[j]+w[i]), i-m<=j<i;
    for(int i=1;i<=n;i++){
        if(hh<=tt && i-q[hh]+1>m+1)
            hh++;
        f[i] = f[q[hh]] + a[i];
        while(hh<=tt && f[i] <= f[q[tt]])
            tt--;
        q[++tt] = i;
    } 
    int res = 0x3f3f3f3f;
    for(int i=n;i>=n-m+1;i--){
        res = min(res,f[i]);
    }
    cout<<res;
    return 0;
}

1090. 绿色通道


只不过是烽火传递加了一个二分答案,但是又发生了奇怪的事情。二分答案得到的左边界需要+1才能变成正确答案。吐了。

/*
A: 10min
B: 20min
C: 30min
D: 40min
*/ 
#include <iostream>
#include <cstdio>
#include <cmath>
#include <algorithm>
#include <cstring>
#include <queue>
#include <set>
#include <map>
#include <vector>
#include <assert.h>
#include <sstream>
#define pb push_back 
#define all(x) (x).begin(),(x).end()
#define mem(f, x) memset(f,x,sizeof(f)) 
#define fo(i,a,n) for(int i=(a);i<=(n);++i)
#define fo_(i,a,n) for(int i=(a);i<(n);++i)
#define debug(x) cout<<#x<<":"<<x<<endl;
#define endl '\n'
using namespace std;
//#pragma GCC optimize("Ofast,no-stack-protector,unroll-loops,fast-math,O3")
//#pragma GCC target("sse,sse2,sse3,ssse3,sse4,popcnt,abm,mmx,avx,tune=native")

template<typename T>
ostream& operator<<(ostream& os,const vector<T>&v){for(int i=0,j=0;i<v.size();i++,j++)if(j>=5){j=0;puts("");}else os<<v[i]<<" ";return os;}
template<typename T>
ostream& operator<<(ostream& os,const set<T>&v){for(auto c:v)os<<c<<" ";return os;}
template<typename T1,typename T2>
ostream& operator<<(ostream& os,const map<T1,T2>&v){for(auto c:v)os<<c.first<<" "<<c.second<<endl;return os;}
template<typename T>inline void rd(T &a) {
    char c = getchar(); T x = 0, f = 1; while (!isdigit(c)) {if (c == '-')f = -1; c = getchar();}
    while (isdigit(c)) {x = (x << 1) + (x << 3) + c - '0'; c = getchar();} a = f * x;
}

typedef pair<long long ,long long >PII;
typedef pair<long,long>PLL;

typedef long long ll;
typedef unsigned long long ull; 
const int N=5e4+10,M=1e9+7;

int n,m,a[N];
int f[N];
int q[N],hh,tt=-1;
bool check(int mid){
    fo(i,1,n)f[i] = 0x3f3f3f3f;
    hh = 0,tt = 0;
    f[0] = 0;
    for(int i=1;i<=n;i++){  
        if(hh<=tt && q[hh] < i-mid-1)
            hh++;
        f[i] = f[q[hh]] + a[i];
        while(hh<=tt && f[i] <= f[q[tt]])
            tt--;
        q[++tt] = i;
    }
    for(int i=n-mid;i<=n;i++){
        if(f[i]<=m)return 1;
    }
    return 0;
}

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

int main(){
    solve();
    return 0;
}

琪露诺

题目描述

在幻想乡,琪露诺是以笨蛋闻名的冰之妖精。

某一天,琪露诺又在玩速冻青蛙,就是用冰把青蛙瞬间冻起来。但是这只青蛙比以往的要聪明许多,在琪露诺来之前就已经跑到了河的对岸。于是琪露诺决定到河岸去追青蛙。

小河可以看作一列格子依次编号为 0 0 0 N N N,琪露诺只能从编号小的格子移动到编号大的格子。而且琪露诺按照一种特殊的方式进行移动,当她在格子 i i i 时,她只移动到区间 [ i + L , i + R ] [i+L,i+R] [i+L,i+R] 中的任意一格。你问为什么她这么移动,这还不简单,因为她是笨蛋啊。

每一个格子都有一个冰冻指数 A i A_i Ai,编号为 0 0 0 的格子冰冻指数为 0 0 0。当琪露诺停留在那一格时就可以得到那一格的冰冻指数 A i A_i Ai。琪露诺希望能够在到达对岸时,获取最大的冰冻指数,这样她才能狠狠地教训那只青蛙。

但是由于她实在是太笨了,所以她决定拜托你帮它决定怎样前进。

开始时,琪露诺在编号 0 0 0 的格子上,只要她下一步的位置编号大于 N N N 就算到达对岸。

输入格式

第一行三个正整数 N , L , R N, L, R N,L,R

第二行共 N + 1 N+1 N+1 个整数,第 i i i 个数表示编号为 i − 1 i-1 i1 的格子的冰冻指数 A i − 1 A_{i-1} Ai1

输出格式

一个整数,表示最大冰冻指数。

样例 #1

样例输入 #1

5 2 3
0 12 3 11 7 -2

样例输出 #1

11

提示

对于 60 % 60\% 60% 的数据, N ≤ 1 0 4 N \le 10^4 N104

对于 100 % 100\% 100% 的数据, N ≤ 2 × 1 0 5 N \le 2\times 10^5 N2×105,$-10^3 \le A_i\le 10^3 $,$1 \le L \le R \le N $。数据保证最终答案不超过 2 31 − 1 2^{31}-1 2311


巨折磨的一道题。

详情看第一篇题解。

学会处理右端点和 i 有间隔的滑动窗口

f [ i ] = m a x ( f [ j ] ) + A [ i ]   ( i − R ≤ j ≤ i − L ) f[i]=max(f[j])+A[i] \ (i−R≤j≤i−L) f[i]=max(f[j])+A[i] (iRjiL)

/*
A: 10min
B: 20min
C: 30min
D: 40min
*/ 
#include <iostream>
#include <cstdio>
#include <cmath>
#include <algorithm>
#include <cstring>
#include <queue>
#include <set>
#include <map>
#include <vector>
#include <assert.h>
#include <sstream>
#define pb push_back 
#define all(x) (x).begin(),(x).end()
#define mem(f, x) memset(f,x,sizeof(f)) 
#define fo(i,a,n) for(int i=(a);i<=(n);++i)
#define fo_(i,a,n) for(int i=(a);i<(n);++i)
#define debug(x) cout<<#x<<":"<<x<<endl;
#define endl '\n'
using namespace std;
//#pragma GCC optimize("Ofast,no-stack-protector,unroll-loops,fast-math,O3")
//#pragma GCC target("sse,sse2,sse3,ssse3,sse4,popcnt,abm,mmx,avx,tune=native")

template<typename T>
ostream& operator<<(ostream& os,const vector<T>&v){for(int i=0,j=0;i<v.size();i++,j++)if(j>=5){j=0;puts("");}else os<<v[i]<<" ";return os;}
template<typename T>
ostream& operator<<(ostream& os,const set<T>&v){for(auto c:v)os<<c<<" ";return os;}
template<typename T1,typename T2>
ostream& operator<<(ostream& os,const map<T1,T2>&v){for(auto c:v)os<<c.first<<" "<<c.second<<endl;return os;}
template<typename T>inline void rd(T &a) {
    char c = getchar(); T x = 0, f = 1; while (!isdigit(c)) {if (c == '-')f = -1; c = getchar();}
    while (isdigit(c)) {x = (x << 1) + (x << 3) + c - '0'; c = getchar();} a = f * x;
}

typedef pair<long long ,long long >PII;
typedef pair<long,long>PLL;

typedef long long ll;
typedef unsigned long long ull; 
const int N=3e5+10,M=1e9+7;

int n,l,r;
ll a[N],f[N];
int q[N],hh,tt;
void solve(){
    cin>>n>>l>>r;
    fo(i,0,n)cin>>a[i];
	memset(f,0xcf,sizeof f);
	f[0] = 0;
	// f[i]表示跳到第i格能获得的最大值
    ll ans = -0x3f3f3f3f;
    for(int i=l;i<=n;i++){	
    
    	while(hh<=tt && f[i-l] >= f[q[tt]])
    		tt--;
    	q[++tt] = i-l;
    	
    	if(hh<=tt && q[hh] < i-r)
    		hh++;
    	f[i] = f[q[hh]] + a[i];
    	if(i+r>n)
    		ans = max(ans,f[i]);
    }
    cout<<ans;
}

int main(){
    solve();
    return 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)$。具体实现细节可以参考下面的代码实现:
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值