【算法每日一练]-单调队列 篇3 琪露诺 ,选数游戏 ,寻找段落 ,切蛋糕,新月轩就餐

目录

题目:琪露诺

 思路:

题目:选数游戏

思路: 

题目:寻找段落

思路:

 题目:切蛋糕

       

思路:


        

最后一期单调队列:

要注意维护单调队列时维护的是哪个数组?在的哪个区间?

        

题目:琪露诺

       

 思路:

     

首先f[i]表示走到i格子时获得最大和:f[i]=max(f[i-r]~f[i-l])+v[i]

常规做法O(n^2)会超时  那就维护f数组[i-r,i-l]的单调队列,当遍历i时,维护f[i-r,i-l]的最大值的单调队列,不是原数组v!!!

     
入队新元素为i-l  所以h<=t&&f[q[t]]<f[i-l],队尾才能出队,然后i-l入队
队头过期元素和i-r比较  所以h<=t&&q[h]<i-r,队头下标比i-r还小就要丢掉
更新f[i]:f[i]=f[q[h]]+v[i]
      

#include <bits/stdc++.h>
using namespace std;
const int N=2e5+86;
int n,f[N],l,r,ans=-0x3f3f3f3f,v[N],q[N],h=1,t=0;//其实h和t的值不重要,只要h==t就是队中仅1个元素,h>t队空,里面存放的下标才是关键
//有负数所以 ans 初始化为负无穷
int main()
{	
	memset(f,-0x3f,sizeof(f));f[0]=0;
	scanf("%d%d%d",&n,&l,&r);
	for(int i=0;i<=n;i++)
		scanf("%d",&v[i]);
	for(int i=l;i<=n;i++)//遍历到i时,我们的新入队元素应该是i-l,不是i
	{
		while(h<=t&&q[h]<i-r) h++;//过期队头出队
		while(h<=t&&f[q[t]]<f[i-l]) t--;//维护[i-r,i-l]的单调最值
		q[++t]=i-l;//新元素i-l入队
		f[i]=f[q[h]]+v[i];//从最值中取出更新f[i]
		if(i+r>n) //及时更新答案
			ans=max(f[i],ans);
	}
	printf("%d\n",ans);
	return 0;
}

         

可以欣赏一下这个超时版本


//朴素版本:    超时版本
#include <bits/stdc++.h>
using namespace std;
const int maxn=2e5+1;
int n,l,r,a[maxn],f[maxn],ans=-1<<30;
int main()
{
	scanf("%d%d%d",&n,&l,&r);
	for(int i=0;i<=n;i++) scanf("%d",&a[i]);
	memset(f,0xcf,sizeof(f));f[0]=0;//由于冰冻指数可能是负数,所以一定要记得初始化
	for(int i=l;i<=n+r-1;i++)//第一步至少跳到第l格,最后一步至多跳到第n+r-1格 
	{
		for(int j=max(0,i-r);j<=i-l;j++) f[i]=max(f[i],f[j]+a[i]);
		if(i>=n) ans=max(ans,f[i]);//已经跳到对岸了再更新答案 
	}
	printf("%d",ans);
	return 0;
}

       

        

题目:选数游戏

     

思路: 

              

不同于连续的区间和,这个若干离散长度,且最大连续不超过k,和之前做法不一样。

每个点都有两种状态,被选上和未被选上,但是不能有连续超过k个被选上,那么不被选上的数应该会很少。      

     

我们设置f[i] 代表第i个点不取的最优解。状态转移:f[i]=max{f[j],sum[j+1~i-1]}(i-j<=k)向后递推,最终求解f[n+1]即可(f[j]是不被选上的,故不能加上a[j])

       
那么如何判优呢?

假设从f[a]转移到f[c]优于f[b]转移到f[c](a<b<c) 则f[a]+sum[c-1]-sum[a]>=f[b]+sum[c-1]-sum[b] 易得f[a]-sum[a]>=f[c]-sum[b]

      
故我们维护一个f[j]-sum[j]数组[i-k-1~i-1]的单调队列,遍历每个i不断取出最大值即可

      
入队新元素为i-1  所以h<=t&&f[q[t]]-suf[q[t]]<f[i-1]-suf[i-1]
过期队头和i-k-1比较  所以h<=t&&q[h]<i-k-1  取等时成立
更新f[i]  f[i]=f[q[h]]+suf[i-1]-suf[q[h]]
 

#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
const int N=1e5+5;
int a[N],n,k,q[N];
ll suf[N],f[N];
int main(){
	cin>>n>>k;
	for(int i=1;i<=n;i++)cin>>a[i],suf[i]=suf[i-1]+a[i];
	int h=1,t=0;
	for(int i=1;i<=n+1;i++){
		while(h<=t&&f[q[t]]-suf[q[t]]<f[i-1]-suf[i-1])t--;//这个一定要在最前面,因为这一步可能影响到q[h]
		q[++t]=i-1;
		while(h<=t&&q[h]<i-k-1)h++;
		f[i]=f[q[h]]+suf[i-1]-suf[q[h]];
	}
	cout<<f[n+1];
}

      

       

题目:寻找段落

        

思路:

      

注意到段落长度是个范围(还很大),只能二分来做,那么是对段落长度二分吗? 这样的话怎么对获得的最大平均值来调整段落长度呢?

     
其实我们应该对最大平均值二分,然后根据题目的这个段落能不能达到,若能达到就继续减小最大平均值mid!!!

     
操作:

首先对数组减去mid,那么只需要判断[s,t]的段落中有没有出现>=0,如果出现就说明答案至少是它,可以尝试增加mid
    

再细节一点:假如遍历到了i,那么只需要看看suf[i]-min(suf[i-T~i-S])有没有>=0,故我们要维护一个suf数组[i-T, i-S]到最小值的单调队列,从而遍历所有i

       
入队新元素为i-S  所以h<=t&&suf[q[t]]>suf[i-S] 
过期队头和i-T比较  所以h<=t&&q[h]<i-T  取等时也成立
判断suf[i]-suf[q[h]]>=0成不成立即可
      

#include <bits/stdc++.h>
using namespace std;
const int N=100010;
double l,r,mid,a[N],suf[N];
int n,S,T,q[N];
int check(double mid){
	int h=1,t=0;
	suf[0]=0;
	for(int i=1;i<=n;i++) suf[i]=suf[i-1]+a[i]-mid;//初始化前缀和数组
	for(int i=S;i<=n;i++){
		while(h<=t&&suf[q[t]]>suf[i-S])t--;//维护单调性,新元素为suf[i-S]哦
		q[++t]=i-S;//入队
		while(h<=t&&q[h]<i-T)h++;//过期的出队
		if(h<=t&&suf[i]-suf[q[h]]>=0)return 1;//判断每个[S,T]长度的段落有没有>=0
	}
	return 0;
}
int main(){
	cin>>n>>S>>T;
	for(int i=1;i<=n;i++)cin>>a[i];
	l=-100000,r=100000;
	while(r-l>1e-5){//二分精确模板
		mid=(l+r)/2;
		if(check(mid))l=mid;
		else r=mid;
	}
	printf("%.3f",mid);
}

        

        

 题目:切蛋糕

       

思路:

     

首先我们这道题是要不定长的前缀和,最优前缀和是max{sum[i]−sum[j],0}(i-j<=m),我们按题意固定i后就是max(0,sum[i]−min{sum[j]})。

也就是我们只需要维护[i-m+1,i]中最小的sum[j]即可,但是窗口右固定大小,所以要剔除过期的决策
      

#include<bits/stdc++.h>           
using namespace std;
const int N=5e5+10,INF=1e9;
int sum[N],q[N],ans=-INF;//注意有可能有负数 
int main()
{
	int n,m;scanf("%d%d",&n,&m); //n为蛋糕块数,m为窗口大小
	for (register int i=1;i<=n;++i)
	{
		int x;scanf("%d",&x);
		sum[i]=sum[i-1]+x;//求前缀和 
	}
	int head=1,tail=1;q[1]=0;
	for (register int i=1;i<=n;++i)
	{                                     //对上个元素的队列处理
		while (head<=tail&&q[head]+m<i+1) head++;//过期的最优决策出队 ,这里不能取等哦
		while (head<=tail&&sum[i]<=sum[q[tail]]) tail--;//(维持单调)新元素i入队会使一些元素“无效 ”
		q[++tail]=i;
		ans=max(ans,sum[i]-sum[q[head]]);
	}
	printf("%d\n",ans);
	return 0;
}

也可这样

//  也可以这么写,(<queue>头文件)  
//访问:front,back  操作:pop_front,pop_back,push_front;push_back
	deque<int>q; //deque是双向队列
    q.push_back(0);
    for(int i=1;i<=n;i++)
    {	
        while(!q.empty()&&q.front()+m<i) q.pop_front();//越界就pop
        ans=max(ans,sum[i]-sum[q.front()]);
        while(!q.empty()&&sum[q.back()]>=sum[i])  q.pop_back();//递减就pop
        q.push_back(i);
    }

        

        

题目:新月轩就餐

思路:

其实很容易想到是双指针或者双端队列。

我们设置一个type表示当前区间已经有了多少种厨师,同时还需要记录区间中每个元素出现的次数,然后比较棘手的是移动问题了,什么时候移动呢?

我们可以发现当区间当队头元素多余时候肯定就要移动了:

统计答案就是在type等于m的时候,只要统计l和r的位置就行了。

为什么这样的移动方式就一定可以让l和r落在最佳的答案区间上?

你可以反推:因为我们的l指向的元素一定是区间中没有多余的元素,那么如果它再右移,则区间非法;如果它不移动,也就是在l的左边,那么这个区间明显不是最佳区间。

#include <bits/stdc++.h>
using namespace std;
const int N=16+5,inf=0x3f3f3f3f;
deque<int>q;
int ans=inf;
int n,m,a[N],cnt[2001],l,r,type;
int main(){
	cin>>n>>m;
	for(int i=1;i<=n;i++){
		cin>>a[i];
		if(!cnt[a[i]])type++;
		cnt[a[i]]++;
		q.push_back(i);//新元素进入队尾
		while(!q.empty()&&cnt[a[q.front()]]>1){//队头多余时候就移动
			cnt[a[q.front()]]--;q.pop_front();
		}
		if(type==m){//因为种类最多m,达到m时候就一直更新答案就行
			if(q.size()<ans){
				ans=q.size();l=q.front();r=q.back();
			}
		}
	}
	cout<<l<<" "<<r;
}

 

 

  • 6
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值