AtCoder - code_festival_2017_qualb_f Largest Smallest Cyclic Shift 题解

link 本题为原T3

题意

给定一个字符串 SS,定义 f(S)f(S)SS 的字典序最小的循环移位。

举个例子,如果 S=S=babca,那么 f(S)f(S)=ababc,因为它是所有循环移位中字典序最小的(包括 babcaabcabbcabacababababc)。

现在给你三个整数 X,Y,ZX,Y,Z

你需要构造一个字符串 TT,它恰好包含 XXaYYb,和 ZZc

如果存在多个满足条件的字符串,你想选出一个使得 f(T)f(T) 字典序最大。

请计算 f(T)f(T) 可能达到的字典序最大值。注意:输出为 f(T)f(T)

1X,Y,Z1051\leq X,Y,Z\leq 10^5 on gjoj

这篇题解的做法是和其他题解相同的。不同点在于结论推导的过程会在下面给出,因此结论会在最后面。
(其实是我菜,不能一遍推出来)

推导过程

先不考虑某个字符数量为 0 的情况,即 abc 都有。

我们考虑一件事情:由于一定会有最小的字符 a,在循环移位中,肯定会以 a 为 f(T)f(T) 的开头。

考虑 TT 被 a 拆分成若干个块,对于 f(T)f(T) 的选择而言,其一定会选择字典序更小的块在前

因为每一个块都是以 a 为开头,这是最小的字符。

考虑两个块 SSsubSsubS,由于 subSsubS 后面紧跟了一个 a,而 SS 中除了第一位以外就没有 a 了,因此 subS<SsubS<S 是能确定在前面的。

其他不是子串的就不用仔细想了。

因此,想象 XX 个底部为 a 的栈,为了保证字典序往大,其他字符肯定会平铺在这 XX 个栈中。

我们来模拟看看:

比如当 X=5,Y=7,Z=12X=5,Y=7,Z=12 时:

我们考虑将 c 平铺至所有栈中,如下:

显然对于任何 ZZ,会有 最多能完整铺满的层数 ZX\lfloor \frac{Z}{X}\rfloor 和 最后一层空缺的个数 XZmodXX-Z\bmod X

能平铺完的层自然是不管的,所有 acccZXc\text{accc}\dots{\lfloor \frac{Z}{X}\rfloor个c}\dots 都是一样的,自然不管了。

关键是有空缺的位置。

根据上面的理论,无论在空缺的位置填充 b 或不填充,这些位置永远都是比其他位置小的。

那么当我们填充 b 的时候,就不需要考虑其他位置了。

如下:

这样我们就完成了块的填充,考虑怎么使它输出了。


一种非常显而易见的想法是先输出最小的块,然后按照字典序倒序输出其余的块。

这样能保证 f(T)f(T) 第一个块为最小的同时 f(T)f(T) 的字典序最大。

但是这样有问题。

考虑以下两个样例:

  1. X=114514,Y=1,Z=2X=114514,Y=1,Z=2,整理出来为 ac,ac,ab,a,a,a,...,输出为 aacacabaaaaaaaaa

  2. X=8,Y=4,Z=4X=8,Y=4,Z=4,整理出来为 ac*4,ab*4,输出为 abacacacacababab

显然这里的共性为“有多个相同的最小块”。

两个问题:第一,这样并不是 f(T)f(T),因为可以让后面的最小块移到前面,这样才是 f(T)f(T)

第二:这样的构造方式,还有优化空间。

以第二个样例举例,可以构造成 abacabacabacabac,这样显然比 ababababacacacac 更优。


我们不妨设字典序最小的块的字符排列方式为 PP。当 f(T)f(T) 请求最小的块时,前缀一定为 PP

我们希望使得这些 PP 能够尽量大。

考虑这些 PP 能够与字典序较大的几个块“拼”在一起,这样会使得这些 PP 尽量变大。

将这几个块拼在一起后,重新求最小的 PP,重复这个过程,直到 没有较大的块 / PP 只有一个。

理1:除非 PP 本身已经不存在,不然 PP 永远是最小的

理2:对于较大的块而言,P+P+ 它们会使它们变小

可以想想这两个理为什么正确,以及理是怎么保证上面的算法正确的。

接下来有几个问题:

Q:为什么要重复这个过程

显然可以构造 X=10,Y=6X=10,Y=6aab,aab,aab,aab,ab,ab -> aabab,aabab,aab,aab-> aabaabab,aabaabab

Q:重新求最小的 PP 是否会超时

不会。显然重新求的最小的 PP 是原来的 PP 的子集。

然后就可以实现了…吗?

考虑“较大的块”的集合是否包含 P+较大的块中较大的值P+较大的块中较大的值

就是说,能否在没有 除了“新合并的结果”的“较大的块” 的情况下,使用新合并的结果作为 P+P+ 的对象。

这个结论是一定的,或者说是必须的。

观察到上面 X=10,Y=6X=10,Y=6,当 P=P=aab 时,显然如果只使用ab,是不够的。

如果此时退出,显然是不能推到最后面的。

因此后面的两个 aab 应该使用 aabab


事实上我们可以直接将 P+较大的块中较大的值P+较大的块中较大的值较大的块中的值较大的块中的值 混为一谈。由于 P<较大的块P<较大的块,因此这俩的顺序不用我们刻意强调。

观察到上面两个条件,除了 PP 本身,对于 PP 其他都可以直接在一个池子里使用了。

那我们就不妨直接使用全部了。

选择所有最小的 PP,执行操作:对每一个 PP,从池子里拿出一个最大的值 vv,将 P+vP+v 重新丢回池子里。直到 没有较大的块 / PP 只有一个。

没有较大的块是假的,池子实际上只会通过“选择P”而消耗。其实这样就可以直接 break 掉了。

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
multiset<string> pool;
string mn;
int cnt;
void print(){
sort(ans+1,ans+x+1);
int k=1;
mn=ans[1];
for(k=2;k<=x;++k){
if(ans[k]!=ans[k-1]) break;
++cnt;
}
for(int i=k;i<=x;++i) pool.insert(ans[i]);
while(cnt>1&&pool.size()){
string nxtmn="z";
int nxtcnt=-114;
for(int i=1;i<=cnt;++i){
auto _end=pool.end();--_end;
string v=*_end;//理2:从池子里面访问出来的值肯定越来越小
pool.pop();
pool.insert(mn+v);
if(mn+v<nxtmn){//重新求的最小的P是原来的P的子集
nxtmn=mn+v;
nxtcnt=1;
}
else ++nxtcnt;
}
//将最小的P重新调出来
pool.erase(mn+v);
mn=nxtmn;cnt=nxtcnt;
}
if(pool.size()==0){
//池子里没有东西了,直接输出mn*cnt即可
}
else{
//按照一开始的做法来写
}
}

到这里为止,已经非常像正解的实现方式了。甚至逻辑都是一样的

正解唯二与这个的区别是:

  1. 每次查询最小的 PP,而不是一次将最小的 PP 选完

  2. PP 只有一个时,和一开始的做法实际上是一样的。因为以 PP 为开头的块始终是最小的,就会倒序 cat 掉其余的块。

那么正解就是:从池子里选择最小的 PP 和一个最大的值 vv,将 P+vP+v 重新丢回池子里。直到池子里只有一个值。

1
2
3
4
5
6
7
8
9
10
11
12
13
multiset<string> pool;
void print(){
for(int i=1;i<=x;++i) pool.insert(ans[i]);
while(pool.size()>=2){
auto itf=pool.begin(),itb=pool.end();--itb;
string c=(*itf)+(*itb);
pool.erase(itf);
pool.erase(itb);
pool.insert(c);
}
string outputstr=*pool.begin();
printf("%s",outputstr.c_str());
}

实际上还能发现一开始,几个块生成的操作也是可以这样得到的。不过我不想解释了,很容易得出来。

最后再把有字符数量为 0 的情况做个归约就好。

代码

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
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
// #define DEBG 1
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
const int maxn=1e5+35;
const ll inf=1e18;

ll x,y,z;
ll n;

void single(){
char c=((x==0)?((y==0)?'c':'b'):'a');
for(int i=1;i<=n;++i) putchar(c);
}

string add[maxn];
string ans[maxn];
void solve();

char mapf,mapt;
void _double(){
if(x==0) swap(x,y),mapf='a',mapt='b';
else if(z==0) swap(y,z),mapf='c',mapt='b';
solve();
for(int i=1;i<=x;++i){
ans[i]+=('a'==mapf)?mapt:'a';
for(int j=0;j<add[i].length();++j){
if(add[i][j]==mapf) ans[i]+=mapt;
else ans[i]+=add[i][j];
}
}
}

void triple(){
for(int i=1;i<=x;++i){
ans[i]+='a';
ans[i]+=add[i];
}
}

void print();

void sep(){
int zerocnt=0;
if(x==0) ++zerocnt;
if(y==0) ++zerocnt;
if(z==0) ++zerocnt;
if(zerocnt==3) puts("");
else if(zerocnt==2) single();
else if(zerocnt==1) _double(),print();
else solve(),triple(),print();
}

void solve(){
for(int i=1;i<=z;++i){
int pos=(i%x==0)?(x):(i%x);
add[pos]+='c';
}
ll zmod=z%x,zgap=x-zmod;
ll gapl=x-zgap+1,gapr=x;
for(int i=gapl;i<=gapr;++i){
if(y==0) break;
add[i]+='b';
--y;
}
if(y==0) return;
if(z!=0){
while(1){
for(int i=gapl;i<=gapr;++i){
if(y==0) return;
add[i]+='b';
--y;
}
}
}
else{
for(int i=1;i<=y;++i){
int pos=x-(i%x)+1;
if(pos>x) pos=1;
add[pos]+='b';
}
}
}

multiset<string> que;
void print(){
for(int i=1;i<=x;++i) que.insert(ans[i]);
while(que.size()>=2){
auto itf=que.begin(),itb=que.end();--itb;
string c=(*itf)+(*itb);
que.erase(itf);
que.erase(itb);
que.insert(c);
}
string outputstr=*que.begin();
printf("%s",outputstr.c_str());
}

void reset(){
mapf=mapt=0;
for(int i=0;i<=x+5;++i){
add[i].clear();
ans[i].clear();
}
que.clear();
}

int main(){
int T=read();
while(T--){
x=read();y=read();z=read();
n=x+y+z;
sep();
puts("");
reset();
}
return 0;
}

做完啦哈哈哈


AtCoder - code_festival_2017_qualb_f Largest Smallest Cyclic Shift 题解
https://formu1-github.github.io/Hexo-blog/2025/10/16/AtCoder-code-festival-2017-qualb-f-Largest-Smallest-Cyclic-Shift-题解/
作者
Formu1
发布于
2025年10月16日
许可协议