6374. 【NOIP2019模拟2019.10.04】结界[生与死的境界]

题目

题目大意

给你一个数列,每次可以选择任意两个相邻的数 x x x y y y,将其删去,并在原来位置插入 x + 2 y x+2y x+2y
每次询问一个区间,对这个区间进行上述操作。求最后剩下的数最大是多少。
答案需要取模。


思考历程

看到这题,第一个想法是:这题既要搞个最大值,又要取模,所以肯定是贪心。
然而不会……
O ( n 3 ) O(n^3) O(n3)的暴力是可以打的,直接区间 D P DP DP。然而我没有打。
其实最大的瓶颈是,我需要比大小,然而数太大,会炸掉……
这题题面本身就对暴力不友好……


正解

其实我比赛的时候就发现了一个有趣的性质:
我们把操作看成整个区间分成两个区间,后面的那个区间的系数乘二,然后两边继续递归下去处理。
容易发现,假设 2 … … k i 2……{k_i} 2ki为第 i i i项的系数,则 1 ≤ k i ≤ k i − 1 + 1 1\leq k_i \leq k_{i-1}+1 1kiki1+1 k 1 = 0 k_1=0 k1=0
可以把答案序列看成几个 k i k_i ki的等差数列,每个等差数列的开头都是 1 1 1到前一个的结尾(第一个等差数列除外)。

然而有个更加神奇的结论: k i k_i ki要么是 1 1 1,要么就是 k i − 1 + 1 k_{i-1}+1 ki1+1
简单地证明一下:
对于一个等差数列,记一下它的和。如果和大于等于 0 0 0,则将它接到前面一个等差数列后面会更优;如果小于 0 0 0,则将开头的 k k k设为 1 1 1最优。

先不要考虑所有的询问。假设询问从 1 1 1开始,增量法构造这些等差数列。
首先新增一个节点, k i k_i ki 1 1 1,然后试着跟前面合并。
如果大于等于 0 0 0,就接到前面去。接了之后试着继续跟前面合并。
否则就不接到前面去。
显然这样是没有问题的。

接下来考虑如何处理区间的询问。先离线一下,按照区间右端点排序,询问挂在右端点上。
在统计答案的时候,找到左端点所在等差数列。找的时候可以二分或者并查集。
左端点所在等差数列的后面那些等差数列的和直接加起来。可以用前缀和实现。
左端点可能会将等差数列分成两部分,直接计算后面的那一块就好了。计算的时候可以一开始就预处理 s u m i = ∑ j = 1 i 2 j a j sum_i=\sum_{j=1}^{i}2^ja_j sumi=j=1i2jaj,后面的 s u m sum sum减去 s u m l − 1 sum_{l-1} suml1,再除以 2 l 2^l 2l

为什么分割之后等差数列不会变呢?
其实只需要证明后面的不会合并上来,并且这个也不会继续分裂就行了。
后面的和肯定是负数,所以前面的变了它们也不会合并上来。
如果这个分裂了(假设分裂成 B B B C C C,分割处左边的为 A A A),原来的和为 A + 2 x B + 2 x + y C A+2^xB+2^{x+y}C A+2xB+2x+yC,在分割之后和变成 B + 2 y C B+2^yC B+2yC。如果它分裂了,就变成 B + C B+C B+C.。
各自乘上 2 x 2^x 2x,加上 A A A,就分别变成了 A + 2 x B + 2 x + y C A+2^xB+2^{x+y}C A+2xB+2x+yC A + 2 x B + 2 x C A+2^xB+2^xC A+2xB+2xC
由于前者一定是最优的,所以 2 y C ≥ C 2^yC\geq C 2yCC,所以 C > 0 C>0 C>0
所以它分裂了不会更优。

在实现的时候,它很容易爆掉long long。实际上,虽然正数很大,但是它最小的负数不会太小。因为在最小的时候,一定是某个等差数列中只有一个负数(如果有几个数从后面合并上来,后面的那几个数的和一定非负数,然后和不会更小)。所以最小是 − 2 e 9 -2e9 2e9
如果有个数大于 2 e 9 2e9 2e9,说明它以后永远不会变成负数。记作 2 e 9 + 1 2e9+1 2e9+1就行了。
实现的时候要记录一个真的和,用来计算答案(有模);记录一个假的和,用来比大小。


代码

using namespace std;
#include <cstdio>
#include <cstring>
#include <algorithm>
#define N 500010
#define ll long long
#define INF 2000000000
#define mo 1000000007
inline ll my_pow(ll x,int y){
	ll res=1;
	for (;y;x=x*x%mo,y>>=1)
		if (y&1)
			res=res*x%mo;
	return res;
}
int n,m;
int a[N];
struct Query{
	int l,r,num;
} q[N];
inline bool cmpq(Query a,Query b){return a.r<b.r;}
ll ans[N];
ll _2,pow2[N],ipow2[N],bpow2[N],g[N];
int st[N],top;
ll sum[N],rs[N],pre[N];
int main(){
	freopen("border.in","r",stdin);
	freopen("border.out","w",stdout);
	scanf("%d%d",&n,&m);
	for (int i=1;i<=n;++i)
		scanf("%d",&a[i]);
	_2=my_pow(2,mo-2);
	pow2[0]=bpow2[0]=ipow2[0]=1;
	for (int i=1;i<=n;++i){
		pow2[i]=pow2[i-1]*2%mo;
		bpow2[i]=bpow2[i-1]*2;
		bpow2[i]=(bpow2[i]>INF?INF+1:bpow2[i]);
		ipow2[i]=ipow2[i-1]*_2%mo;
		g[i]=(g[i-1]+pow2[i]*a[i])%mo;
	}
	for (int i=1;i<=m;++i)
		scanf("%d%d",&q[i].l,&q[i].r),q[i].num=i;
	sort(q+1,q+m+1,cmpq);
	st[top=1]=1,pre[1]=sum[1]=rs[1]=a[1];
	int j=1;
	for (;q[j].r<=1;++j)
		ans[q[j].num]=(a[1]+mo)%mo;
	for (int i=2;i<=n;++i){
		st[++top]=i;
		sum[top]=2*a[i];
		rs[top]=2ll*(a[i]+mo)%mo;
		while (top>1 && sum[top]>=0){
			rs[top-1]=(rs[top-1]+rs[top]*pow2[st[top]-st[top-1]-(top==2)])%mo;
			sum[top-1]=sum[top-1]+sum[top]*bpow2[st[top]-st[top-1]-(top==2)];
			sum[top-1]=(sum[top-1]>INF?INF+1:sum[top-1]);
			top--;
		}
		pre[top]=(pre[top-1]+rs[top])%mo;
		st[top+1]=i+1;
		for (;j<=m && q[j].r<=i;++j){
			int l=2,r=top,x=1;
			while (l<=r){
				int mid=l+r>>1;
				if (st[mid]<=q[j].l)
					l=(x=mid)+1;
				else
					r=mid-1;
			}
			ans[q[j].num]=((pre[top]-pre[x]+mo)+(g[st[x+1]-1]-g[q[j].l-1])*ipow2[q[j].l]%mo+mo)%mo;
		}
	}
	for (int i=1;i<=m;++i)
		printf("%lld\n",ans[i]);
	return 0;
}

总结

感觉自己太菜了……
面对贪心的题,一定要敢于下结论……

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值