- 给定一个数字串,让你把它划分成若干段,使得每段数字递增(可以有前导\(0\)),且在最后一个数尽可能小的前提下,字典序最大。
- 数据组数\(\le10^3\),\(|S|\le2\times10^3\)
暴力动态规划
首先大体思路自然是先从前往后\(DP\)一遍求出最后一个数最小的取值,再从后往前\(DP\)一遍求出字典序最大的序列。
所以先设\(f_i\)表示以\(i\)为右端点时最大的满足条件的左端点做第一次\(DP\)。
考虑暴力的转移,自然是枚举前一个划分点\(j\),只要满足\([f_j,j]\)对应的数比\([j+1,i]\)对应的数小,就可以给\(f_i\)一个\(j+1\)的贡献。
然后我们发现这个贡献并不取决于\(i\),只是取决于\(j\)。
所以我们使用刷表法转移,变成从每一个\(j\),去找比\([f_j,j]\)大的\([j+1,i]\),对它们产生一个相同的贡献\(j+1\)。
二分+哈希比大小
注意这道题的一大坑点是可能有前导\(0\)。
所以我们记录\(pre_i\)和\(nxt_i\)表示每个位置前一个/后一个非零数的位置(包括自身)。
那么对于一个从\(j\)的转移,显然如果除去前导之后,\([f_j,j]\)比\([j+1,i]\)的位数小,就必然满足转移条件。
因此只要找到\([f_j,j]\)和\([j+1,i]\)位数恰好一样的\(i\),显然根据我们预处理出的\(nxt\)数组,两者除去前导零应该分别对应着\([nxt_{f_j},j]\)和\([nxt_{j+1},i]\),因此\(j-nxt_{f_j}=i-nxt_{j+1}\Leftrightarrow i=nxt_{j+1}+j-nxt_{f_i}\)。
要比较这两个数的大小,只要二分+哈希即可,而更大的\(i\)必然全都可以转移。
第二次\(DP\)(简略版)
第二次\(DP\)与第一次大体相同,从第一次的\(f_n\)位置开始向前转移保证最后一个数尽可能小,并设新的\(f_i\)表示以\(i\)为左端点时最大的满足条件的右端点。
初始时还要注意对最后一个数的前导零初始化\(f_i=n\)。
但是,第二次\(DP\)在转移过程中对于前导零的处理可能和第一次有些差异,具体细节可见代码,这里就不具体展开了。
线段树优化\(DP\)
发现我们相当于每次将一段后缀内的数向一个数取\(\max\),可以无脑上线段树。
虽然第一次\(DP\)中将一段后缀内的数打标记是不用撤销的,不用线段树。但由于第二次\(DP\)的时候是从后往前,后缀的标记就可能消失,反正前后缀需要的操作都是区间取\(\max\)、单点求值,索性写在一起了。
代码:\(O(Tnlogn)\)
#include<bits/stdc++.h>
#define Tp template<typename Ty>
#define Ts template<typename Ty,typename... Ar>
#define Reg register
#define RI Reg int
#define Con const
#define CI Con int&
#define I inline
#define W while
#define N 2000
using namespace std;
int n,f[N+5],pre[N+5],nxt[N+5];char s[N+5];
struct Hash
{
#define ull unsigned long long
#define CU Con ull&
ull x,y;I Hash() {x=y=0;}I Hash(CU a) {x=y=a;}I Hash(CU a,CU b):x(a),y(b){}
I Hash operator + (Con Hash& o) Con {return Hash(x+o.x,y+o.y);}
I Hash operator - (Con Hash& o) Con {return Hash(x-o.x,y-o.y);}
I Hash operator * (Con Hash& o) Con {return Hash(x*o.x,y*o.y);}
I bool operator == (Con Hash& o) Con {return x==o.x&&y==o.y;}
}h[N+5],pw[N+5],seed(302627441,11743207);
class SegmentTree
{
private:
#define PT CI l=1,CI r=n,CI rt=1
#define LT l,mid,rt<<1
#define RT mid+1,r,rt<<1|1
int V[N<<2];
public:
I void Build(PT) {if(V[rt]=1,l==r) return;RI mid=l+r>>1;Build(LT),Build(RT);}
I int Q(CI x,PT) {if(l==r) return V[rt];RI mid=l+r>>1;return max(x<=mid?Q(x,LT):Q(x,RT),V[rt]);}//单点询问
I void U(CI L,CI R,CI v,PT)//区间取min,标记永久化
{
if(v<V[rt]) return;if(L<=l&&r<=R) return (void)(V[rt]=max(V[rt],v));
RI mid=l+r>>1;L<=mid&&(U(L,R,v,LT),0),R>mid&&(U(L,R,v,RT),0);
}
}S;
I bool Chk(CI l1,CI r1,CI l2,CI r2)//二分+哈希比大小
{
#define G(x,y) (h[y]-h[(x)-1]*pw[(y)-(x)+1])
if((r1-l1)^(r2-l2)) return r1-l1<r2-l2;if(G(l1,r1)==G(l2,r2)) return 0;RI l=0,r=r1-l1,mid;//判断长度不同和两数相同
W(l^r) mid=l+r-1>>1,G(l1,l1+mid)==G(l2,l2+mid)?l=mid+1:r=mid;return s[l1+r]<s[l2+r];//二分最大的相同位,比较后一位
}
int main()
{
RI i,t;W(~scanf("%s",s+1))
{
for(n=strlen(s+1),i=n,t=n+1;i;--i) s[i]^'0'&&(t=i),nxt[i]=t;
for(pw[0]=i=1,t=0;i<=n;++i) h[i]=h[i-1]*seed+s[i],pw[i]=pw[i-1]*seed,s[i]^'0'&&(t=i),pre[i]=t;
for(S.Build(),i=1;i<=n;++i) f[i]=S.Q(i),//第一遍DP
(t=nxt[i+1]+i-nxt[f[i]])<=n&&(!Chk(nxt[f[i]],i,nxt[i+1],t)&&++t,t<=n&&(S.U(t,n,i+1),0));//向后转移,特判相同长度
for(S.Build(),S.U(pre[f[n]-1]+1,n,n),i=f[n];i;--i) f[i]=i^f[n]?S.Q(i):n,//第二遍DP
t=pre[max(i-(f[i]-nxt[i]+1)-1,0)]+1,!Chk(nxt[t],i-1,nxt[i],f[i])&&(t=nxt[t]+1),t<i&&(S.U(t,i-1,i-1),0);//向前转移,特判相同长度
for(i=t=1;i<=n;++i) putchar(s[i]),i==f[t]&&i^n&&(putchar(','),t=i+1);putchar('\n');//输出答案,每段右端点处输出逗号
}return 0;
}