CF1404C Fixed Point Removal

题目大意

题目链接

给定一个正整数序列\(a_{1},\dots ,a_{n}\)。你可以进行若干次操作,每次操作可以选择一个满足\(a_i=i\)的位置\(i\),将其删去。删去后,左右两边会自动拼合到一起(也就是右边所有数下标都会相应地变化)。

你想要最大化删去的元素数量。

但是这个问题太简单了,所以你需要回答\(q\)次询问。每次询问给定两个正整数\(x,y\),求如果将序列的前\(x\)个数和后\(y\)个数变成\(n+1\)(也就是强制它们不得被删去),新的序列里最多能删掉多少个数呢?

注意,每次询问是独立的,也就是说询问后,序列会复原。

数据范围:\(1\leq n,q\leq 3\times 10^5\)\(1\leq a_i\leq n\)\(x,y\geq 0,x+y< n\)

本题题解

先不考虑多次询问。只针对一个给定的序列,如何求答案呢?

我们发现,后面的数被删除时,不会对前面的数产生影响。因此,我们总能通过巧妙地安排删除顺序,使得所有“理论上能被删除的数”都被删掉。

例如,初始时的一个位置\(i\),满足\(a_i=i-2\)(也就是\(a_i\)需要向前移动\(2\)格才能被删掉)。假设,我们已经知道了\(i\)前面有\(5\)个能被删除的数(即我们能构造出一种删除它们的方案)。那我们只要在这个方案进行到第\(2\)步时(此时\(a_i\)向前移动了\(2\)格,满足了\(a_i=i\)),把\(a_i\)删掉。然后再继续删除后\(3\)个数即可。因为\(i\)的位置在它们后面,所以中间插入一个“删除\(a_i\)”的操作,不会对原本的构造产生影响。于是我们就在“删除前\(i-1\)个数的方案”的基础上,构造出了“删除前\(i\)个数的方案”。用这种归纳,可以证明上一段所说的“应删尽删”的结论。

形式化地,我们设\(f_i\)表示序列的前\(i\)个位置,最多有多少位置被删掉。则:

\[f_i=f_{i-1}+[i\geq a_i\text{ and }f_{i-1}\geq i-a_i] \]

如果没有多次询问,我们现在可以\(O(n)\)求出一个序列的答案。


考虑多次询问。

此时有两种方向,一种是直接用数据结构维护区间,把整体问题搬到区间上,例如用线段树或者分块。但是我们发现,这个\(f\)是需要递推的,也就是说,前面一个\(f\)变化,可能会造成连锁反应。因此不好直接维护区间。

另一种方向是发现问题性质中的可二分性。我们来具体说说。

我们称【把序列的\(x\)个数强行变成\(n+1\)】为【ban掉\(x\)个数】。

对于任何一个位置,显然是它前面ban掉的数越少,它越可能可以被删除。那么,对每个位置\(i\),预处理出一个\(\text{lim}_i\) (\(-1\leq \text{lim}_i<i\)),表示ban掉\(\leq \text{lim}_i\)个数时,\(i\)这个位置是可以被删除的;ban掉\(>\text{lim}_i\)个数,\(i\)这个位置就不能被删除了。这也就是我说的,“发现问题性质中的可二分性”。

如何预处理\(\text{lim}_i\)?考虑对每个\(i\)二分答案。然后相当于要查询前\(i-1\)个位置里,满足\(\text{lim}_j\geq \text{mid}\)\(j\)有多少个 (\(1\leq j<i\))。可以用主席树维护。我们甚至可以直接在主席树上二分,这样复杂度从\(O(n\log^2n)\)降为\(O(n\log n)\)

知道了\(\text{lim}_1,\dots ,\text{lim}_n\)后,如何求答案?相当于要查询,\(j\in[1,n-y]\)里,满足\(\text{lim}_j\geq x\)\(j\)有多少个。可以直接在主席树上查询。时间复杂度\(O(q\log n)\)

总时间复杂度\(O((n+q)\log n)\)

总结反思

这个题给了我们一个很好的启示:处理多次询问的问题时,不一定要用数据结构大力维护出区间答案。可以尝试发现问题里的可二分性,然后实现“降维”。例如本题里,这种思路就成功地去掉了\(x\)这一维,我们只需要简单地用数据结构维护\(y\)的答案即可。

有一类经典问题,多次询问,每次问一个区间\([l_i,r_i]\)是否可行。如果发现了可二分性,那么一个做法是:对每个\(r\in[1,n]\),预处理出能够使它合法的最小/最大的\(l\)。这个\(l\)可以二分,有时也可以直接用 two pointers 求。这个经典做法,与本题的思路多少有些异曲同工之妙。

参考代码

Codeforces提交记录

//problem:CF1404C
#include <bits/stdc++.h>
using namespace std;

#define pb push_back
#define mk make_pair
#define lob lower_bound
#define upb upper_bound
#define fi first
#define se second
#define SZ(x) ((int)(x).size())

typedef unsigned int uint;
typedef long long ll;
typedef unsigned long long ull;
typedef pair<int,int> pii;

template<typename T>inline void ckmax(T& x,T y){x=(y>x?y:x);}
template<typename T>inline void ckmin(T& x,T y){x=(y<x?y:x);}

const int MAXN=3e5;
int n,q,a[MAXN+5],b[MAXN+5],lim[MAXN+5];
struct FrogTree{
	int rt[MAXN+5],sum[MAXN*40],ls[MAXN*40],rs[MAXN*40],cnt;
	void ins(int& x,int y,int l,int r,int pos){
		x=++cnt;
		sum[x]=sum[y]+1;
		ls[x]=ls[y];
		rs[x]=rs[y];
		if(l==r) return;
		int mid=(l+r)>>1;
		if(pos<=mid)
			ins(ls[x],ls[y],l,mid,pos);
		else
			ins(rs[x],rs[y],mid+1,r,pos);
	}
	int binary_search(int x,int l,int r,int b){
		// 最短的, 和>=b的后缀
		if(l==r)
			return l;
		int mid=(l+r)>>1;
		if(sum[rs[x]] >= b)
			return binary_search(rs[x],mid+1,r,b);
		else
			return binary_search(ls[x],l,mid,b-sum[rs[x]]);
	}
	int query(int x,int l,int r,int ql,int qr){
		if(!x) return 0;
		if(ql<=l && qr>=r){
			return sum[x];
		}
		int mid=(l+r)>>1;
		int res=0;
		if(ql<=mid)
			res += query(ls[x],l,mid,ql,qr);
		if(qr>mid)
			res += query(rs[x],mid+1,r,ql,qr);
		return res;
	}
	FrogTree(){}
}T;
int main() {
	cin>>n>>q;
	for(int i=1;i<=n;++i){
		cin>>a[i];
		if(i<a[i]){
			T.rt[i]=T.rt[i-1];
			b[i]=-1;
			lim[i]=-1;
			continue;
		}
		b[i]=i-a[i];
		if(T.sum[T.rt[i-1]] < b[i]){
			lim[i]=-1;
			T.rt[i]=T.rt[i-1];
			continue;
		}
		if(!b[i])
			lim[i]=i-1;
		else
			lim[i] = T.binary_search(T.rt[i-1],0,n-1,b[i]);
		T.ins(T.rt[i],T.rt[i-1],0,n-1,lim[i]);	
	}
	//for(int i=1;i<=n;++i)
	//	cerr<<lim[i]<<" ";
	//cerr<<endl;
	while(q--){
		int x,y;
		cin>>x>>y;
		cout<<T.query(T.rt[n-y],0,n-1,x,n-1)<<endl;
	}
	return 0;
}
posted @ 2020-09-07 10:48  duyiblue  阅读(704)  评论(2编辑  收藏  举报