题解 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;
}