【PR #2】史莱姆(值域分段)

首先看单次询问我们怎么做。对于一个人,他的最优策略显然是不断吃最小的,并看最后能不能吃完。

假设我们把区间内的数排好序了,设为 a 1 ≤ a 2 ≤ ⋯ ≤ a n a_1\leq a_2\leq \cdots\leq a_n a1a2an。对于一个 u u u,它能吃完所有的人当且仅当:
∀ i < u , a u + ∑ j = 1 i − 1 a j − a i ≥ k ∀ i > u , ∑ j = 1 i − 1 a j − a i ≥ k \begin{aligned} \forall i<u,a_u+\sum_{j=1}^{i-1}a_j-a_i\geq k\\ \forall i>u,\sum_{j=1}^{i-1}a_j-a_i\geq k \end{aligned} i<u,au+j=1i1ajaiki>u,j=1i1ajaik
对于第一个条件 ∀ i < u , a u ≥ k + a i − ∑ j = 1 i − 1 a j \forall i<u,a_u\geq k+a_i-\sum_{j=1}^{i-1}a_j i<u,auk+aij=1i1aj,我们考虑维护出 a i − ∑ j = 1 i − 1 a j a_i-\sum_{j=1}^{i-1} a_j aij=1i1aj 的前缀最值的位置(即类似一个单调栈的东西),那么对于同一段内的限制都是相同的。

注意到这样的前缀最值个数不是很多:设上一次前缀最值的位置为 l s t lst lst,那么 i i i 成为新的前缀最值需要满足 a i − ∑ j = 1 i − 1 a j > a l s t − ∑ j = 1 l s t − 1 a j → a i > a l s t + ∑ j = l s t i − 1 a j a_i-\sum_{j=1}^{i-1}a_j>a_{lst}-\sum_{j=1}^{lst-1}a_j\to a_i>a_{lst}+\sum_{j=lst}^{i-1}a_j aij=1i1aj>alstj=1lst1ajai>alst+j=lsti1aj,也就是说 a i a_i ai 至少会翻倍,那么前缀最值个数的一个上界为 O ( log ⁡ V ) O(\log V) O(logV)

但直接找到前缀最值是困难的,不过发现我们可以放宽一点限制:我们可以找到 a i − ∑ j = 1 i − 1 a j a_i-\sum_{j=1}^{i-1}a_j aij=1i1aj 上升的位置,即 a i − ∑ j = 1 i − 1 a j > a i − 1 − ∑ j = 1 i − 2 a j → a i > 2 a i − 1 a_i-\sum_{j=1}^{i-1}a_j>a_{i-1}-\sum_{j=1}^{i-2}a_j\to a_i>2a_{i-1} aij=1i1aj>ai1j=1i2ajai>2ai1。满足这个条件的 a i a_i ai 同样只有至多 O ( log ⁡ V ) O(\log V) O(logV) 个,而且可以用主席树挨个求出。

第二个条件是与 u u u 无关的,那么我们可以先找到最大的 p p p 满足 ∑ j = 1 p − 1 a j − a p < k \sum_{j=1}^{p-1}a_j-a_p<k j=1p1ajap<k,那么对于任意 u < p u<p u<p 它们都不是答案,对于任意 u ≥ p u\geq p up 我们只需关注第一个条件。

发现在解决第一个条件的过程中,我们已经找到了 a i − ∑ j = 1 i − 1 a j a_i-\sum_{j=1}^{i-1}a_j aij=1i1aj 上升的所有位置,而我们要找的是满足 a p − ∑ j = 1 p − 1 a j > − k a_p-\sum_{j=1}^{p-1}a_j>-k apj=1p1aj>k 的最大的 p p p,于是我们就已经可以锁定 p p p 在哪两个上升位置之间,而这两个位置之间的 a i − ∑ j = 1 i − 1 a j a_i-\sum_{j=1}^{i-1}a_j aij=1i1aj 是单降的,所以可以通过二分求 p p p

而对于一个前缀最值段内,对 a u a_u au 的限制都是一样的,容易用主席树求出合法的 u u u 的个数。

总时间复杂度 O ( n log ⁡ n + q log ⁡ V log ⁡ n ) O(n\log n+q\log V\log n) O(nlogn+qlogVlogn)

接下来介绍蒋老师的单 log ⁡ \log log 做法:

首先应注意到单调性,即若 i i i 最后能获胜,则 ≥ i \geq i i 的人最后也一定能获胜。这样我们只需要找第一个能获胜的人 u u u

用的是值域分段的技巧,我们将 [ 2 b , 2 b + 1 ) [2^b,2^{b+1}) [2b,2b+1) 分成一段。那么仍然考虑 u u u 能吃完所有人的条件,发现:

  • 对于非 u u u 所在的段,只要 u u u 能够吃掉该段的段头(出现在该段中的最小的数), u u u 就能吃掉这一整段
  • 对于 u u u 所在的段:若 u u u 不是段头,则条件和上一条相同;若 u u u 是段头,只要 u u u 能够吃掉该段第二个数, u u u 就能吃掉这一整段。

同样,对于 u u u 后面的那些段,能吃掉段头的限制是和 u u u 无关的。那么我们可以先找到最后一个吃不掉段头的段,设为第 T T T 段,那么 u u u 就必须在第 T T T 段及之后,且此时我们无需再考虑 u u u 之后的段的限制。

那么我们考虑从前往后枚举 u u u 所在的段,对于 u u u 为段头的情况我们单独考虑,对于 u u u 非段头的情况,根据前面的段和当前段,我们会得到一个形如 a u ≥ x a_u\geq x aux 的限制,此时只需要看该段中最大的数是否 ≥ x \geq x x:若 ≥ x \geq x x,则能获胜的人就是所有 ≥ x \geq x x 的人;若 < x <x <x,则继续考虑 u u u 是否在下一段。

我们只需要支持一些主席树上的操作,以及对每一段的区间求和、区间 min 和次 min、区间 max,使用 st 表即可做到 O ( ( n + q ) ( log ⁡ n + log ⁡ V ) ) O((n+q)(\log n+\log V)) O((n+q)(logn+logV))

#include<bits/stdc++.h>

#define N 200010
#define ll long long
#define INF 0x7fffffff
#define fi first
#define se second
#define pii pair<int,int>
#define mk(a,b) make_pair(a,b)

using namespace std;

inline int read()
{
	int x=0,f=1;
	char ch=getchar();
	while(ch<'0'||ch>'9')
	{
		if(ch=='-') f=-1;
		ch=getchar();
	}
	while(ch>='0'&&ch<='9')
	{
		x=(x<<1)+(x<<3)+(ch^'0');
		ch=getchar();
	}
	return x*f;
}

const int B=30;
inline int hb(int x){return 31-__builtin_clz(x);}

int n,q,a[N];

namespace Seg
{
	#define lc(u) ch[u][0]
	#define rc(u) ch[u][1]
	const int nn=1e9;
	const int NN=10000000;
	int node,rt[N],ch[NN][2],size[NN];
	inline void update(int &u,int lst,int l,int r,int x)
	{
		u=++node,lc(u)=lc(lst),rc(u)=rc(lst),size[u]=size[lst]+1;
		if(l==r) return;
		int mid=(l+r)>>1;
		if(x<=mid) update(lc(u),lc(lst),l,mid,x);
		else update(rc(u),rc(lst),mid+1,r,x);
	}
	inline int query(int a,int b,int l,int r,int x)
	{
		if(x<=l) return size[b]-size[a];
		int mid=(l+r)>>1,ans=0;
		if(x<=mid) ans+=query(lc(a),lc(b),l,mid,x);
		return ans+query(rc(a),rc(b),mid+1,r,x);
	}
	void init()
	{
		for(int i=1;i<=n;i++)
			update(rt[i],rt[i-1],1,nn,a[i]);
	}
	int query(int l,int r,int x){return query(rt[l-1],rt[r],1,nn,x);}
	#undef lc
	#undef rc
}

struct data
{
	pii min1,min2;
	data(){}
	data(int p){min1=mk(a[p],p),min2=mk(INF,114514);}
	data(pii x,pii y){min1=x,min2=y;}
};

inline data max(const data &a,const data &b)
{
	if(a.min1<b.min1) return data(a.min1,min(a.min2,b.min1!=a.min1?b.min1:b.min2));
	return data(b.min1,min(b.min2,a.min1!=b.min1?a.min1:a.min2));
}

template <class T> T Set(int p);
template<> int Set<int>(int p){return a[p];}
template<> data Set<data>(int p){return data(p);}

template<class T>
struct ST
{
	vector<vector<T>> maxn;
	void init(const vector<int> &pos)
	{
		const int nn=pos.size();
		maxn.resize(nn);
		for(int i=nn-1;i>=0;i--)
		{
			maxn[i].resize(hb(nn-i)+1);
			maxn[i][0]=Set<T>(pos[i]);
			for(int j=0;i+(1<<(j+1))-1<nn;j++)
				maxn[i][j+1]=max(maxn[i][j],maxn[i+(1<<j)][j]);
		}
	}
	inline T query(int l,int r)
	{
		const int b=hb(r-l+1);
		return max(maxn[l][b],maxn[r-(1<<b)+1][b]);
	}
};

struct block
{
	int nn,lef[N],rig[N];
	vector<int> pos;
	ST<int> maxn;
	ST<data> minn;
	vector<ll> sum;
	void init()
	{
		nn=pos.size();
		static int vis[N];
		memset(vis,-1,sizeof(vis));
		for(int i=0;i<nn;i++) vis[pos[i]]=i;
		for(int i=1,lst=-1;i<=n;lef[i]=lst,i++) if(~vis[i]) lst=vis[i];
		for(int i=n,lst=nn;i>=1;rig[i]=lst,i--) if(~vis[i]) lst=vis[i];
		maxn.init(pos),minn.init(pos);
		sum.resize(nn);
		for(int i=0;i<nn;i++) sum[i]=a[pos[i]]+(i?sum[i-1]:0);
	}
	inline bool empty(int l,int r){return rig[l]>lef[r];}
	inline int querymax(int l,int r){return maxn.query(rig[l],lef[r]);}
	inline data querymin(int l,int r){return minn.query(rig[l],lef[r]);}
	inline ll querysum(int l,int r){return sum[lef[r]]-(rig[l]?sum[rig[l]-1]:0);}
}b[B];

int query(int l,int r,int k)
{
	int T=0;
	ll sum=0;
	for(int i=0;i<B;i++)
	{
		if(b[i].empty(l,r)) continue;
		int head=b[i].querymin(l,r).min1.fi;
		if(sum<head+k) T=i;
		sum+=b[i].querysum(l,r);
	}
	ll premax=-1e15; sum=0;
	for(int i=0;i<T;i++)
	{
		if(b[i].empty(l,r)) continue;
		int head=b[i].querymin(l,r).min1.fi;
		premax=max(premax,head-sum);
		sum+=b[i].querysum(l,r);
	}
	int X=-1;
	for(int i=T;i<B;i++)
	{
		if(b[i].empty(l,r)) continue;
		data minn=b[i].querymin(l,r);
		int head=minn.min1.fi,sec=minn.min2.fi;
		if(sec==INF)
		{
			if(head>=premax+k)
			{
				X=head;
				break;
			}
			premax=max(premax,head-sum);
			sum+=head;
			continue;
		}
		if(head>=premax+k&&sum+head>=sec+k)
		{
			X=head;
			break;
		}
		premax=max(premax,head-sum);
		sum+=b[i].querysum(l,r);
		int maxn=b[i].querymax(l,r);
		if(maxn>=premax+k)
		{
			X=max(sec,(int)premax+k);
			break;
		}
	}
	if(X==-1) return 0;
	return Seg::query(l,r,max(X,1));
}

int main()
{
	n=read(),q=read();
	for(int i=1;i<=n;i++) 
		a[i]=read(),b[hb(a[i])].pos.push_back(i);
	for(int i=0;i<B;i++) b[i].init();
	Seg::init();
	while(q--)
	{
		int l=read(),r=read(),k=read();
		printf("%d\n",query(l,r,k));
	}
	return 0;
}
/*
6 4
3 1 5 3 7 5
4 6 4
*/
/*
3 2
3 3 3
1 3 1
1 3 0
*/
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值