大意: 给定序列, 将序列划分为若干段, 使得每段不同数字不超过k, 分别求出k=1...n时的答案.
考虑贪心, 对于某个k
从1开始, 每次查询最后一个颜色数<=k的点作为一个划分, 直到全部划分完毕
由于每个划分大小至少为k, 故最多需要查询$frac{n}{k}$次, 所以总共需要查询$O(nlogn)$次.
查询操作考虑用主席树实现.
对序列中每个点维护一棵线段树, 对于位置$x$的线段树, $[x,n]$的每个位置存它到点$x$的种类数, 非叶结点存儿子的最小值用来二分.
从大到小更新, 这样就相当于每次对[x,nxt[a[x]]-1]位置进行区间加, 可以用标记永久化来优化.
#include <iostream> #include <cstdio> #define REP(i,a,n) for(int i=a;i<=n;++i) #define PER(i,a,n) for(int i=n;i>=a;--i) #define hr putchar(10) #define lc tr[o].l #define rc tr[o].r #define mid ((l+r)>>1) #define ls lc,l,mid #define rs rc,mid+1,r using namespace std; const int N = 1e5+10; int n, tot, a[N], nxt[N], T[N]; struct {int l,r,v;} tr[N<<6]; void add(int &o, int l, int r, int ql, int qr) { tr[++tot] = tr[o], o = tot; if (ql<=l&&r<=qr) return ++tr[o].v,void(); int tag = tr[o].v-min(tr[lc].v,tr[rc].v); if (mid>=ql) add(ls,ql,qr); if (mid<qr) add(rs,ql,qr); tr[o].v = tag+min(tr[lc].v,tr[rc].v); } int find(int o, int l, int r, int x, int k) { if (l==r) return l; k -= tr[o].v-min(tr[lc].v,tr[rc].v); if (mid<x) return find(rs,x,k); return tr[rc].v>k?find(ls,x,k):find(rs,x,k); } int solve(int k) { int ans = 0, now = 1; while (now<=n) { now = find(T[now],1,n,now,k)+1; ++ans; } return ans; } int main() { scanf("%d", &n); REP(i,1,n) scanf("%d", a+i); PER(i,1,n) { T[i] = T[i+1]; add(T[i],1,n,i,nxt[a[i]]?nxt[a[i]]-1:n); nxt[a[i]] = i; } REP(i,1,n) printf("%d ", solve(i));hr; }