高手专项训练-单调队列

A. 奶牛问题

【题目描述】

小 A 有 n n n 头奶牛沿着一维的栅栏吃草,第 i i i 头奶牛在目标点 x i x_i xi,它的身高是 h i h_i hi

当一头奶牛左边 D D D 距离内且右边 D D D 距离内均有身高至少是它的两倍的奶牛,它就会觉得拥挤。

请计算觉得拥挤的奶牛的数量。

【输入格式】

第一行两个整数 N , D N,D N,D

接下来 N N N行,每行两个整数 x i , h i x_i,h_i xi,hi

【输出格式】

一行一个整数,表示觉得拥挤的奶牛的数量。

【数据范围与提示】

对于 100 % 100\% 100% 的数据, 1 ≤ N ≤ 1 0 6 1 \leq N \leq 10^6 1N106 1 ≤ x i , h i ≤ 1 0 9 1 \leq x_i,h_i \leq 10^9 1xi,hi109 1 ≤ D ≤ 1 0 9 1 \leq D \leq 10^9 1D109,保证 x i x_i xi 互不相同。

想法

为数不多的模板题),跑两边单调队列,判断 D D D 内有没有大于两倍 x x x 点(具体看代码了)

代码
#include<iostream>
#include<cstdio>
#include<cstring>
#include<algorithm>
using namespace std;
struct node{
	int x,h;
}dis[1000001];
int q[1000001],l,r,n,d,ans;
bool vis1[1000001],vis2[1000001];
bool cmp(node a,node b){
	return a.x<b.x;
}
int main(){
	//freopen("cow.in","r",stdin);
	//freopen("cow.out","w",stdout);
	scanf("%d%d",&n,&d);
	memset(vis1,0,sizeof(vis1));
	memset(vis2,0,sizeof(vis2));
	for(int i=1;i<=n;++i)
	    scanf("%d%d",&dis[i].x,&dis[i].h);
	sort(dis+1,dis+1+n,cmp);
    l=1,r=0;
    for(int i=1;i<=n;++i){
    	while(r>=l&&dis[q[r]].h<dis[i].h) r-=1;
        q[++r]=i;
        while(dis[i].x-dis[q[l]].x>d) l+=1;
        if(dis[q[l]].h>=dis[i].h*2) vis1[i]=1;
	}
	l=n,r=n+1;
	for(int i=n;i>=1;--i){
		while(l>=r&&dis[q[r]].h<dis[i].h) r+=1;
		q[--r]=i;
		while(dis[q[l]].x-dis[i].x>d) l-=1;
		if(dis[q[l]].h>=dis[i].h*2) vis2[i]=1;
	}
	for(int i=1;i<=n;++i)
	    if(vis1[i]&&vis2[i]) ans+=1;
	printf("%d\n",ans);
	return 0;
}

B. 打保龄球

【题目描述】

小 B 有 n n n 个排成一排的球瓶,每个球瓶上面有一个数字,表示击中它的得分。

小 B 还有 k k k 个保龄球,每个球可以击中 w w w 个球瓶宽度的区域。

有的球瓶的数字为负数,可以利用“被击倒的球瓶留下的空白位置”或者“原先球瓶左边或右边本来的空白位置”去尽可能地避免这些负分球。

小 B 想要知道这 k k k 个保龄球能打出的最大得分是多少。

【输入格式】

输入有多组测试数据。

第一行 t t t 表示测试数据的组数。

每个测试数据第一行 3 3 3 个整数 n , k , w n,k,w n,k,w,分别表示球瓶数量、保龄球的数量、保龄球的击打宽度。

接下来一行 n n n 个整数 a 1 , a 2 , … , a n a_1,a_2,…,a_n a1,a2,,an,按顺序表示对应球瓶的分值。

【输出格式】

每组测试数据一行一个整数,表示最大得分。

想法

可以设数组 d p [ i ] [ k ] [ 0 / 1 ] dp[i][k][0/1] dp[i][k][0/1],表示当前击打了 k k k 个球,末尾为 i i i时,是否向前击打过。
为0时:直接从上一层继承过来,即 d p [ i ] [ k ] [ 0 ] = m a x ( d p [ i − 1 ] [ k ] [ 0 ] , d p [ i − 1 ] [ k ] [ 1 ] ) dp[i][k][0]=max(dp[i-1][k][0],dp[i-1][k][1]) dp[i][k][0]=max(dp[i1][k][0],dp[i1][k][1])
为1时:枚举与前一个球的范围重合的大小 x x x,用前缀和 s u m sum sum 存储 a a a 的和,那么式子为 d p [ i ] [ k ] [ 1 ] = m a x ( d p [ i − x ] [ k − 1 ] [ 1 ] + s u m [ i ] − s u m [ x ] ) dp[i][k][1]=max(dp[i-x][k-1][1]+sum[i]-sum[x]) dp[i][k][1]=max(dp[ix][k1][1]+sum[i]sum[x]),( 0 ≤ x ≤ w 0 \leq x \leq w 0xw)
注意:当 x = w x=w x=w 时,由于与前一个球范围直接重叠,所以可以将 d p [ i − x ] [ k − 1 ] [ 0 ] dp[i-x][k-1][0] dp[ix][k1][0] 加入更新范围。
现在的时间复杂度为 O ( t n k w ) O(tnkw) O(tnkw),会出事,所以用单调队列优化1的那个部分,时间复杂度为 O ( t n k ) O(tnk) O(tnk) (应该是这样的吧……)

代码
#include<iostream>
#include<cstdio>
#include<cstring>
#include<algorithm>
using namespace std;
const int inf=1e9+7;
int t,n,m,w,a[10001],sum[20001];
int dp[20001][501][2],ans;
int q[20001],l,r;
int main(){
	//freopen("bowling.in","r",stdin);
	//freopen("bowling.out","w",stdout);
	scanf("%d",&t);
	while(t--){
		memset(dp,0,sizeof(dp));
		memset(sum,0,sizeof(sum));
		ans=0;
		scanf("%d%d%d",&n,&m,&w);
		for(int i=1;i<=n;++i)
		    scanf("%d",&a[i]);
	    for(int i=1;i<=n;++i)
	        sum[i]=sum[i-1]+a[i];
	    for(int i=n+1;i<=n+w-1;++i)
	        sum[i]=sum[n];
	    for(int i=1;i<=w+n-1;++i)
	        for(int j=1;j<=m;++j)
	            dp[i][j][0]=dp[i][j][1]=-inf;
	    for(int i=1;i<=w+n-1;++i)//初始化
	        dp[i][0][1]=-inf;
	    for(int k=1;k<=m;++k){
	    	l=1,r=1;q[r]=0;
	    	for(int i=1;i<=n+w-1;++i){
	    		dp[i][k][0]=max(dp[i][k][0],max(dp[i-1][k][0],dp[i-1][k][1]));
	    		while(l<=r&&dp[q[r]][k-1][1]-sum[q[r]]<dp[i][k-1][1]-sum[i]) r-=1;//单调队列优化dp
	    	    q[++r]=i;
	    	    while(l<=r&&q[r]-q[l]>w) l+=1;
	    	    dp[i][k][1]=max(dp[i][k][1],dp[q[l]][k-1][1]-sum[q[l]]+sum[i]);
	    	    int x=max(i-w,0);
	    	    dp[i][k][1]=max(dp[i][k][1],dp[x][k-1][0]-sum[x]+sum[i]);
	    	    ans=max(ans,max(dp[i][k][0],dp[i][k][1]));
			}
		}
		printf("%d\n",ans);
	}
	return 0;
}
  • 6
    点赞
  • 14
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值