- 给定一个数字串,让你把它划分成若干段,使得每段数字递增(可以有前导(0)),且在最后一个数尽可能小的前提下,字典序最大。
- 数据组数(le10^3),(|S|le2 imes10^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('
');//输出答案,每段右端点处输出逗号
}return 0;
}