单调队列单调栈和优化dp学习笔记
单队+斜率
一、单队
原理:在动态规划问题中,要求区间最值,便可以维护一个单调队列,使得时间复杂度降低。
单调队列模板:
int tt=1,hh=1;
q[1]=a[1];
for(int i=1;i<=n;i++)
{
while(hh<=tt&&dp[q[tt]]>=dp[i-1])tt--;//弹出队尾元素
// while(i-q[tt]>=m)++tt;
q[++tt]=i-1;//加入现在的元素,至于为什么是 i-1 而不是 i,我也没明白
if(i-q[hh]>m)hh++;//保持最小值在当前区间内
dp[i]=dp[q[hh]]+a[i];//dp 状态转移
if(i>n-m)ans=min(ans,dp[i]);//更新答案
}
这是 T182. 「一本通 5.5 练习 1」烽火传递 的代码,可以参考。
T182. 「一本通 5.5 练习 1」烽火传递
模板题。主要是熟悉代码和熟悉变更。
#include<bits/stdc++.h>
using namespace std;
int m,n,a[200010];
int dp[200010];
int q[200100];
int main()
{
cin>>n>>m;int ans=INT_MAX;
for(int i=1;i<=n;i++) cin>>a[i];
// dp[1]=a[1];
int tt=1,hh=1;
q[1]=a[1];
for(int i=1;i<=n;i++)
{
while(hh<=tt&&dp[q[tt]]>=dp[i-1])tt--;
// while(i-q[tt]>=m)++tt;
q[++tt]=i-1;
if(i-q[hh]>m)hh++;
dp[i]=dp[q[hh]]+a[i];
if(i>n-m)ans=min(ans,dp[i]);
}
// for(int i=1;i<=n;i++)cout<<dp[i]<<" ";
// for(int i=n-m+1;i<=n;i++)ans=min(ans,dp[i]);
cout<<ans;
}
P2331. 「一本通 5.5 例 2」最大连续和
模板题 +1。维护一个前缀和 s[i]
,
让答案从头开始取单调队列顶端元素,即维护的前缀和最大值到当前位置的前缀和。
从头开始 从头开始 从头开始 从头开始 从头开始 从头开始
P1725 琪露诺(luogu)
这个题和烽火传递还有滑动窗口很像。
只是对于 \(i \in [l,n],dp[i]=\max(dp[i-r],\dots dp[i-l])+a[i]\)。
所以,在循环中,我们设定两个变量(差值为 \(l\))去实现对于当前循环的 \(i\) ,从 \([i-r,i-l]\) 的状态转移。
代码:
cin>>n>>l>>r;int ans=-INT_MAX-1;
for(int i=0;i<=n;i++) cin>>a[i];
for(int i=1;i<=n;i++)dp[i]=-1145141919;//要赋初值为极小值
dp[0]=0;//dp的边界
int tt=1,hh=1;//单队的首尾指针
int p=0;//这个p就是核心。p=i-l
for(int i=l;i<=n;i++)
{
while(hh<=tt&&dp[q[tt]]<=dp[p])tt--;//窗口起点在p
// while(i-q[tt]>=m)++tt;
q[++tt]=p;//入队也要入p
while(q[hh]+r<i)hh++;
dp[i]=dp[q[hh]]+a[i];
if(i>=n-r+1) ans=max(ans,dp[i]);
p++;
}
P1714 切蛋糕(luogu)
简单题。似曾相识。其实就是P2331. 「一本通 5.5 例 2」最大连续和。。。
P2629 好消息,坏消息(luogu)
但是这个题和上面的很像。但是第一遍要倒序 第一遍要倒序 第一遍要倒序。
第一遍先找区间 \([i,n]\) 内的前缀和最小值,第二遍找区间 \([1,i-1]\) 内的前缀和最小值,如果都满足条件:\(\ge0\) ,计数器加一。
致敬我上午条代码的半个小时。。
为什么我的线段树被卡了 3 个点??题解里的却 AC 了?
for(int i=n;i>=1;i--)
{
while(hh<=tt&&s[q[tt]]>=s[i])tt--;
q[++tt]=i;
while(q[hh]<i)hh++;
if(s[q[hh]]-s[i-1]<0)f[i]=1;
}
for(int i=1;i<=n;i++)
{
while(hh<=tt&&s[q[hh]]>=s[i])tt--;
q[++tt]=i;
if(s[n]-s[i-1]+s[q[hh]]>=0&&!f[i])cnt++;
}
单调队列的变种:单调栈
P2947 [USACO09MAR] Look Up S
模板。题目中说,
对于奶牛 \(i\),如果奶牛 \(j\) 满足 \(i<j\) 且 \(H_i<H_j\),我们可以说奶牛 \(i\) 可以仰望奶牛 \(j\)。 求出每只奶牛离她最近的仰望对象。
我们考虑倒序。对于 \(\forall i \in [1,n]\),要找到一个 \(j\in [ 1,i),j>i\) 并且 \(i-j\) 最小。
那么我们可以维护一个数据结构 \(q\),使得在每一位上,都有 \(q.top>i\) ,那么我们在将数据传入 \(q\) 中时,使 \(q.top>i\),再每次记录答案 q[hh]
就好了。
代码:
#include<bits/stdc++.h>
using namespace std;
int n,a[100001],q[100001],ans[100001];
int main()
{
cin>>n;
int hh=0;
for(int i=1;i<=n;i++)cin>>a[i];
for(int i=n;i>=1;i--)
{
while(hh>0&&a[q[hh]]<=a[i]) hh--;
ans[i]=q[hh];
q[++hh]=i;
}
for(int i=1;i<=n;i++) cout<<ans[i]<<endl;
}
P286. [USACO Open11] 修剪草坪
有点小难,但还好,多亏了玮子。
单调队列不如硬 D,也不如优先队列。
——wmw
考虑硬 D ,对于第 \(i\in[1,n]\) 个牛,有两种状态:选与不选。
- 不选:
那么这一头牛对答案就没有贡献,显然
- 选:
这个有点难搞。考虑对于 \(i\in[1,n]\),如果要在这个点上取得最大值,那么在 \(j\in[i-k,i)\) 的区间内,一定有一个 \(j\) 不选,其他在 \((j,i)\) 区间内的牛必须选,可以使用前缀和 s
数组。
我们可以得到:
又因为 s[i]
对于每个 \(i\),可以认为是定值,那么就可以使用单调队列维护区间 \([i-k,i)\) 的 dp[j][0]-s[j]
。
代码:
for(int i=1;i<=n;i++)
{
dp[i][0]=max(dp[i-1][0],dp[i-1][1]);
while(q[hh]<i-k)++hh;
dp[i][1]=dp[q[hh]][0]+s[i]-s[q[hh]];
while(hh<=tt&&dp[q[tt]][0]<dp[i][0]) tt--;
q[++tt]=i;
}
cout<<max(dp[n][1],dp[n][0]);
P1638 逛画展(luogu)
想不出用单调队列解决的方法,我在题解区看到了一个好办法: 区间伸缩,就是 尺取法。
尺取法的思路是这样的:
类似于双指针,我们使用两个变量 l
和 r
分别带表当前区间的端点。
-
当此区间不符合要求时,我们使
r++
,扩大区间。 -
当此区间符合要求时,我们使
l++
,缩小区间。
循环此操作直到 r==n
结束,此时找到的最小值即是答案。
while(l<=r&&r<=n)
{
if(cnt==m)
{
if(ans>r-l+1) ans=r-l+1,ansa=l,ansb=r;//统计答案
b[a[l]]--;//缩小区间
if(b[a[l]]==0)cnt--;
l++;
}
else
{
r++;b[a[r]]++;//扩大区间
if(b[a[r]]==1)cnt++;//判断当前左端点的贡献
}
}
cout<<ansa<<" "<<ansb;
P330. [SCOI2009] 生日礼物
我使用的思路也是区间伸缩。输入的时候离散化一下,就可以继续使用区间伸缩
。
#include<bits/stdc++.h>
using namespace std;
int n,k,cnt;
int ans=INT_MAX;
int b[1000010];
struct emw{
int pos,val;
bool operator <(const emw &l)const
return pos<l.pos;
}a[1000100];
int main()
{
cin>>n>>k;
for(int i=1;i<=k;i++)
{
int s;cin>>s;
for(int j=1;j<=s;j++)
{
cin>>a[++cnt].pos,a[cnt].val=i;
}
}
sort(a+1,a+1+cnt);
int l=1,r=1,c=1;b[a[1].val]=1;
for(int i=2;i<=n;i++)
if(a[i].pos==a[i-1].pos)
{
r++,b[a[i].val]++;
if(b[a[i].val]==1)++c;
}
else break;
while(l<=r&&r<=n)
{
if(c==k)
{
ans=min(ans,a[r].pos-a[l].pos);
b[a[l].val]--; if(b[a[l].val]==0)c--;
l++; if(l>n)break;
while(a[l].pos==a[l-1].pos)
{
b[a[l].val]--; if(b[a[l].val]==0)c--;
l++;if(l>n)break;
}
}
else
{
r++; if(r>n)break; b[a[r].val]++;
if(b[a[r].val]==1) c++;
while(a[r+1].val==a[r].val)
{
r++;if(r>n)break; b[a[r].val]++;
if(b[a[r].val]==1)c++;
}
}
}
cout<<ans;
}
斜率
放一下,到年后再搞。