题解 洛谷P6095 [JSOI2015]串分割

后缀数组 + 二分练习


题意

给定首位相接的数字串 ss ,将其划分为 kk 段,使得 kk 段所表示的数字中最大的数最小,求出最大数。
其中 3n2105,2kn3 \leq n \leq 2 \cdot 10^5, 2 \leq k \leq n

思路

观察很容易发现最终分成的 kk 段数字串的长度只可能是 nk\lceil \frac{n}{k} \rceil 或者 nk\lfloor \frac{n}{k} \rfloor 。可以反证,若最优解存在两个相邻数字串的长度之差大于等于 22 ,显然可以将较长串划分一个数字给较短串,最大值一定不会变大。
且观察到要求最大数字尽可能小,考虑可以二分答案值。从而将问题转换为了判定问题 \rightarrow 即判定能否将给定串分成不超过 kk 份使得每一份表示的数都小于等于二分求得的数,具体操作如下。
给定的数字串是一个环,首先想到断环成链,我们考虑枚举断点,再从断开处开始贪心地分割,每次尽可能分割出 nk\lceil \frac{n}{k} \rceil 长度的字符,如果分割出的数字串大于二分值则分割 nk1\lceil \frac{n}{k} \rceil - 1 个字符,最后判断 kk 次分割能否将串 ss 分完。这样总共枚举 nn 个断点,每次判断 kk 次,判断复杂度是 O(nk)O(n \cdot k) 的,但考虑存在重复分割的情况,实际上只需要枚举前 nk\lceil \frac{n}{k} \rceil 个断点就可以了,总复杂度为 O(nkk)=O(n)O(\frac{n}{k} \cdot k) = O(n)
考虑为什么这样贪心是对的,假设我当前操作分割出了 nk\lceil \frac{n}{k} \rceil 个数字,但因此下次操作只能分出 nk1\lceil \frac{n}{k} \rceil - 1 个,这样两次分割的总长度与我这一次分割 nk1\lceil \frac{n}{k} \rceil - 1 个,下一次分割nk\lceil \frac{n}{k} \rceil 个(分割 nk1\lceil \frac{n}{k} \rceil - 1 个的最优情况)的总长度相同,若当前分割之后下一次还能继续分 nk\lceil \frac{n}{k} \rceil 个,结果就更优了,所以贪心分割是正确的。
考虑实现,最终的答案一定是 ss 的一段子串,我们将串 ss 复制一份为 s=s+ss' = s + s ,求出 ss'SASA ,我们二分答案在 SASA 中的排名 pp ,单次判断枚举 ss' 的前 nk\lceil \frac{n}{k} \rceil 个点为起点,判断当前起点往后 nk\lceil \frac{n}{k} \rceil 长度的串与二分对应串的字典序与 lcplcp 长度,满足则跳,最后判断 kk 次操作之后的终点位置减去起点位置是否大于等于 nn 即可。
二分加判断的总复杂度为 O(nlogn)O(n \cdot logn)

代码

注意数组大小为 nn 的两倍。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
#include<bits/stdc++.h>
using namespace std;

using ll=long long;

constexpr int N=4e5+5;

int n,k,len;
struct SA{
int n,m;
char s[N];
int sa[N],rk[N],height[N],h[N];
int minn[21][N],lg[N];
void init(int _n,int _m){
n=_n;
m=_m;
}
void buildsa(){
static int oldrk[N<<1],id[N],px[N],cnt[N];
int i,p,w;
for(i=1;i<=m+1;i++) cnt[i]=0;
for(i=1;i<=n+1;++i) sa[i]=rk[i]=id[i]=px[i]=0;
for(i=1;i<=n*2;i++) oldrk[i]=0;
for(i=1;i<=n;++i) ++cnt[rk[i]=(s[i]-'0')];
for(i=1;i<=m;++i) cnt[i]+=cnt[i-1];
for(i=n;i>=1;--i) sa[cnt[rk[i]]--]=i;
for(w=1;;w<<=1,m=p){
for(p=0,i=n;i>n-w;--i) id[++p]=i;
for(i=1;i<=n;++i)
if(sa[i]>w) id[++p]=sa[i]-w;
for(i=1;i<=m;i++) cnt[i]=0;
for(i=1;i<=n;++i) ++cnt[px[i]=rk[id[i]]];
for(i=1;i<=m;++i) cnt[i]+=cnt[i-1];
for(i=n;i>=1;--i) sa[cnt[px[i]]--]=id[i];
for(i=1;i<=n;i++) oldrk[i]=rk[i];
for(p=0,i=1;i<=n;++i)
rk[sa[i]]=(oldrk[sa[i]]==oldrk[sa[i-1]]&&oldrk[sa[i]+w]==oldrk[sa[i-1]+w])?p:++p;
if(p==n){
for(i=1;i<=n;++i) sa[rk[i]]=i;
break;
}
}
}
void buildheight(){
for(int i=1;i<=n+1;++i) height[i]=h[i]=0;
int k=0;
for(int i=1;i<=n;i++){
if(rk[i]==1) continue;
if(k) --k;
int j=sa[rk[i]-1];
while(j+k<=n&&i+k<=n&&s[i+k]==s[j+k]) k++;
height[rk[i]]=k;
}
for(int i=1;i<=n;i++) h[i]=height[rk[i]];
for(int i=2;i<=n;i++) lg[i]=lg[i>>1]+1;
for(int i=1;i<=n;i++) minn[0][i]=height[i];
for(int j=1;(1<<j)<=n;j++)
for(int i=1;i+(1<<j)-1<=n;i++) minn[j][i]=min(minn[j-1][i],minn[j-1][i+(1<<(j-1))]);
}
int get_lcp(int l,int r){
if(l==r) return n-l+1;
l=rk[l],r=rk[r];
if(l>r) swap(l,r);
l++;
int k=lg[r-l+1];
return min(minn[k][l],minn[k][r-(1<<k)+1]);
}
}sa;

int check(int mid){
for(int i=1;i<=len;i++){
int pos=i;
for(int j=1;j<=k;j++) pos+=len-(sa.rk[pos]>mid&&sa.get_lcp(pos,sa.sa[mid])<len);
if(pos>=i+n) return 1;
}
return 0;
}

int main(){
ios::sync_with_stdio(0);
cin.tie(0);cout.tie(0);

cin>>n>>k;
len=n/k+(n%k!=0);
cin>>sa.s+1;
for(int i=1;i<=n;i++) sa.s[i+n]=sa.s[i];
sa.init(n*2,9);
sa.buildsa();
sa.buildheight();
int l=1,r=n*2,ans=-1;
while(l<=r){
int mid=l+r>>1;
if(check(mid)) ans=mid,r=mid-1;
else l=mid+1;
}
for(int i=sa.sa[ans];i<=sa.sa[ans]+len-1;i++) cout<<sa.s[i];

return true&&false;
}