题解 CF786C【Till I Collapse】(贪心,离线扫描线)

有一个不需要主席树的时间 \(O(n\log^2 n)\) 、空间 \(O(n)\)做法。

考虑贪心,对于划分出的每个区间使其在满足 \(k\) 的限制下最长,一定不劣。即对于每个 \(k\),每次固定当前区间 \([l,r]\)\(l\) (初始时 \(l=1\)) ,满足 \(k\) 的限制下尽量让 \(r\) 大。

问题转化为如何找到满足条件最大的 \(r\),怎么实现?常见套路:预处理出每个位置 \(i\) 下一个与其值相同的位置记为 \(nex_i\),然后转化为二维数点问题。从前往后扫,确定左端点 \(l\),用一颗线段树或者树状数组维护每个 \(r\) 上的值:\([l,r]\) 中不同的数的个数,答案满足单调性,所以可以线段树上二分或树状数组上二分;左端点从 \(l\) 变为 \(l+1\) 的时候,原本的 \(a_l\) 消失,区间 \([l+1,n]\) 所有数减一,下一个相同的值是 \(a_{nex_l}\),区间 \([nex+l,n]\) 所有数加一。

这样对于每个询问 \(k\) 我们暴力从前往后扫一遍数组,时间复杂度为 \(O(n^2\log n)\)

但是我们发现中间有很多信息是被浪费的:询问为 \(k\) 时,答案至多有 \(\lceil\dfrac{n}{k}\rceil\) 个区间,一共只有 \(\sum_{k=1}^n \lceil\frac{n}{k}\rceil=H_n=O(n\ln n)\) 个有用的 \(r\) 端点,而我们中间计算了 \(n^2\)\(l\) 端点,中间的计算是浪费的。

有一个选择是将上面的线段树可持久化,记录下每个左端点对应的线段树,开始 \(O(n\log n)\) 预处理,每个查询确定 \(r_x\) 时只查询 \(r_{x-1}+1\) 对应的线段树,总时间复杂度是 \(O(n\log^2 n)\),空间复杂度为 \(O(n\log n)\)

但是不要学 DS 学傻了,没必要这样做。

发现不同的 \(k\) 之间计算相互独立,可以放在一起算。具体的说,可以用队列 \(q_i\) 存放当前 \([1,l-1]\) 已经划分完毕、等待从 \(l\) 开始划分一段新区间的询问 \(k\)。从前往后扫,确定左端点 \(l\) 及其对应的每个区间的不同值的个数(一个普通的树状数组),将队列 \(q_l\) 中所有的询问更新,设更新后询问 \(k\) 的已划分区间的右端点为 \(r\) ,在队列 \(q_{r+1}\) 加入 \(k\)

时间复杂度依然是 \(O(n\log^2 n)\),但空间复杂度降为 \(O(n)\)

代码:

#include<bits/stdc++.h>
using namespace std;
#define rg register
#define ll long long
#define ull unsigned ll
#define inf 0x3f3f3f3f
#define djq 1000000007
#define lowbit(x) (x&(-x))
#define eps 1e-8
inline void file(){
	freopen("test.in","r",stdin);
	freopen("test.out","w",stdout);
}
char buf[1<<21],*p1=buf,*p2=buf;
inline int getc(){
    return p1==p2&&(p2=(p1=buf)+fread(buf,1,1<<21,stdin),p1==p2)?EOF:*p1++;
}
inline int read(){
	rg int ret=0,f=0;char ch=getc();
    while(!isdigit(ch)){if(ch==EOF)exit(0);if(ch=='-')f=1;ch=getc();}
    while(isdigit(ch)){ret=ret*10+ch-48;ch=getc();}
    return f?-ret:ret;
}
int n,a;
int precol[100005],nex[100005],ans[100005];
int t[100005];
vector<int> q[100005],nw;
inline void add(int x,int v){
	while(x<=n){
		t[x]+=v;
		x+=x&(-x);
	}
}
inline int find(int v){
	int mid=0,sum=0;
	for(rg int i=20;~i;--i)
		if(mid+(1<<i)<=n&&sum+t[mid+(1<<i)]<=v){
			mid+=(1<<i);
			sum+=t[mid];
		}
	return mid;
}//二分 r 端点。 
signed main(){
	//file();
	n=read();
	for(rg int i=1;i<=n;++i){
		a=read();
		if(!precol[a]) nw.push_back(i);  //单独处理一下第一次出现的值。 
		else nex[precol[a]]=i;
		precol[a]=i;
		q[1].push_back(i);
	}
	for(rg int i=0;i<n;++i){
		if(i){
			add(i,-1);  
			if(nex[i]) add(nex[i],1);
		}else for(auto v:nw) add(v,1);
		for(auto v:q[i+1]){  //将当前左端点对应的队列中的询问更新。 
			++ans[v];  
			q[find(v)+1].push_back(v);
		}
		q[i+1].clear(); 
	}
	for(rg int i=1;i<=n;++i) printf("%d ",ans[i]);
	return 0;
}
posted @ 2022-02-09 16:36  Legitimity  阅读(90)  评论(0编辑  收藏  举报