题目链接:点击查看
题目大意:给出一个字符串,现在要求将其分为不大于k个连续的子串,对于每个子串求出字典序最大的子串,现在要求所有子串的最大子串的最大值最小,输出这个最大子串
题目分析:最大值最小,标准的二分条件,所以我们需要想办法二分,因为最后需要的答案是子串,我们可以用后缀数组预处理出一个前缀和,表示每个后缀贡献的本质不同子串,这样我们就可以二分子串的长度,也就是二分答案了,现在问题转换为了判定问题,也就是给出一个子串,如何在分割k-1次(分割k-1次也就是分成了k块)的情况下满足子串的子串的最大值为当前答案
其实看了晚上的题解后自己也是有了一点小理解了,直接照着做法说吧,首先对于当前二分的位置,我们首先需要找到包含当前子串的后缀pos,这样在这个位置之前的子串我们都无需去管了,因为字典序肯定比当前的答案要小,我们只需要将所有大于当前答案的子串都切割一下就好了,一开始我们无需切割,只需要记录一下需要切割的位置即可,哪些位置需要切割呢?我们从pos+1开始,沿着sa数组一直往后寻找,如果lcp(pos,i)==0的话,那么当前答案一定无解,网上都是说显然无解,我来稍微说一下为什么这样显然吧,因为我们当前枚举的答案在后缀pos中一定是一个前缀,既然当前的后缀 i 与后缀 pos 的最大公共前缀为 0 ,也就是第一个字符都不相同,加上我们又是沿着sa数组升序往后找的,这就只能说明一个问题,那就是后缀 i 的首字母就比当前枚举的答案要大了,无论如何分割,最后的最大子串一定不会小于这个首字母了,所以当前的答案必定不可能符合条件了,如此一来我们必须保证lcp(pos,i)始终大于零才行,这样我们每次在[sa[i],sa[i]+len-1]中随便选一个点切割,就能使得当前后缀中不会再出现比当前二分的答案字典序还要大的答案了,列出所有的区间后,我们对其排序,以贪心的思想将割点最小化,最后再和k-1比较就能完成check函数了
这里还有一个细节需要注意一下,我们储存的是割点的区间,这个直接来储存的话不太好转换,但是我们不妨储存每个区间的首端点,因为题目要求分成的k个区间连续,所以第一个区间的首端点一定是 0 了,无需我们储存,我们只需要储存后续的k-1个区间的首端点就好了
代码:
#include<iostream>
#include<cstdio>
#include<string>
#include<ctime>
#include<cstring>
#include<algorithm>
#include<stack>
#include<queue>
#include<map>
#include<set>
#include<cmath>
#include<sstream>
using namespace std;
typedef long long LL;
const int inf=0x3f3f3f3f;
const int N=1e5+100;
char str[N];
int sa[N]; //SA数组,表示将S的n个后缀从小到大排序后把排好序的
//的后缀的开头位置顺次放入SA中
int t1[N],t2[N],c[N];
int rk[N],height[N],len,k;
int s[N];
LL sum[N];
void build_sa(int s[],int n,int m)//n为添加0后的总长
{
int i,j,p,*x=t1,*y=t2;
for(i=0;i<m;i++)
c[i]=0;
for(i=0;i<n;i++)
c[x[i]=s[i]]++;
for(i=1;i<m;i++)
c[i]+=c[i-1];
for(i=n-1;i>=0;i--)
sa[--c[x[i]]]=i;
for(j=1;j<=n;j<<=1)
{
p=0;
for(i=n-j;i<n;i++)
y[p++]=i;
for(i=0;i<n;i++)
if(sa[i]>=j)
y[p++]=sa[i]-j;
for(i=0;i<m;i++)
c[i]=0;
for(i=0;i<n;i++)
c[x[y[i]]]++;
for(i=1;i<m;i++)
c[i]+=c[i-1];
for(i=n-1;i>=0;i--)
sa[--c[x[y[i]]]]=y[i];
swap(x,y);
p=1,x[sa[0]]=0;
for(i=1;i<n;i++)
x[sa[i]]=y[sa[i-1]]==y[sa[i]]&&y[sa[i-1]+j]==y[sa[i]+j]?p-1:p++;
if(p>=n)
break;
m=p;
}
}
void get_height(int s[],int n)//n为添加0后的总长
{
int i,j,k=0;
for(i=0;i<=n;i++)
rk[sa[i]]=i;
for(i=0;i<n;i++)
{
if(k)
k--;
j=sa[rk[i]-1];
while(s[i+k]==s[j+k])
k++;
height[rk[i]]=k;
}
}
void solve(int base=128)
{
build_sa(s,len+1,base);
get_height(s,len);
}
struct Node
{
int l,r;
Node(int L,int R)
{
l=L,r=R;
}
bool operator<(const Node& a)const
{
if(r==a.r)
return l<a.l;
return r<a.r;
}
};
bool check(LL mid)
{
int pos=lower_bound(sum+1,sum+1+len,mid)-sum;//定位sa数组
int length=mid-sum[pos-1]+height[pos];//确定串长
vector<Node>cut;//记录割点
if(len-sa[pos]>length)//如果对于当前后缀中还存在比二分的答案还要大的子串,需要割掉
cut.push_back(Node(sa[pos],sa[pos]+length-1));
for(int i=pos+1;i<=len;i++)//从 pos+1 到 n 寻找哪些需要割掉
{
length=min(length,height[i]);
if(!length)
return false;
cut.push_back(Node(sa[i],sa[i]+length-1));//添加割点所在的区间
}
sort(cut.begin(),cut.end());//排序
int cnt=0;
int p=-1;
for(int i=0;i<cut.size();i++)//贪心找最少的割点
{
if(cut[i].l>p)
{
p=cut[i].r;
cnt++;
}
}
return cnt<k;
}
int main()
{
// freopen("input.txt","r",stdin);
// ios::sync_with_stdio(false);
while(scanf("%d",&k)!=EOF&&k)
{
scanf("%s",str);
len=strlen(str);
for(int i=0;i<len;i++)
s[i]=str[i]-'a'+1;
s[len]=0;
solve();
for(int i=1;i<=len;i++)
sum[i]=sum[i-1]+len-sa[i]-height[i];
LL l=1,r=sum[len],ans,pos,length;
while(l<=r)//二分答案,也就是字符串
{
LL mid=l+r>>1;
if(check(mid))
{
ans=mid;
r=mid-1;
}
else
l=mid+1;
}
int j=lower_bound(sum+1,sum+1+len,ans)-sum;//获得第ans个子串的所属后缀
pos=sa[j];//找到后缀
length=ans-sum[j-1]+height[j];//计算第ans个子串的长度
for(int i=pos;i<pos+length;i++)
putchar(str[i]);
putchar('\n');
}
return 0;
}