洛谷 P2282 [HNOI2003] 历史年份 题解

Announcement

  • Programmed on 2024/3/1 & 2024/3/2
  • Written on 2024/3/2

题目来源

Description

给定一个仅由数字构成的字符串 s s s,用 , 将其划分为若干个正整数,使其严格递增。

求在满足最后一个数字最小的情况下,字典序最大(第一个数最大,在此基础上第二个数最大……)的方案。

  • 简单版: ∣ s ∣ ≤ 500 |s|\le500 s500
  • 进阶版:有 T T T 组询问( T ≤ 1000 T\le1000 T1000),且每组数据均满足 ∣ s ∣ ≤ 2000 |s|\le2000 s2000

前置知识(进阶版)

字符串哈希

给定一个质数 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[i1]×p+s[i]c0)modq,这里的 c 0 c_0 c0 可以是字符 0A 等,根据需要选取。

一般而言 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[lr] 的哈希值呢?

结论是 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]×prl+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[xy] 表示为 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[j1]j1]<s[ij]}
初始化 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[ij]<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(s3),可以通过简单版。

进阶版

然而,单次询问时间复杂度其实可以优化到 O ( ∣ s ∣ log ⁡ ∣ s ∣ ) O(|s|\log|s|) O(slogs)

首先发现比较字符串大小可以通过哈希二分两个字符串相同的前缀,进而比较大小,可以降到 O ( log ⁡ ∣ s ∣ ) O(\log|s|) O(logs)

而转移过程显然是需要优化的。

我们改变转移方式,变为主动转移。

先以 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{jj[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{jj[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 jr[i+1]+1>ir[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+1x]>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,i1] 的长度小于 [ i , g [ i ] ] [i,g[i]] [i,g[i]],充分条件为 [ j , i − 1 ] [j,i-1] [j,i1] 的长度小于等于 [ 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 i1r[j]+1g[i]r[i]+1 r [ j ] ≥ i − 1 + r [ i ] − g [ i ] r[j]\ge i-1+r[i]-g[i] r[j]i1+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 jl[i1+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] ji1+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[i1+r[i]g[i]1]+1],r[l[i1+r[i]g[i]1]+1]]) r r r 值都相同,都为 i − 1 + r [ i ] − g [ i ] i-1+r[i]-g[i] i1+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[i1+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[i1+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[ig[i]]>s[xi1](用字符串哈希比较),则令 ∀ 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]=i1+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]>i1+r[i]g[i]

注意类似简单版中的前导零等问题,这里不再赘述。

最后输出即可,由于判断字符串大小和线段树只是并列的,总复杂度 O ( T ∣ s ∣ log ⁡ ∣ s ∣ ) O(T|s|\log|s|) O(Tslogs)

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;
}
  • 29
    点赞
  • 20
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值