数位DP get !

· · 个人记录

数位 DP 一般应用于:

  1. 求出在给定区间[A,B]内,符合条件P(i)的数i的个数。

  2. 条件P(i)一般与数的大小无关,而与 数的组成 有关。

先来举个例子:

求区间 [a,b] 中不含 49 的数的个数 (2<a,b<2*10^9)

可以发现, a,b 的范围很大,直接暴力会 T 飞,所以考虑 DP

我们可以枚举每一位进行统计,然后利用类似前缀和的形式给出答案。

f[i][1/0] 表示在第 i 位是 4(1) 不是(0) 的合法数字的个数

code:

int a,b,digital[20],f[20][2];
//digital用于将a,b的每一位拆开存
int dfs(int len, bool if4, bool limit)
{//len为当前是第几位,if4表示上一位是不是4,
 //limit表示之前的数位有没有全部达到上限
    if(len == 0) return 1;
    if(!limit && f[len][if4]) return f[len][if4];
    int ans = 0;
    int maxx = (limit ? digital[len] : 9);
    //如果之前的数位都达到上限了,那这一位就有了限制。
    //比如数字1234,当前搜到了123x,那x最大到4,
    //如果只搜到了122x,那x可以取1~10
    for(int i=0;i<=maxx;i++)
    {
        if(if4 && i==9) continue;
        //上一位为4,而这一位是9,就不合法了。
        ans += dfs(len-1,i==4,limit&&i==maxx);
    }
    return limit ? ans : f[len][if4]=ans;
    //如果没达到限制,转移f的状态,否则不转移。
    //比如数字1234,当前搜到123x,那x的答案数就不能存到f[1]里,
    //如果只搜到122x,x可以取1~10,所以f[1]的值就是x的答案
}

int solve(int x)
{
    int cnt = 0;
    while(x)
    {
        digital[++cnt] = x%10;
        x /= 10;
    }
    return dfs(cnt,0,1);
}

int main(void)
{
    cin >> a >> b;
    cout << solve(b)-solve(a-1);
    return 0;
}

模板:

传送门

这是一道模板题,重点在于前导零的处理:

首先要知道的是,当首位为 0 时,第二位可以选任何数,因为此时的第二位相当于第一位,但此时的状态不能计入 f 数组,因为数字的第一位不可能为 0 ,所以它不属于当前状态。

对于有前导零的转移,可以记一个参数 leading\_zero 表示当前数位之前是不是全为 0 ,如果 leading\_zreo = 1 并且当前位的数 i = 0,那么它的下一位可以选任何数,所以 last 记为 -2 即可。

对了,还是说一下 dfs 中的参数吧:

```cpp int a,b,cnt; int f[MAXN][MAXN]; int digital[MAXN]; int dfs(int len, int last, bool limit, bool leading_zero) { if(!len) return 1; if(!leading_zero && !limit && f[len][last]) return f[len][last]; int flag,ans = 0; int maxn = (limit ? digital[len] : 9); for(int i=0;i<=maxn;i++) { if(abs(last-i)<2) continue; flag = i; if(leading_zero && i==0) flag = -2; ans += dfs(len-1,flag,limit&&(i==maxn),(flag==-2)); } return (limit&&!leading_zero) ? ans : f[len][last]=ans; } int slove(int x) { cnt = 0; while(x) { digital[++cnt] = x%10; x /= 10; } return dfs(cnt,-2,1,1); } signed main(void) { cin >> a >> b; cout << slove(b)-slove(a-1); return 0; } ``` # 提高: [传送门](https://www.luogu.org/problemnew/show/P3413) 一个数中有回文子串的情况有两种: $aba$ 或 $aa$ 其他情况都可以转换为这两种样子,所以,在搜索时要记录上一位和上上位的数。 **定义 $f[i][j][1/0]$ 为在第 $i$ 位上一位数字为 $j$ 时当前位有(1) 无(0) 回文串的数字个数。** ```cpp char a[MAXN],b[MAXN]; int s[MAXN]; int f[MAXN][10][2]; int lena,lenb,r; int dfs(int len, bool limit, int last, int lastlast, int ifh, int lz) { if(len < 1) return ifh; if(!limit && f[len][last][ifh]!=-1) return f[len][last][ifh]; int cnt = 0; int maxn = limit ? s[len] : 9; for(int i=0;i<=maxn;i++) cnt = (cnt+dfs(len-1,(limit&&(i==maxn)),i,lz?last:-1,ifh||((i==last)&&lz)||((i==lastlast)&&lz),(lz||(i!=0)))%p)%p; return (!limit&&lz&&lastlast!=-1) ? f[len][last][ifh]=cnt : cnt; } int solve(int len, char a[]) { memset(f,-1,sizeof f); int shit=0; while(len) { s[++shit] = a[len]-'0'; len--; } if(s[shit] == 0) shit--; return dfs(shit,1,-1,-1,0,0); } int main(void) { cin >> (a+1) >> (b+1); lena = strlen(a+1); lenb = strlen(b+1); r = 0; while(a[lena-r]=='0' && r+1<lena) { a[lena-r] = '9'; r++; } a[lena-r] -= 1; cout << (solve(lenb,b)-solve(lena,a)+p)%p; return 0; } ```