【Codeforces 786C】Till I Collapse

整除分块思想,二分,根号分治。

题目大意:

给定一个长度为 $n$ 的数列 $a_1,a_2,a_3,\ldots,a_n$,你要将其划分为最少的段,使得每个段里不同的数只有不超过 $m$ 个。

对于 $m\in [1,n]$ 都要求出答案。

题解:

对于一个 $m$,答案的上界为 $\lceil\frac nm\rceil$。

由整除分块的思想可以知道,这个的不同取值只有 $O(\sqrt n)$ 种,所以不同的答案只有 $O(\sqrt n)$ 种。

而且由于答案是单调不增的,所以相同的答案一定是连续出现。

所以我们有一个简单的做法:枚举每个答案未知的 $m$,$O(n)$ 求出它的答案,然后二分这个答案最右边的出现位置。

这个做法的时间复杂度为 $O(n\sqrt n\log n)$,并无法通过。

我们发现,当 $m$ 比较小的时候,这个答案相同的区间是很短的,但是每次都要在后面剩余的位置进行二分,这就非常不划算。

所以在 $m$ 比较小的时候,我们枚举每个位置并 $O(n)$ 求出答案,在 $m$ 比较大的时候用上面的方法求答案。

当阈值为 $\sqrt {n\log n}$ 时,总时间复杂度最优,为 $O(n\sqrt{n\log n})$。可以通过本题。

Code:

#include<cstdio>
const int N=1e5+8;
int n,a[N];
bool vis[N];
inline int solve(int m){
    int k=1,l=1,tot=0;
    for(int i=1;i<=n;++i)if(!vis[a[i]]){
        if(tot==m){
            for(int j=l;j<i;++j)vis[a[j]]=0;
            tot=0,++k,l=i;
        }
        ++tot,vis[a[i]]=1;
    }
    for(int i=l;i<=n;++i)vis[a[i]]=0;
    return k;
}
int main(){
    scanf("%d",&n);
    for(int i=1;i<=n;++i)scanf("%d",a+i);
    for(int i=1;i<=1000&&i<=n;++i)printf("%d ",solve(i));
    for(int l=1001;l<=n;){
        int s=solve(l);
        int L=l+1,R=n,t=l;
        while(L<=R){
            const int mid=L+R>>1;
            if(solve(mid)==s)L=(t=mid)+1;else R=mid-1;
        }
        for(int i=l;i<=t;++i)printf("%d ",s);
        l=t+1;
    }
    return 0;
}