算法-数位dp

算法-数位dp

前置知识:

\(\texttt{dp}\)
\(\texttt{Dfs}\)

参考文献

https://www.cnblogs.com/y2823774827y/p/10301145.html
https://www.luogu.com.cn/blog/mak2333/solution-p2602


\(\texttt{Introduction}\)

数位 \(\texttt{dp}\) 是指求在数位限制下有多少满足要求的数的 \(\texttt{dp}\)。例如,求“在 \([L,R]\) 范围内连续出现过 \(3\)\(3\) 的数”,“相邻两位之间差为质数的 \(5\) 位数”或“在 \([L,R]\) 区间内 \(6\) 出现的次数”。读完这篇文章以后,你就都会做了。

数位 \(\texttt{dp}\) 有两种主要方法:循环递推记忆化搜索

先讲循环递推,例题是数字计数。


\(\texttt{Description}\)

[ZJOI2010]数字计数
求在 \([a,b]\) 区间内的数 \(0\sim9\) 数字分别出现次数,前导 \(0\) 不算。
数据范围:\(1\le a\le b\le 12\)


\(\texttt{Solution}\)

为了讲得更透彻,蒟蒻会把同一个东西用不同的方法多次描述,文章较长,请见谅。

Step 1 预处理

\(sum_{i,j}(1\le i\le 12,1\le j\le 9)\) 表示数字 \(j\) 在满 \(i\) 位整数(\([1,10^i-1]\))中出现的次数。因为除了 \(0\) 以外,\(1\sim 9\) 在这题中其实是一模一样的,所以 \(sum_{i,1}=sum_{i,2}=...=sum_{i,9}\)

所以蒟蒻们还不如直接用 \(sum_i\) 表示 \(sum_{i,j}\),表示数字 \(1\sim9\) 在满 \(i\) 位整数中出现的次数。所以 \(sum_1=1\),因为 \(sum_2\) 可以由 \(sum_1\) 个数前面加 \(0\sim 9\) 递推得,也可以把数放在首位,所以 \[sum_n=10sum_{n-1}+10^{n-1}(2\le n)\]\(sum\) 数列打表出来就是 \(1,20,300,4000,...\)

code

void pro(){ //其实代码很短
    ten[0]=1;//10^0=1
    for(int i=1;i<=12;i++){
        ten[i]=ten[i-1]*10;
        sum[i]=sum[i-1]*10+ten[i-1];
    }
}

Step 2 DP
预处理完 \(sum_i\) 后,可以抓一只 \(p\) 位数 \(n\)\(0\sim9\)\([1,n]\) 中出现的次数。首先设 \(nl_i(1\le i\le p)\) 表示 \(n\) 的从右往左第 \(i\) 位的数字。即 \[nl_i=\lfloor\frac{n \mod 10^i}{10^{i-1}}\rfloor\]

code

int p; lng bit=n;
for(p=0;n;n/=10) nl[++p]=n%10;//最后p就是n的位数

然后令 \(f_j(0\le j\le 9)\) 表示数字 \(j\)\([1,n]\) 中出现的次数。考虑 \([1,n]\)\(i\) 位数中数字 \(j(1\le j\le 9)\) 的出现次数:

  1. 如果 \(j\) 为第 \(i\) 位(从右往左,即最高位,\(j\) 满足 \(1\le j<nl_i\)),则 \(j\) 出现了 \(10^{i-1}\) 次。
  2. 如果 \(j\) 不是第 \(i\) 位(\(j\) 满足 \(1\le j\le 9\)),则 \(j\) 出现了 \(nl_i\times sum_{i-1}\)
  3. 如果 \(j\) 为第 \(i\) 位并且 \(j==nl_i\),则 \(j\) 出现了 \(n \mod 10^{i-1}+1\) 次(包括 \(nl_i0...00\))。

最后的问题——这个我们一直避着的 \(0\) 出现次数怎么算?\[\texttt{0出现次数=别的数出现的次数-前导0总数}\]比如 \(n=1000\),如果考虑前导 \(0\),数就会是 \(0000,0001,0002,0003,0004,...0999,1000\) 这样,有:

  • 对于第 \(i\) 位的前导 \(0\),出现了 \(10^{i-1}\)

又因为 \(p\) 位数就没有前导 \(0\) 了,所以前导 \(0\) 的总数根据 \(p\) 而定,跟 \(nl_i(1\le i\le p)\) 无关。

code

for(int i=p;i>=1;i--){
    for(int j=0;j<=9;j++)
        f[j]+=sum[i-1]*nl[i];
    for(int j=0;j<=nl[i]-1;j++)
        f[j]+=ten[i-1];
    bit-=nl[i]*ten[i-1];//维护bit=n mod 10^(i−1)
    f[nl[i]]+=bit+1;
    f[0]-=ten[i-1];
}

最后,把 \(b\)\(a-1\) 各当做 \(n\) 跑一次数位 \(\texttt{dp}\),作差就是答案。

\(\texttt{Code}\)

#include <bits/stdc++.h>
using namespace std;

//&Start
#define lng long long

//&Debug
void debug(int x,lng*arr){
    for(int i=1;i<=x;i++)
        printf("%lld%c",arr[i],"\n "[i<x]);
}

//&dpight
const int W=15;
lng ten[W],sum[W],fa[10],fb[10];
void pro(){
    ten[0]=1;
    for(int i=1;i<=12;i++){
        ten[i]=ten[i-1]*10;
        sum[i]=sum[i-1]*10+ten[i-1];
    }
}
int nl[W];
void dp(lng n,lng*f){
    int p; lng bit=n;
    for(p=0;n;n/=10) nl[++p]=n%10;
    for(int i=p;i>=1;i--){
        for(int j=0;j<=9;j++)
            f[j]+=sum[i-1]*nl[i];
        for(int j=0;j<=nl[i]-1;j++)
            f[j]+=ten[i-1];
        bit-=nl[i]*ten[i-1];
        f[nl[i]]+=bit+1;
        f[0]-=ten[i-1];
    }
}

//&Main
lng a,b;
int main(){
    scanf("%lld%lld",&a,&b);
    pro();
    dp(a-1,fa), dp(b,fb);
    for(int i=0;i<=9;i++)
        printf("%lld%c",fb[i]-fa[i],"\n "[i<9]);
    return  0;
}

然后是记忆化搜索,例题是\(\texttt{windy}\)数。


\(\texttt{Description}\)

[SCOI2009]windy数
求在 \([A,B]\) 中满足“相邻两个数字之差至少为 \(2\)”的数的数量。
数据范围:\(1\le A\le B\le 2000000000\)


\(\texttt{Solution}\)

有人说记忆化搜索的数位 \(\texttt{dp}\) 就是套模板,但是如果你不懂原理,模板都套不起来。

同理,把求 \([A,B]\) 范围中 \(\texttt{windy}\) 数的数量变成求 \([1,B]\) 中的减去 \([1,A-1]\) 中的。

直接抓 \(p\) 位数 \(n\)\(nl_i\) 表示 \(n\) 从右往左第 \(i\) 位数,代码就不放了。

Step 1 求不满 \(p\)\(\texttt{windy}\) 数数量

\(f_{i,j}\) 表示有 \(i\) 位,最高位是 \(j\)\(\texttt{windy}\) 数数量,所以递推方程明显:

  1. \[f_{1,j}=1(0\le j\le 9)\]
  2. \[f_{i,j}=\sum\limits_{J=0,|j-J|\ge2}^9f_{i-1,J}(2\le i\le p,0\le j\le 9)\]

然后有 \(i(1\le i<p)\) 位的 \(\texttt{windy}\) 数量就为\[\sum\limits_{j=1}^9f_{i,j}\]

code

void Pre(){
    for(int j=0;j<=9;j++) f[1][j]=1;
    for(int i=2;i<=10;i++)
        for(int j=0;j<=9;j++)
            for(int J=0;J<=9;J++)
                if(abs(J-j)>=2) f[i][j]+=f[i-1][J];
}
//...
lng DP(lng n){
    int p; lng res=0;
    for(p=0;n;n/=10) nl[++p]=n%10;
    //...
    for(int i=p-1;i>=1;i--)
        for(int j=1;j<=9;j++)
            res+=f[i][j];
    return res;
}

Step 2 求 \(p\)\(\texttt{windy}\) 数数量

记忆化搜索上场。\[\texttt{Dfs(}int ~w,int~d,bool~free\texttt{)}\]表示当前要求从右往左第 \(w\) 位,第 \(w+1\) 位是 \(d\)\(free\) 表示前面从左往右的 \(p-w\) 位是否\(n\) 的前 \(p-w\) 位相同。从 \(w\) 递归到 \(w-1\)\(\texttt{Dfs}\) 的值表示这样的 \(\texttt{windy}\) 数数量。

首先因为同理在这题中 \(0\sim9\) 也是几乎相同的,除了顶到 \(nl_i\) 的情况。所以把除了 \(free==0\) 以外的状态 \((w,d)\) 的答案用记忆化搜索的数组 \[g_{w,d}=\texttt{Dfs(}w,d,0\texttt{)}\] 记录下来。刚开始时 \(g_{w,d}=-1(1\le w\le p,0\le d\le 9)\),如果某次 \(\texttt{Dfs}\) 中发现已经 \(g_{w,d}\neq-1\),就直接返回 \(g_{w,d}\) 的值。

如果 \(w==0\)\(return~1\),具体递归数位 \(\texttt{dp}\) 的方法看代码。

code

lng Dfs(int w,int d,bool free){
    if(!w) return 1;
    if(free&&~g[w][d]) return g[w][d];
    //输出记忆答案,~x为真表示x!=-1
    int up=free?9:nl[w]; lng res=0; //up是递归的下一个d最大值
    for(int i=0;i<=up;i++)
        if(abs(i-d)>=2)//满足windy数要求
            res+=Dfs(w-1,i,free||i<up);//递归
    if(free) g[w][d]=res; //储存记忆
    return res;
}
lng DP(lng n){
    int p; lng res=0;
    for(p=0;n;n/=10) nl[++p]=n%10;
    memset(g,-1,sizeof g);//初始化
    for(int i=1;i<=nl[p];i++) res+=Dfs(p-1,i,i<nl[p]);
    //第一位的取值为[1,nl[p]]
    for(int i=p-1;i>=1;i--)
        for(int j=1;j<=9;j++)
            res+=f[i][j];//不足p位的windy数总数
    return res;
}

\(\texttt{Code}\)

#include <bits/stdc++.h>
using namespace std;

//%Start
#define lng long long

//%dp
const int W=15,D=10;
int nl[W];
lng a,b,f[W][D],g[W][D];
void Pre(){
    for(int j=0;j<=9;j++) f[1][j]=1;
    for(int i=2;i<=10;i++)
        for(int j=0;j<=9;j++)
            for(int J=0;J<=9;J++)
                if(abs(J-j)>=2) f[i][j]+=f[i-1][J];
}
lng Dfs(int w,int d,bool free){
    if(!w) return 1;
    if(free&&~g[w][d]) return g[w][d];
    int up=free?9:nl[w]; lng res=0;
    for(int i=0;i<=up;i++)
        if(abs(i-d)>=2)
            res+=Dfs(w-1,i,free||i<up);
    if(free) g[w][d]=res;
    return res;
}
lng DP(lng n){
    int p; lng res=0;
    for(p=0;n;n/=10) nl[++p]=n%10;
    memset(g,-1,sizeof g);
    for(int i=1;i<=nl[p];i++) res+=Dfs(p-1,i,i<nl[p]);
    for(int i=p-1;i>=1;i--)
        for(int j=1;j<=9;j++)
            res+=f[i][j];
    return res;
}

//%Main
int main(){
    scanf("%lld%lld",&a,&b);
    Pre();
    printf("%lld\n",DP(b)-DP(a-1));
    return 0;
}

然后放道例题,手机号码。


\(\texttt{Description}\)

[CQOI2016]手机号码
求在 \([L,R]\) 中,满足:

  1. 不能同时有 \(4\)\(8\)
  2. 出现过 \(3\) 个连续相同数。

\(11\) 位数个数。
数据范围:\(10^{10}\le L\le R<10^{11}\)


\(\texttt{Solution}\)

用记忆化搜索好,用循环递推代码至少 \(100\) 行。

\[\texttt{Dfs(}int~w,int~d,int~ld,bool~free,bool~h4,bool~h8,bool~h3\texttt{)}\]

要找从右往左第 \(w\) 位的数。
个数(从右往左第 \(w+1\) 个数)是 \(d\)
上上个数(从右往左第 \(w+2\) 个数)是 \(ld\)
\(free\) 表示前 \(p-w\) 位是否\(n\) 的前 \(p-w\) 位相同。
\(h4\) 表示 \(4\) 是否在前 \(p-w\) 位中出现过。
\(h8\) 表示 \(8\) 是否在前 \(p-w\) 位中出现过。
\(h3\) 表示 \(3\) 个连续相同数是否在前 \(p-w\) 位中出现过。

然后用记忆化搜索数组 \(f_{w,d,ld,h4,h8,h3}\) 储存 \(\texttt{Dfs}\) 值(注意了,不能缺斤少两,不能用 \(f_{w,d,h4,h8,h3}\),必须把所以状态作为下标!),然后类似 \(\texttt{windy}\) 数地 \(\texttt{Dfs}\) 一下。具体见代码。

code

lng Dfs(int w,int d,int ld,bool free,bool h4,bool h8,bool h3){
    if(h4&&h8) return 0ll;//剪枝,如果4和8已经同时
    if(!w) return 1ll*h3;//如果w==0并且h3==1,return 1
    if(free&&~f[w][d][ld][h4][h8][h3]) return f[w][d][ld][h4][h8][h3];
    //输出记忆答案
    int up=(free?9:nl[w]); lng res=0;//up是下一个d的最大值
    for(int i=0;i<=up;i++)
        res+=Dfs(w-1,i,d,free||i<up,h4||(i==4),h8||(i==8),h3||(i==d&&i==ld));
    //递归,如果i==d&&i==ld,h3=1
    if(free) f[w][d][ld][h4][h8][h3]=res;//储存答案
    return res;
}

然后这题还有一个坑点,因为最后答案是 \(DP(R)-DP(L-1)\),而 \(L-1\) 可能是 \(10\) 位数,所以 \(\texttt{dp(n)}\) 时特判,如果 \(p\neq 11\)\(\texttt{dp(n)}=0\)

code

lng DP(lng n){
    int p; lng res=0;
    for(p=0;n;n/=10) nl[++p]=n%10;
    if(p!=11) return 0;
    // debug(p,nl);
    memset(f,-1,sizeof f);
    for(int i=1;i<=nl[p];i++)//只有11位数
        res+=Dfs(p-1,i,-1,i<nl[p],(i==4),(i==8),0);
    return res;
}

\(\texttt{Code}\)

#include <bits/stdc++.h>
using namespace std;

//^Start
#define lng long long

//^Debug
void debug(int x,int*arr){
    for(int i=1;i<=x;i++)
        printf("%d%c",arr[i],"\n "[i<x]);
}

//^DP
const int W=15,D=10;
int nl[W];
lng f[W][D][D+1][2][2][2];
void Pre(){/*Nothing*/}   
lng Dfs(int w,int d,int ld,bool free,bool h4,bool h8,bool h3){
    if(h4&&h8) return 0ll;
    if(!w) return 1ll*h3;
    if(free&&~f[w][d][ld][h4][h8][h3]) return f[w][d][ld][h4][h8][h3];
    int up=(free?9:nl[w]); lng res=0;
    for(int i=0;i<=up;i++)
        res+=Dfs(w-1,i,d,free||i<up,h4||(i==4),h8||(i==8),h3||(i==d&&i==ld));
    if(free) f[w][d][ld][h4][h8][h3]=res;
    return res;
}
lng DP(lng n){
    int p; lng res=0;
    for(p=0;n;n/=10) nl[++p]=n%10;
    if(p!=11) return 0;
    // debug(p,nl);
    memset(f,-1,sizeof f);
    for(int i=1;i<=nl[p];i++)
        res+=Dfs(p-1,i,10,i<nl[p],(i==4),(i==8),0);
    return res;
}

//^Main
lng L,R;
int main(){
    scanf("%lld%lld",&L,&R);
    Pre();
    printf("%lld\n",DP(R)-DP(L-1));
    return 0;
}

到此,我们可以总结出记忆化搜索版数位 \(\texttt{dp}\) 的模板了。

\(\texttt{Code}\)

#include <bits/stdc++.h>
using namespace std;

//^Start
#define lng long long

//^DP
const int W=15,D=10;
int nl[W];
lng f[W][]...[][][];
void Pre(){
    /*
    写些预处理
    */
}   
lng Dfs(int w,/*w+1位等相关的数字*/,bool free,/*布尔类型的要求*/){
    if(/*已经不符合*/) return 0;
    if(!w&&/*符合*/) return 1;
    if(free&&~f[w][]...[][][]) return f[w][]...[][][];
    int up=(free?9:nl[w]); lng res=0;
    for(int i=0;i<=up;i++)
        res+=Dfs(w-1,/*相关数组递推*/,free||i<up,/*要求完成递推*/);
    if(free) f[w][]...[][][]=res;
    return res;
}
lng DP(lng n){
    int p; lng res=0;
    for(p=0;n;n/=10) nl[++p]=n%10;
    if(/*已经不符合*/) return 0;
    memset(f,-1,sizeof f);
    for(int i=1;i<=nl[p];i++)
        res+=Dfs(p-1,/*初始相关数*/,i<nl[p],/*初始要求完成情况*/);
    return res;
}

//^Main
lng L,R;
int main(){
    scanf("%lld%lld",&L,&R);
    Pre();
    printf("%lld\n",DP(R)-DP(L-1));
    return 0;
}

两种数位 \(\texttt{dp}\) 那种好?

本蒟蒻认为记忆化搜索好,毕竟时间复杂度、空间复杂度两种都没什么区别,但 \(\texttt{Dfs}\) 又好想,代码又短,而且还有模板。要说数位 \(\texttt{dp}\) 的时间复杂度和空间复杂度,是根据题目而定的,并且除非特别毒瘤的题目,绝对不会 \(\texttt{TLE}\)\(\texttt{MLE}\) 什么的。


练习题

数位 \(\texttt{dp}\) 的题到处都是,我想我也没有提供练习题的必要。


然后我就讲完了,祝大家学习愉快!

猜你喜欢

转载自www.cnblogs.com/Wendigo/p/12523331.html
今日推荐