Announcement
- Programmed on
2024/3/1
&2024/3/2
- Written on
2024/3/2
题目来源
Description
给定一个仅由数字构成的字符串
s
s
s,用 ,
将其划分为若干个正整数,使其严格递增。
求在满足最后一个数字最小的情况下,字典序最大(第一个数最大,在此基础上第二个数最大……)的方案。
- 简单版: ∣ s ∣ ≤ 500 |s|\le500 ∣s∣≤500。
- 进阶版:有 T T T 组询问( T ≤ 1000 T\le1000 T≤1000),且每组数据均满足 ∣ s ∣ ≤ 2000 |s|\le2000 ∣s∣≤2000。
前置知识(进阶版)
字符串哈希
给定一个质数 p p p 和一个模数 q q q,并将字符串变为一个 p p p 进制数,并用前缀和数组 H a s h Hash Hash 记录。
则
H
a
s
h
[
i
]
=
(
H
a
s
h
[
i
−
1
]
×
p
+
s
[
i
]
−
c
0
)
m
o
d
q
Hash[i]=(Hash[i-1]\times p+s[i]-c_0)\bmod q
Hash[i]=(Hash[i−1]×p+s[i]−c0)modq,这里的
c
0
c_0
c0 可以是字符 0
或 A
等,根据需要选取。
一般而言
p
=
131
or
13331
p=131~\text{or}~13331
p=131 or 13331,
q
=
2
64
q=2^{64}
q=264 时较不容易冲突(可直接用 unsigned long long
存储),之后就特意不写出取模了。
那么如何求出 s [ l ⋯ r ] s[l\cdots r] s[l⋯r] 的哈希值呢?
结论是 H a s h [ r ] − H a s h [ l ] × p r − l + 1 Hash[r]-Hash[l]\times p^{r-l+1} Hash[r]−Hash[l]×pr−l+1。
举个例子,对于 a b c d e abcde abcde, H a s h ( d e ) = H a s h ( a b c d e ) − H a s h ( a b c 00 ) = H a s h ( a b c d e ) − H a s h ( a b c ) × p 2 Hash(de)=Hash(abcde)-Hash(abc00)=Hash(abcde)-Hash(abc)\times p^2 Hash(de)=Hash(abcde)−Hash(abc00)=Hash(abcde)−Hash(abc)×p2,这里的 0 0 0 表示空字符(之前的 c 0 c_0 c0)。
Solution
简单版
首先考虑简单版。
设 f [ i ] f[i] f[i] 表示把前 i i i 个数按照条件划分后,最后一个数字最大的起始下标为 f [ i ] f[i] f[i](此时最后一个最小)。
若将
s
[
x
⋯
y
]
s[x\cdots y]
s[x⋯y] 表示为
s
s
s 中下标属于
x
x
x 到
y
y
y 的字符组成的数字,转移方程:
f
[
i
]
=
max
{
j
∣
j
∈
[
1
,
i
]
∩
Z
,
s
[
f
[
j
−
1
]
⋯
j
−
1
]
<
s
[
i
⋯
j
]
}
f[i]=\max\{j\big|j\in[1,i]\cap\Z,s[f[j-1]\cdots j-1]<s[i\cdots j]\}
f[i]=max{j
j∈[1,i]∩Z,s[f[j−1]⋯j−1]<s[i⋯j]}
初始化
f
[
1
]
=
1
f[1]=1
f[1]=1,这样我们就求出了最后一个数的最小值,接着考虑字典序的限制。
设 g [ i ] g[i] g[i] 表示把 [ i , n ] [i,n] [i,n] 按照条件划分后,在保证最后一个数字为之前求出的最小值 s [ f [ n ] ⋯ n ] s[f[n]\cdots n] s[f[n]⋯n] 的前提下,第一个数字最大的终止下标为 g [ i ] g[i] g[i](此时第一个数最大)。
转移方程:
g
[
i
]
=
max
{
j
∣
j
∈
[
i
,
∣
s
∣
]
∩
Z
,
s
[
i
⋯
j
]
<
s
[
j
+
1
,
g
[
j
+
1
]
]
}
g[i]=\max\{j\big|j\in\big[i,|s|\big]\cap\Z,s[i\cdots j]<s[j+1,g[j+1]]\}
g[i]=max{j
j∈[i,∣s∣]∩Z,s[i⋯j]<s[j+1,g[j+1]]}
正序求
f
[
i
]
f[i]
f[i],再倒序求
g
[
i
]
g[i]
g[i]。
初始化 g [ f [ n ] ] = n g[f[n]]=n g[f[n]]=n,注意 f [ n ] f[n] f[n] 前面连续的一段零也需要令其 g [ i ] = n g[i]=n g[i]=n,因为前导零对最后一个数字的大小无影响,且还能使 g [ i ] g[i] g[i] 变大,从而更有可能转移。
最后输出即可。
比较两个字符串需要 O ( ∣ s ∣ ) O(|s|) O(∣s∣),枚举 f [ i ] , g [ i ] f[i],g[i] f[i],g[i] 的下标需要 O ( ∣ s ∣ ) O(|s|) O(∣s∣),转移枚举需要 O ( ∣ s ∣ ) O(|s|) O(∣s∣),总的需要 O ( ∣ s ∣ 3 ) O(|s|^3) O(∣s∣3),可以通过简单版。
进阶版
然而,单次询问时间复杂度其实可以优化到 O ( ∣ s ∣ log ∣ s ∣ ) O(|s|\log|s|) O(∣s∣log∣s∣)。
首先发现比较字符串大小可以通过哈希二分两个字符串相同的前缀,进而比较大小,可以降到 O ( log ∣ s ∣ ) O(\log|s|) O(log∣s∣)。
而转移过程显然是需要优化的。
我们改变转移方式,变为主动转移。
先以 f [ i ] f[i] f[i] 为例,考虑对于 i i i 能够转移至哪些下标。
一个下标 j j j 能够被 i i i 转移,其必要条件为 [ f [ i ] , i ] [f[i],i] [f[i],i] 的长度小于 [ i + 1 , j ] [i+1,j] [i+1,j],充分条件为 [ f [ i ] , i ] [f[i],i] [f[i],i] 的长度小于等于 [ i + 1 , j ] [i+1,j] [i+1,j]。
然而由于可能存在前导零,我们可以引入 l [ i ] l[i] l[i] 表示第 i i i 个位置及之前第一个不为 0 0 0 的位置下标,即 l [ i ] = max { j ∣ j ∈ [ 1 , i ] ∩ Z , s [ j ] ! = 0 } l[i]=\max\{j|j\in[1,i]\cap\Z,s[j]!=0\} l[i]=max{j∣j∈[1,i]∩Z,s[j]!=0}; r [ i ] r[i] r[i] 表示第 i i i 个位置及之后第一个不为 0 0 0 的位置下标,即 r [ i ] = min { j ∣ j ∈ [ i , ∣ s ∣ ] ∩ Z , s [ j ] ! = 0 } r[i]=\min\{j|j\in\big[i,|s|\big]\cap\Z,s[j]!=0\} r[i]=min{j∣j∈[i,∣s∣]∩Z,s[j]!=0}。
因此可以得出 j j j 能被转移的必要条件为 j − r [ i + 1 ] + 1 > i − r [ f [ i ] ] + 1 j-r[i+1]+1>i-r[f[i]]+1 j−r[i+1]+1>i−r[f[i]]+1 即 j > i + r [ i + 1 ] − r [ f [ i ] ] j>i+r[i+1]-r[f[i]] j>i+r[i+1]−r[f[i]]。
我们需要对 j = i + r [ i + 1 ] − r [ f [ i ] ] j=i+r[i+1]-r[f[i]] j=i+r[i+1]−r[f[i]] 的情况进行判断,设 x = i + r [ i + 1 ] − r [ f [ i ] ] x=i+r[i+1]-r[f[i]] x=i+r[i+1]−r[f[i]],则:
- 若 s [ i + 1 ⋯ x ] > s [ f [ i ] ⋯ i ] s[i+1\cdots x]>s[f[i]\cdots i] s[i+1⋯x]>s[f[i]⋯i](用字符串哈希比较),则令 ∀ j ∈ [ x , ∣ s ∣ ] , f [ j ] = max ( f [ j ] , f [ i ] ) \forall j\in\big[x,|s|\big],f[j]=\max(f[j],f[i]) ∀j∈[x,∣s∣],f[j]=max(f[j],f[i]);
- 否则,令 ∀ j ∈ ( x , ∣ s ∣ ] , f [ j ] = max ( f [ j ] , f [ i ] ) \forall j\in\big(x,|s|\big],f[j]=\max(f[j],f[i]) ∀j∈(x,∣s∣],f[j]=max(f[j],f[i])。
于是我们发现,可以利用线段树优化转移过程,区间只需记录最大值和懒标记即可。
同理,对于 g [ i ] g[i] g[i], j j j 能被 i i i 转移,其必要条件为 [ j , i − 1 ] [j,i-1] [j,i−1] 的长度小于 [ i , g [ i ] ] [i,g[i]] [i,g[i]],充分条件为 [ j , i − 1 ] [j,i-1] [j,i−1] 的长度小于等于 [ i , g [ i ] ] [i,g[i]] [i,g[i]]。
因此可以得出 j j j 能被转移的充分条件为 i − 1 − r [ j ] + 1 ≥ g [ i ] − r [ i ] + 1 i-1-r[j]+1\ge g[i]-r[i]+1 i−1−r[j]+1≥g[i]−r[i]+1 即 r [ j ] ≥ i − 1 + r [ i ] − g [ i ] r[j]\ge i-1+r[i]-g[i] r[j]≥i−1+r[i]−g[i]。
似乎无法求出 j j j 的取值范围?
其实不然,我们发现可以推出 j j j 能被转移的充分条件为 j ≥ l [ i − 1 + r [ i ] − g [ i ] − 1 ] + 1 j\ge l[i-1+r[i]-g[i]-1]+1 j≥l[i−1+r[i]−g[i]−1]+1。
为什么呢?若 j j j 左侧不是 0 0 0 则 j ≥ i − 1 + r [ i ] − g [ i ] j\ge i-1+r[i]-g[i] j≥i−1+r[i]−g[i],与上面的式子相符;否则,由于 [ l [ i − 1 + r [ i ] − g [ i ] − 1 ] + 1 ] , r [ l [ i − 1 + r [ i ] − g [ i ] − 1 ] + 1 ] ] ) \Big[l[i-1+r[i]-g[i]-1]+1],r\big[l[i-1+r[i]-g[i]-1]+1]\big]\Big) [l[i−1+r[i]−g[i]−1]+1],r[l[i−1+r[i]−g[i]−1]+1]]) 的 r r r 值都相同,都为 i − 1 + r [ i ] − g [ i ] i-1+r[i]-g[i] i−1+r[i]−g[i],也相符。
我们需要对 j = l [ i − 1 + r [ i ] − g [ i ] − 1 ] + 1 j=l[i-1+r[i]-g[i]-1]+1 j=l[i−1+r[i]−g[i]−1]+1 的情况进行判断,设 x = l [ i − 1 + r [ i ] − g [ i ] − 1 ] + 1 x=l[i-1+r[i]-g[i]-1]+1 x=l[i−1+r[i]−g[i]−1]+1,则:
- 若 s [ i ⋯ g [ i ] ] > s [ x ⋯ i − 1 ] s[i\cdots g[i]]>s[x\cdots i-1] s[i⋯g[i]]>s[x⋯i−1](用字符串哈希比较),则令 ∀ j ∈ [ x , i ) , g [ j ] = max ( g [ j ] , g [ i ] ) \forall j\in\big[x,i\big),g[j]=\max(g[j],g[i]) ∀j∈[x,i),g[j]=max(g[j],g[i]);
- 否则,令 ∀ j ∈ ( r [ x ] , i ) , g [ j ] = max ( g [ j ] , g [ i ] ) \forall j\in\big(r[x],i\big),g[j]=\max(g[j],g[i]) ∀j∈(r[x],i),g[j]=max(g[j],g[i]),因为 r [ x ] r[x] r[x] 即为满足 r [ j ] = i − 1 + r [ i ] − g [ i ] r[j]=i-1+r[i]-g[i] r[j]=i−1+r[i]−g[i] 的最大下标 j j j ,加 1 1 1 就可以使得 r [ j ] > i − 1 + r [ i ] − g [ i ] r[j]>i-1+r[i]-g[i] r[j]>i−1+r[i]−g[i]。
注意类似简单版中的前导零等问题,这里不再赘述。
最后输出即可,由于判断字符串大小和线段树只是并列的,总复杂度 O ( T ∣ s ∣ log ∣ s ∣ ) O(T|s|\log|s|) O(T∣s∣log∣s∣)。
Conclusion
- 字符串比较可以用二分+哈希从 O ( n ) O(n) O(n) 优化到 O ( log n ) O(\log n) O(logn)。
- 动态规划转移时,可以考虑主动、被动两种不同的方式,进而找到不同的优化方法。
Code
简单版
#include <bits/stdc++.h>
using namespace std;
int n,f[505],g[505]; // f[i]-把s[1~i]划分为递增数列,最后一个数最小为s[f[i]~i];g[i]-在最后一个数最小的情况之下,把s[i~n]划分为递增数列,第一个数最大为s[i~g[i]]
char s[505];
string get(int x,int y){
bool Flag=0;
string res="";
for (int i=x;i<=y;i++){
if (s[i]!='0') Flag=1;
if (Flag) res+=s[i];
}
return res;
}
bool cmp(string x,string y){
if (x.size()!=y.size()) return x.size()<y.size();
return x<y;
}
int main(){
scanf(" %s",s+1),n=strlen(s+1);
f[1]=1;
for (int i=2;i<=n;i++) for (int j=i;j;j--) if (cmp(get(f[j-1],j-1),get(j,i))){f[i]=j;break;}
int t=0;
for (int i=f[n]-1;i;i--)
if (s[i]=='0') g[i]=n;
else{t=i;break;}
g[f[n]]=n;
for (int i=t;i>0;i--) for (int j=f[n]-1;j>=i;j--) if (cmp(get(i,j),get(j+1,g[j+1]))){g[i]=j;break;}
for (int i=1;i<=n;i=g[i]+1){
for (int j=i;j<=g[i];j++) putchar(s[j]);
if (g[i]!=n) putchar(',');
}
return 0;
}
进阶版
#include <bits/stdc++.h>
using namespace std;
int n,f[2005],g[2005],l[2005],r[2005]; // l[i]-i左侧第一个非零的位置的下标 r[i]-右侧
unsigned long long Hash[2005],power[2005];
const int p=131;
char s[2005];
unsigned long long getHash(int l,int r){
if (l>r) return 0;
return Hash[r]-Hash[l-1]*power[r-l+1]; // 如:s=ABCDE Hash(DE)=HASH(ABCDE)-HASH(ABC)*P^2=HASH(ABCDE)-HASH(ABC00)
}
struct Node{
int l,r,Max,lazy;
}tree[8005];
void pushup(int p){
tree[p].Max=max(tree[p<<1].Max,tree[p<<1|1].Max);
}
void pushdown(int p){
if (tree[p].lazy){
tree[p<<1].lazy=max(tree[p<<1].lazy,tree[p].lazy);
tree[p<<1|1].lazy=max(tree[p<<1|1].lazy,tree[p].lazy);
tree[p<<1].Max=max(tree[p<<1].Max,tree[p].lazy);
tree[p<<1|1].Max=max(tree[p<<1|1].Max,tree[p].lazy);
tree[p].lazy=0;
}
}
void build(int p,int l,int r,int x){
tree[p].l=l,tree[p].r=r,tree[p].lazy=0;
if (l==r){
tree[p].Max=x;
return;
}
int Mid=(l+r)>>1;
build(p<<1,l,Mid,x);
build(p<<1|1,Mid+1,r,x);
pushup(p);
}
int get(int p,int k){
if (tree[p].l==tree[p].r) return tree[p].Max;
pushdown(p);
int Mid=(tree[p].l+tree[p].r)>>1;
if (k<=Mid) return get(p<<1,k);
return get(p<<1|1,k);
}
void update(int p,int l,int r,int d){
if (l<=tree[p].l&&tree[p].r<=r){
tree[p].Max=max(tree[p].Max,d);
tree[p].lazy=max(tree[p].lazy,d);
return;
}
pushdown(p);
int Mid=(tree[p].l+tree[p].r)>>1;
if (l<=Mid) update(p<<1,l,r,d);
if (r>Mid) update(p<<1|1,l,r,d);
pushup(p);
}
bool cmp(int l1,int r1,int l2,int r2){ // [l1,r1]<[l2,r2]?
l1=r[l1],l2=r[l2]; // 去除前导0
if (r1-l1+1!=r2-l2+1) return r1-l1+1<r2-l2+1;
int l=0,r=r1-l1+1;
while (l<r){
int Mid=(l+r+1)>>1;
if (getHash(l1,l1+Mid-1)==getHash(l2,l2+Mid-1)) l=Mid;
else r=Mid-1;
}
return l<r1-l1+1&&s[l1+l]<s[l2+l];
}
int main(){
power[0]=1;
for (int i=1;i<=2000;i++) power[i]=power[i-1]*p;
while (~scanf(" %s",s+1)){
n=strlen(s+1);
for (int i=1;i<=n;i++) Hash[i]=Hash[i-1]*p+s[i]-'0';
for (int i=1;i<=n;i++)
if (s[i]=='0') l[i]=l[i-1];
else l[i]=i;
r[n+1]=n+1;
for (int i=n;i;i--)
if (s[i]=='0') r[i]=r[i+1];
else r[i]=i;
// 正向DP f[i]
build(1,1,n,1); // 初始化:f[i]=1
for (int i=1;i<=n;i++){
f[i]=get(1,i);
int x=i+r[i+1]-r[f[i]];
if (!cmp(f[i],i,i+1,x)) x++;
if (x<=n) update(1,x,n,i+1);
}
// 逆向DP g[i]
build(1,1,n,0);
update(1,l[f[n]-1]+1,n,n);
for (int i=f[n];i;i--){
g[i]=get(1,i);
int x=l[max(r[i]-g[i]+i-1-1,0)]+1;
if (!cmp(x,i-1,i,g[i])) x=r[x]+1;
if (x<=i-1) update(1,x,i-1,i-1);
}
for (int i=1;i<=n;i=g[i]+1){
for (int j=i;j<=g[i];j++) putchar(s[j]);
if (g[i]!=n) putchar(',');
}
puts("");
}
return 0;
}