单调队列单调栈和优化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]\) 个牛,有两种状态:选与不选。

  • 不选:

那么这一头牛对答案就没有贡献,显然

\[dp[i][0]=\max(dp[i-1][0],dp[i-1][1]) \]

  • 选:

这个有点难搞。考虑对于 \(i\in[1,n]\),如果要在这个点上取得最大值,那么在 \(j\in[i-k,i)\) 的区间内,一定有一个 \(j\) 不选,其他在 \((j,i)\) 区间内的牛必须选,可以使用前缀和 s 数组。

我们可以得到:

\[dp[i][1]=\max(dp[j][0]-s[j]+s[i]),j\in[i-k,i) \]

又因为 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)

想不出用单调队列解决的方法,我在题解区看到了一个好办法: 区间伸缩,就是 尺取法

尺取法的思路是这样的:

类似于双指针,我们使用两个变量 lr 分别带表当前区间的端点。

  • 当此区间不符合要求时,我们使 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;
}

斜率

放一下,到年后再搞。

posted @ 2024-02-20 19:37  ccjjxx  阅读(3)  评论(0编辑  收藏  举报