【算法刷题】动态规划 Day3 单调队列优化
今天还是刷题
先讲一点闲言碎语
昨天看到一个视频,对我很有启发
就是,我们玩枯萎者,很多时候击杀效率就取决于你的横冲/致命横冲与致命横冲间,直撞转向的敏感度以及速度
因为经常看到,你在直撞的时候转向决策过慢,导致最后出刀人类多跑了一步进模型,或者直接蹲下蛇了
所以在直撞的时候还是要果断一点,观察了玩枯魔的屠皇,很多都是这样的
下面开始正题
刚好在刷题单的时候,发现了琪露诺这道题,于是就开始学习了单调队列
查了挺多大佬的博客才搞懂,这种成就感确实不错
这个时候才发现,之前用的单调队列模板的泛用性非常差,还长
不知道写题解的人咋想的
所以,我重新把单调队列的题目和新代码贴一遍,放在这里:
P1886 滑动窗口 /【模板】单调队列
题目描述
有一个长为 \(n\) 的序列 \(a\),以及一个大小为 \(k\) 的窗口。现在这个从左边开始向右滑动,每次滑动一个单位,求出每次滑动后窗口中的最大值和最小值。
例如,对于序列 \([1,3,-1,-3,5,3,6,7]\) 以及 \(k = 3\),有如下过程:
输入格式
输入一共有两行,第一行有两个正整数 \(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 适用方程 & 条件
可以使用单调队列的状态转移方程大多可归为如下形式:
其中:
- \(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太空探索》
还有——
《我的算法竞赛不可能这么可爱》
本期到此结束!

浙公网安备 33010602011771号