【算法刷题】动态规划 Day3 单调队列优化

今天还是刷题

先讲一点闲言碎语
昨天看到一个视频,对我很有启发
就是,我们玩枯萎者,很多时候击杀效率就取决于你的横冲/致命横冲与致命横冲间,直撞转向的敏感度以及速度
因为经常看到,你在直撞的时候转向决策过慢,导致最后出刀人类多跑了一步进模型,或者直接蹲下蛇了
所以在直撞的时候还是要果断一点,观察了玩枯魔的屠皇,很多都是这样的
下面开始正题
刚好在刷题单的时候,发现了琪露诺这道题,于是就开始学习了单调队列
查了挺多大佬的博客才搞懂,这种成就感确实不错
这个时候才发现,之前用的单调队列模板的泛用性非常差,还长
不知道写题解的人咋想的
所以,我重新把单调队列的题目和新代码贴一遍,放在这里:

P1886 滑动窗口 /【模板】单调队列

题目描述

有一个长为 \(n\) 的序列 \(a\),以及一个大小为 \(k\) 的窗口。现在这个从左边开始向右滑动,每次滑动一个单位,求出每次滑动后窗口中的最大值和最小值。

例如,对于序列 \([1,3,-1,-3,5,3,6,7]\) 以及 \(k = 3\),有如下过程:

\[\def\arraystretch{1.2} \begin{array}{|c|c|c|}\hline \textsf{窗口位置} & \textsf{最小值} & \textsf{最大值} \\ \hline \verb![1 3 -1] -3 5 3 6 7 ! & -1 & 3 \\ \hline \verb! 1 [3 -1 -3] 5 3 6 7 ! & -3 & 3 \\ \hline \verb! 1 3 [-1 -3 5] 3 6 7 ! & -3 & 5 \\ \hline \verb! 1 3 -1 [-3 5 3] 6 7 ! & -3 & 5 \\ \hline \verb! 1 3 -1 -3 [5 3 6] 7 ! & 3 & 6 \\ \hline \verb! 1 3 -1 -3 5 [3 6 7]! & 3 & 7 \\ \hline \end{array} \]

输入格式

输入一共有两行,第一行有两个正整数 \(n,k\)
第二行 \(n\) 个整数,表示序列 \(a\)

输出格式

输出共两行,第一行为每次窗口滑动的最小值
第二行为每次窗口滑动的最大值

输入输出样例 #1

输入 #1

8 3
1 3 -1 -3 5 3 6 7

输出 #1

-1 -3 -3 -3 3 3
3 3 5 5 6 7

说明/提示

【数据范围】
对于 \(50\%\) 的数据,\(1 \le n \le 10^5\)
对于 \(100\%\) 的数据,\(1\le k \le n \le 10^6\)\(a_i \in [-2^{31},2^{31})\)

解法&&个人感想

别的原理啥的我就不多说了,这里就讲一个,我们的单调队列不维护原数组的值了,直接维护对应下标,这样省空间,代码还短

#include<bits/stdc++.h>
#define ll long long
#define ull unsigned long long
#define lowbit(x) (x&(-x))
#define maxn 1000005
using namespace std;
int n,k;
int q[maxn];
int res[maxn];
int ma[maxn];
int head=1,tail=0;
int main(){
    cin>>n>>k;
    for(int i=1;i<=n;i++){
        cin>>ma[i];
    }
    for(int i=1;i<=n;i++){
        while(head<=tail&&q[head]<i-k+1) head++;
        while(head<=tail&&ma[q[tail]]>=ma[i]) tail--;
        q[++tail]=i;
        if(i>=k) res[i-k+1]=ma[q[head]];
    }
    for(int i=1;i<=n-k+1;i++){
        cout<<res[i]<<' ';
    }
    cout<<endl;
    head=1,tail=0;
    memset(q,0,sizeof(q));
    for(int i=1;i<=n;i++){
        while(head<=tail&&q[head]<i-k+1) head++;
        while(head<=tail&&ma[q[tail]]<=ma[i]) tail--;
        q[++tail]=i;
        if(i>=k) res[i-k+1]=ma[q[head]];
    }
    for(int i=1;i<=n-k+1;i++){
        cout<<res[i]<<' ';
    }
    system("pause");
    return 0;
}

然后,我们来聊聊单调队列优化DP

1.0 简单介绍

1.1 本质 & 适用范围

运用单调队列优化dp,本质是利用单调性,及时排除不可能的决策,以保持候选集合的有效性和秩序性。

单调队列非常适合优化决策取值范围的上、下界均单调变化,每个决策在候选集合中插入或删除至多一次。

1.2 适用方程 & 条件

可以使用单调队列的状态转移方程大多可归为如下形式:

\[f_i = \min_{L(i) \leq j \leq R(i)} \{ f_j + val(i,j) \} \]

其中:

  • \(L(i)\)\(R(i)\) 是两个关于 \(i\) 的一次函数,限制了 \(j\) 的取值范围
  • \(val(i,j)\) 是一个关于 \(i,j\) 的多项式函数

条件
多项式 \(val(i,j)\) 的每一项仅与 \(i\)\(j\) 中的一个有关

摘自这位大佬的博客

下面,我就讲一下今天碰到的两道题:

P1725 琪露诺

题目描述

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

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

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

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

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

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

输入格式

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

第二行共 \(N+1\) 个整数,第 \(i\) 个数表示编号为 \(i-1\) 的格子的冰冻指数 \(A_{i-1}\)

输出格式

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

输入输出样例 #1

输入 #1

5 2 3
0 12 3 11 7 -2

输出 #1

11

说明/提示

对于 \(60\%\) 的数据,\(N \le 10^4\)

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

解法&&个人感想

我们很容易看出转移方程为\(dp_i = \max\limits_{L \leq j \leq R} \{ dp_j\} +ma_{i}\)
那么,就直接开始维护这个单调队列,为了满足每次更新都从已更新的状态中提取,我们直接正序遍历
注意,因为我们要更新最小值,所以为了不妨碍每次的入队和出队,\(dp\)数组有必要赋值为负无穷

#include<bits/stdc++.h>
#define ll long long
#define ull unsigned long long
#define lowbit(x) (x&(-x))
#define maxn 400005
using namespace std;
int q[maxn];
int head=1,tail=0;
int ma[maxn];
int dp[maxn];
int n,l,r;
const int INF=1e9;
int ans=-INF;
int main(){
    cin>>n>>l>>r;
    for(int i=0;i<=n;i++){
        cin>>ma[i];
    }
    for(int i=0;i<=2*n;i++) dp[i]=-INF;
    dp[0]=0;
    for(int i=0;i<=n;i++){
        while(head<=tail&&q[head]<i-(r-l)) head++;
        while(head<=tail&&dp[q[tail]]<=dp[i]) tail--;
        q[++tail]=i;
        dp[i+l]=dp[q[head]]+ma[i+l];   
    }
    for(int i=n+1;i<=n+r;i++){
        ans=max(ans,dp[i]);
    }
    cout<<ans<<endl;
    system("pause");
    return 0;
}

这题还不够难,因为\(val\)函数直接是0?
好吧,我们看下一题

P3572 [POI 2014] PTA-Little Bird

题目描述

\(n\) 棵树排成一排,第 \(i\) 棵树的高度是 \(d_i\)

\(q\) 只鸟要从第 \(1\) 棵树到第 \(n\) 棵树。

当第 \(i\) 只鸟在第 \(j\) 棵树时,它可以飞到第 \(j+1, j+2, \cdots, j+k_i\) 棵树。

如果一只鸟飞到一颗高度大于等于当前树的树,那么它的劳累值会增加 \(1\),否则不会。

由于这些鸟已经体力不支,所以它们想要最小化劳累值。

输入格式

第一行输入 \(n\)

第二行 \(n\) 个数,第 \(i\) 个数表示 \(d_i\)

第三行输入 \(q\)

接下来 \(q\) 行,每一行一个整数,第 \(i\) 行的整数为 \(k_i\)

输出格式

\(q\) 行,每一行输出第 \(i\) 只鸟的最小劳累值。

输入输出样例 #1

输入 #1

9
4 6 3 6 3 7 2 6 5
2
2
5

输出 #1

2
1

说明/提示

\(1 \le n \le 10^6\)\(1 \le d_i \le 10^9\)\(1 \le q \le 25\)\(1 \le k_i \le n - 1\)

解法&&个人感想

其实这道题我想了蛮久,是因为确实无法确定这个\(val\)应该怎么处理
但是,现在我可以告诉你!我们不需要改变入队的对象,而是从入队的条件入手处理!
就比如,我们引入贪心的思想,如果要满足疲劳值最小,那么新进队的一定得\(dp\)值小,或者\(dp\)值相等但是\(tree\)高度更高(一样高也行),这样才能满足更优,也就是广义的“如果一个人比你小,还比你强,那么你就要退役了”,相当于重载了"强"这个函数(学OOP学的)
P.S:题解全部是先让1进队,然后从2开始遍历,但是在我的调试下,直接从1开始遍历也是可以的(应该是我的模板的更新顺序跟他们不同)

#include<bits/stdc++.h>
#define ll long long
#define ull unsigned long long
#define lowbit(x) (x&(-x))
#define maxn 1000005
using namespace std;
int tree[maxn];
int q[maxn];//这个维护的是什么?
int op,n,p;
int dp[maxn];
const int INF=1e9;
int check(int i,int j){
    if(tree[i]<=tree[j]) return 1;
    else return 0;
}
int main(){
    cin>>n;
    for(int i=1;i<=n;i++) cin>>tree[i];
    cin>>p;
    while(p--){
        cin>>op;
        int head=1,tail=0;
        for(int i=1;i<=n;i++){
            dp[i]=0;
            q[i]=0;
        }
        for(int i=1;i<=n-1;i++){
            while(head<=tail&&q[head]<i-op+1) head++;
            while(head<=tail&&((dp[q[tail]]>dp[i])||(dp[q[tail]]==dp[i]&&tree[q[tail]]<=tree[i]))) tail--;
            q[++tail]=i;            
            dp[i+1]=dp[q[head]]+check(q[head],i+1); 
        }
        cout<<dp[n]<<endl;
    }
    system("pause");
    return 0;
}

后半学期,也请各位继续关注:
《我的青春线代物语果然有问题》
《高数女主养成计划》
《程设の旅》
《青春猪头少年不会梦到多智能体吃豆人》
《某Linux的开源软件》
《Charlotte太空探索》
还有——

《我的算法竞赛不可能这么可爱》

本期到此结束!

posted @ 2025-05-29 15:18  elainafan  阅读(13)  评论(0)    收藏  举报