深度干货 | 万字长文详解数位dp,超详细题集汇总

本文介绍了数位动态规划(DP)在解决特定类型问题时的应用,包括如何定义状态、状态转移方程,以及记忆化搜索的实现。通过多个题目实例,如不要62、定时炸弹、计数问题等,详细阐述了数位DP的解题思路和代码实现,帮助读者掌握数位DP的解决技巧。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

数位dp题集汇总

前置芝士

「数位 DP」属于会就不难,不会就巨难!!本篇文章就来好好详细的总结一下「数位 DP」吧吧吧!!

相信各位小伙伴在学习数位dp过程中,都会感到特别难。但其实数位dp是很简单的,都是有套路的,都可以使用记忆化搜索来求解。在记忆化搜索中,可以使用变量limit来表示是否有限制,而且有限制的一般都是最右侧的那一种分支。对于前导0问题,使用变量lead来表示是否有前导0,一般都是最左侧的那一种分支。从高位枚举到低位,使用变量pos来记录当前枚举到哪一位。

一般使用记忆化搜索的代码:dfs(int pos, bool lead, bool limit)

具体问题具体分析,将需要定义的变量加入到dfs中,lead不是必须的,但是pos和limit都是必要的。

通过前三道题,你就可以大体摸清楚数位dp的套路了,代码模板几乎不变。下面的这些题目就是笔者做过的数位dp习题集,请读者认真阅读,放心食用,从此不怕数位dp的题目。

本文参考博客:传送门

AcWing1085 不要62

题目描述

AcWing1085 不要62

算法思路

记忆化递归在求解时把中间结果记录下来,若某一项的值已经有解,则直接返回,不需要重新计算,从而大大减少计算量。记忆化递归和动态规划(查表法)有异曲同工之妙。

本题考虑另一种枚举方式,从最高位开始往下枚举,在必要时控制上界。

例如,枚举[0, 386]区间的所有数时,首先从百位开始枚举,百位可能是0、1、2、3。枚举时不超过386即可。

  • 百位0: 十位可以是0~9,个位也可以是0~9,枚举没有限制, 因为百位是0时,后面的位数无论是多少,都不可能超过386,相当于枚举000~099。
  • 百位1: 十位可以是0~9,个位也可以是0~9,枚举没有限制,枚举100~199。
  • 百位2: 十位可以是0~9,个位也可以是0~9,枚举没有限制,枚举200~299。
  • 百位3: 十位只可以是0~8,否则超过386,此时是有上界限制的。当十位是0~7时,个位可以是0~9,因为379还是不会超过386。但当十位是8时,个位只可以是0~6,此时有上界限制,相当于枚举300~379、380~386。

我们需要关注以下几个问题。

  1. 记忆化。枚举[0, 386]区间的所有数,当百位是0~2时,十位和个位枚举没有限制,都是一样的,采用记忆化递归,只需计算一次并将结果存储起来,下次判断若已赋值,则直接返回该值即可。百位是3时,十位限制在0~8;十位是0~7时,个位无限制;十位是8时,个位限制在0~6。有限制时,不可以采用记忆化递归,需要继续根据限制枚举。
  2. 上界限制。当高位枚举刚好达到上界时,紧接着的下一位枚举就有上界限制了。可以设置一个变量limit来控制上界限制。
  3. 高位枚举0。为什么高位需要枚举0?这是因为百位枚举0相当于此时枚举的这个数最多是两位数(即0xx这种显然是两位数),若十位继续枚举0,则枚举的是一位数(即00x这种显然是一位数)。枚举小于或等于386的数,一位数、两位数当然也比它小,所以高位要枚举0。
  4. 前导0。有时会有前导0的问题,可以设置一个lead变量表示有前导0。例如统计数字里面0出现的次数。若有前导0,例如008,数字8不包含0,则不应该统计8前面的两个0。若没有前导0,例如108,则应该统计8前面的1个0。

记忆化搜索: 若没有限制且已求解,则直接返回,无须递归。采用记忆化递归方法求解时的算法设计如下。

状态表示

f [ p o s [ s t a ] f[pos[sta] f[pos[sta]表示当前第pos位在sta状态满足条件的元素个数,sta表示前一位是否是6,只有0和1两种状态。

状态转移方程

if (sta && i == 2)  continue;
if (i == 4 ) continue;
ans += dfs(pos - 1, i == 6, limit && i == up);

边界条件

若pos=0,则返回1。

在初始化时,一般limit都初始化为true,表示有限制,sta初始化为false。

示例1: 计算[0, 24]区间不包含4也不包含62的数有多少个,过程如下。

  1. 数字分解:nums[1]=4,nums[2]=2。
  2. 从高位开始,当前位是2,前面一位不是6,有限制up=nums[2]=2,枚举i=0…2,递归求解。
    1. i=0:以0开头的一位数,无限制,up=9,枚举i =0…9,包含00 ~09,除去04, f [ 1 ] [ 0 ] = 9 f[1][0]=9 f[1][0]=9
    2. i=1:以1开头的一位数,无限制且 f [ 1 ] [ 0 ] f[1][0] f[1][0]已赋值,直接返回 9。
    3. i=2:以2开头的一位数,有限制,up=num[1]=4,执行i=0…4,包含20~24,除去24,共有4个数满足条件。
  3. 累加结果,返回[0, 24]区间不包含4也不包含62的22个数, 求解过程如下图所示。

image-20230310175826512

示例2:请尝试计算[0, 386]区间不包含4也不包含62的数有多少个?
在这里插入图片描述

图中无填充色的节点均已有解,直接读取结果,无须再递归求解, 这正是记忆化递归的妙处。这种方式大大提高了算法效率。

算法实现

#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
// nums存储数位
int nums[10], f[10][2];

// f[pos][sta]表示枚举到第pos位,它前面一个位的状态用sta表示,如果前面一个位是6,则sta为true
// limit表示是否有限制 true表示有限制   比如386 那么对于3xx这种就肯定有限制
int dfs(int pos, bool sta, bool limit)
{
    // pos为0表示枚举完了所有数位  这里返回答案1(该返回0还是返回1,根据测试结果来决定)
    if (!pos)   return 1;

    // 当前位没有限制 并且 以及被计算过了 那么直接返回答案
    if (!limit && ~f[pos][sta] )    return f[pos][sta];

    // limit为true 表明存在限制该位只能等于0~nums[pos] 否则该位可以等于0~9;
    int up = limit ? nums[pos] : 9, ans = 0;
    // 有up个分支,可以填的数是0~up
    for (int i = 0; i <= up; i ++ )
    {
        // 前面一个位是6且当前位是2  不符合题意
        if (sta && i == 2)  continue;
        // 数字4不符合题意
        if (i == 4 ) continue;
        // 当前这位数字满足条件继续向下搜索
        // 通过 i==6来更新sta的状态
        // 只有最右侧的分支才会有限制 因此可以通过limit && i == up来更新limit的值
        ans += dfs(pos - 1, i == 6, limit && i == up);
    }

    // 如果没有限制 那么记录一下f[pos][sta]的答案
    // 如果有限制 那么不能直接记录答案 因为它会把之前 没有限制时得到的答案覆盖掉
    if (!limit )    f[pos][sta] = ans;
    return ans;
}

// 求解[0,x]区间内满足条件的个数
int solve(int x)
{
    memset(f, -1, sizeof f);
    int n = 0;
    while (x)   // 数位分解
    {
        nums[ ++ n] = x % 10;
        x /= 10;
    }
    return dfs(n, false, true); // 若不包括0,则此处再减去1即可
}

int main()
{
    int l, r;
    while (cin >> l >> r, l || r) {
        cout << solve(r) - solve(l - 1) << endl;
    }
    return 0;
}

hdu3355 定时炸弹

题目描述

hdu3355 定时炸弹

题意:求解[1,x]中49的出现次数。

比如[1,500]区间包括“49”的数是49、149、249、349、449、 490、491、492、493、494、495、496、497、498、499,所以答案是15。

算法思路

这题可以用两种方法来求解:直接求包含49的元素个数,或者求不包含49的元素个数ans(不包括0),然后输出n-ans即可。第二种方法在上一题已讲解,这里不再赘述。本节介绍第一种方法。

状态表示

f [ p o s ] [ s t a ] f[pos][sta] f[pos][sta]表示当前第pos位在sta状态下满足条件的个数,sta表示前一位是否是4,只有0和1两种状态。

状态转移方程

dfs(int pos, bool sta, bool limit):pos表示枚举到当前位;sta表示前一位是否是4,若前一位是4,则sta=true,否则sta=false。

if (sta && i == 9)
    ans += limit ? n % p[pos - 1] + 1 : p[pos - 1];

其中p[pos]表示 1 0 p o s 10^{pos} 10pos。若无限制,则49后面有多少位,就累加p[pos-1]的个数。例如[1, 500]区间,枚举时49后面还有一个位(可填0~9), 则累加10个包含49的数,分别为490~499。若有限制,则求出49后面的数字再加1。例如[1, 496]区间,枚举时49后面的数位是6,则累加6+1个包含49的数,即490~496。

边界条件

当pos=0时,返回0。

记忆化搜索: 若没有限制且已赋值,则直接返回该值,无须再递归求解。

示例:计算[1, 500]区间有多少个数包含“49”,过程如下。

  1. 数字分解:nums[1]=0,nums[2]=0,nums[3]=5。
  2. 从高位开始,当前位是第三位,前面一位不是4,有限制。up=nums[3]=5,枚举i=0…5。
    1. i=0:以0开头的两位数,无限制,up=9,枚举i=0…9,只有一个数包含“49”,即049,故 f [ 2 ] [ 0 ] = 1 f[2][0]=1 f[2][0]=1
    2. i=1:以1开头的两位数,无限制且 f [ 2 ] [ 0 ] f[2][0] f[2][0]已赋值,返回该值,即149。
    3. i=2:以2开头的两位数,无限制且 f [ 2 ] [ 0 ] f[2][0] f[2][0]已赋值,返回该值,即249。
    4. i=3:以3开头的两位数,无限制且 f [ 2 ] [ 0 ] f[2][0] f[2][0]已赋值,返回该值,即349。
    5. i=4:以4开头的两位数,无限制,up=9,枚举i=0…9。i=4 时,向下找到一个解49,即449;i =9时,累加10个解,即490~499,故 f [ 2 ] [ 1 ] = 11 f[2][1]=11 f[2][1]=11
    6. i=5:以5开头的两位数,有限制,up=nums[2]=0,执行i=0, 递归求解。pos=0时,返回0。
  3. 累加结果,返回[1, 500]区间包含“49”的数15个,求解过程如下图所示。
  4. 在这里插入图片描述

image-20230310181629731

示例2:计算[1, 496]区间有多少个数包含“49”?求解过程如下图所示。

image-20230310181650805

算法实现

思路一

#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
const int N = 30;
typedef long long LL;
// nums存储数位
int nums[N];
LL f[N][2], p[N];
LL x;

LL dfs(int pos, bool sta, bool limit)
{
    if (!pos)   return 0;
    if (!limit && ~f[pos][sta] )    return f[pos][sta];

    int up = limit ? nums[pos] : 9;
    LL ans = 0;
    for (int i = 0; i <= up; i ++ )
    {
        if (sta && i == 9)  ans += limit ? x % p[pos - 1] + 1 : p[pos - 1];
        else    ans += dfs(pos - 1, i == 4, limit & i == up);
    }

    if (!limit )    f[pos][sta] = ans;
    return ans;
}

LL solve(LL x)
{
    memset(f, -1, sizeof f);
    int n = 0;
    while (x)
    {
        nums[ ++ n] = x % 10;
        x /= 10;
    }
    return dfs(n, false, true);
}

int main()
{
    p[0] = 1;
    for (int i = 1; i < N; i ++ )   p[i] = p[i - 1] * 10;
    int T;
    cin >> T;
    while (T -- )
    {
        cin >> x;
        cout << solve(x) << endl;
    }

    return 0;
}

思路二

#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
const int N = 30;
typedef long long LL;
// nums存储数位
int nums[N];
LL f[N][2], p[N];
LL x;

// 求解[0, x]之间不包含49的个数
LL dfs(int pos, bool sta, bool limit)
{
    // 注意是返回1
    if (!pos)   return 1;
    if (!limit && ~f[pos][sta] )    return f[pos][sta];

    int up = limit ? nums[pos] : 9;
    LL ans = 0;
    for (int i = 0; i <= up; i ++ )
    {
        if (sta && i == 9)  continue;
        ans += dfs(pos - 1, i == 4, limit && i == up);
    }

    if (!limit )    f[pos][sta] = ans;
    return ans;
}

// 求解[1, x]之间不包含49的个数
LL solve(LL x)
{
    memset(f, -1, sizeof f);
    int n = 0;
    while (x)
    {
        nums[ ++ n] = x % 10;
        x /= 10;
    }
    // dfs求出的是[0,x]中不包含49的个数 但我们需要计算的是[1,x]中不包含49的个数
    // 因此需要刨去0这一个数
    return dfs(n, false, true) - 1;
}

int main()
{
    p[0] = 1;
    for (int i = 1; i < N; i ++ )   p[i] = p[i - 1] * 10;
    int T;
    cin >> T;
    while (T -- )
    {
        cin >> x;
        // [1,x]中不包含49的个数为solve(x) 那么[1,x]中包含49的个数就是x - solve(x)
        cout << x - solve(x) << endl;
    }

    return 0;
}

POJ3252 Round Numbers

题目描述

POJ3252 Round Numbers

题意:本题求[start, finish]区间Round Numbers的个数,其中Round Numbers是指二进制表示下0的个数大于或等于1的个数,是典型的数位DP问题。

算法思路

状态表示

f [ p o s ] [ x 0 ] [ x 1 ] f[pos][x_0][x_1] f[pos][x0][x1]表示当前数位为pos时Round Numbers的个数,其中pos表示当前数位, x 0 x_0 x0为二进制中0的个数, x 1 x_1 x1为二进制中1的个数。可采用记忆化递归求解。

初始化一般设置含有前导0,即lead为true,并且有限制,即limit为true。

完美图解

计算[0, 5]区间Round Numbers的个数。

  1. 将5转换为二进制数101,nums[0]=1,nums[1]=0,nums[2]=1。
  2. 首先从高位开始,当前位为第二位(下标为2),有前导零;有数位限制,枚举i=0…1。
    1. i=0:有前导0,无限制,包含000、001、010、011四个数,去掉前导零后为0、1、10、11,其中0和10为Round Numbers(0的个数大于或等于1的个数),故 f [ 1 ] [ 0 ] [ 0 ] = 2 f[1][0][0]=2 f[1][0][0]=2
    2. i=1:无前导0,有限制,包含100、101两个数,其中100为Round Numbers,故 f [ 1 ] [ 0 ] [ 1 ] = 1 f[1][0][1]=1 f[1][0][1]=1
  3. 返回 f [ 2 ] [ 0 ] [ 0 ] = f [ 1 ] [ 0 ] [ 0 ] + f [ 1 ] [ 0 ] [ 1 ] = 2 + 1 = 3 f[2][0][0]=f[1][0][0]+f[1][0][1]=2+1=3 f[2][0][0]=f[1][0][0]+f[1][0][1]=2+1=3,表示[0, 5] 区间满足条件的数一共有3个:0、10、100。

求解过程如下图所示。
在这里插入图片描述

算法实现

#include<cstdio>
#include<cstring>
using namespace std;
int f[40][40][40];
int nums[40];

/*求一个区间内Round Numbers(二进制0的个数不小于1的个数)的个数。
  状态f[pos][num0][num1];
  pos为当前数位,num0为二进制中0的个数,num1为二进制中1的个数;
  lead表示前导零;
  limit表示数位限制。
*/

int dfs(int pos, int x0, int x1, bool lead, bool limit)
{
    if (pos == -1)  return x0 >= x1;
    if (!limit && ~f[pos][x0][x1])  return f[pos][x0][x1];

    int up = limit ? nums[pos] : 1, ans = 0;
    for (int i = 0; i <= up; i ++ )
    {
        // 前导0,前面没有1
        if (lead && !i )    ans += dfs(pos - 1, 0, 0, true, limit && i == up);
        // 非前导0,即前面已有1
        else    ans += dfs(pos - 1, x0 + (i == 0), x1 + (i == 1), false, limit && i == up);
    }

    if (!limit )    f[pos][x0][x1] = ans;
    return ans;
}

int solve(int x)
{
    memset(f, -1, sizeof f);
    int n = 0;
    while (x)
    {
        nums[n ++ ] = x % 2;
        x /= 2;
    }
    return dfs(n - 1, 0, 0, true, true);
}

int main()
{
    int l, r;
    while (~scanf("%d%d", &l, &r)) {
        printf("%d\n", solve(r) - solve(l - 1));
    }
    return 0;
}

AcWing338 计数问题

POJ2282

AcWing338 计数问题

题意:本题求[a , b]区间每个数字(0~9)的出现次数,为典型的数位DP问题。

算法思路

状态表示

f [ p o s ] [ x ] [ c n t ] f[pos][x][cnt] f[pos][x][cnt],pos为当前数位,x为当前统计的数字,cnt为当前已统计的x的出现次数。

可采用记忆化递归求解,dfs(int pos, int x, int cnt, bool lead, bool limit),lead表示前导零;limit表示上界限制。

示例1:统计[1, 35]区间数字0的出现次数,过程如下。

  1. 数字分解:nums[1]=5,nums[2]=3。

  2. 统计数字0的出现次数。从高位开始,当前位为第二位(下标为2),有前导零,有限制,up=nums[2]=3,枚举i =0…3。

    1. i=0:有前导零,无限制,枚举i=0…9,包含00~09,去掉前导零后为1~9,数字0出现0次。
    2. i=1:无前导零,无限制,枚举i=0…9,包含10~19,数字0出现1次,故 f [ 1 ] [ 0 ] [ 0 ] f[1][0][0] f[1][0][0]
    3. i=2:无前导零,无限制且 f [ 1 ] [ 0 ] [ 0 ] f[1][0][0] f[1][0][0]已赋值,直接返回该值1。
    4. i=3:无前导零,有限制,up=nums[1]=5,枚举i =0…5,包含30 ~35,数字0出现1次。
  3. 累加结果,返回 f [ 2 ] [ 0 ] [ 0 ] = 3 f[2][0][0]=3 f[2][0][0]=3,表示[1, 35]区间数字0出现 3次:10、20、30。

    求解过程如下图所示。
    在这里插入图片描述

注意: 只有无前导零、无限制且 f [ ] [ ] [ ] f[][][] f[][][]已赋值时,才可以直接返回;只有无前导零且无限制时才可以对dp[][][]赋值;否则在执行完第2层第1个节点dfs(1, 0, 0, 1, 0)后, f [ 1 ] [ 0 ] [ 0 ] f[1][0][0] f[1][0][0]已赋值,到第2层 第2个节点dfs(1, 0, 0, 0, 0)时就直接返回了,这显然不对,因为0的出现次数不同。所以执行完第2层第1个节点dfs(1, 0, 0, 1, 0)后, f [ 1 ] [ 0 ] [ 0 ] f[1][0][0] f[1][0][0]不赋值,只累加结果,执行完第2层第2个节点dfs(1,
0,0, 0, 0)后,无前导零且无限制,直接赋值 f [ 1 ] [ 0 ] [ 0 ] = 1 f[1][0][0]=1 f[1][0][0]=1,到第2层第 3个节点dfs(1, 0, 0, 0, 0)时,无前导零、无限制且已赋值 f [ ] [ ] [ ] f[][][] f[][][],直接返回该值,不需要重新计算。

示例2:统计[1, 35]区间数字3的出现次数,过程如下。

  1. 数字分解:nums[1]=5,nums[2]=3。

  2. 采用记忆化递归求解,当前位为第二位(下标为2),当前统计数字为3,有前导零,有限制up=nums[2]=3,枚举i=0…3。

    1. i=0:有前导零,无限制,up=9,执行i=0…9,包含00~09, 数字3出现1次。
    2. i=1:无前导零,无限制,up=9,执行i =0…9,包含10~19, 数字3出现1次。无前导零且无限制,所以 f [ 1 ] [ 3 ] [ 0 ] = 1 f[1][3][0]=1 f[1][3][0]=1
    3. i=2:无前导零,无限制且 f [ 1 ] [ 3 ] [ 0 ] f[1][3][0] f[1][3][0]已赋值,直接返回该值1。
    4. i=3:无前导零,有限制,up=nums[1]=5,枚举i =0…5,包含 30~35,数字3的个数为7个。
  3. 累加结果, f [ 2 ] [ 3 ] [ 0 ] = 10 f[2][3][0]=10 f[2][3][0]=10,表示[1, 35]区间数字3出现10 次:3、13、23、30、31、32、33、34、35。

    求解过程如下图所示。
    在这里插入图片描述

算法实现

写法一

#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
typedef long long LL;
int nums[40];
LL f[15][10][15], res[2][10];

int dfs(int pos, int x, int cnt, bool lead, bool limit)
{
    if (!pos)   return cnt;
    if (!limit && !lead && ~f[pos][x][cnt] )    return f[pos][x][cnt];

    int up = limit ? nums[pos] : 9, t = 0;
    LL ans = 0;
    for (int i = 0; i <= up; i ++ )
    {
        if (x != i )    t = cnt;
        else
        {
            if (lead && !x )    t = 0;
            else    t = cnt + 1;
        }
        ans += dfs(pos - 1, x, t, lead && i == 0, limit && i == up);
    }

    if (!limit && !lead)    f[pos][x][cnt] = ans;
    return ans;
}

void solve(LL x, int idx)
{
    memset(f, -1, sizeof f);
    int n = 0;
    while (x)
    {
        nums[ ++ n ] = x % 10;
        x /= 10;
    }
    for (int i = 0; i < 10; i ++ )
        res[idx][i] = dfs(n, i, 0, true, true);
}

int main()
{
    LL l, r;
    while (cin >> l >> r, l || r)
    {
        if (l > r)  swap(l, r);
        memset(res, 0, sizeof res);
        solve(l - 1, 0);
        solve(r, 1);
        for (int i = 0; i < 10; i ++ )  cout << res[1][i] - res[0][i] << " ";
        puts("");
    }
    return 0;
}

写法二

#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
const int N = 15;
int nums[N], f[N][N];

int dfs(int pos, int cnt, int x, bool lead, bool limit) 
{
    if (!pos) 
    {
        if (lead && !x) return 1;
        return cnt;
    }
    
    if (!limit && !lead && ~f[pos][cnt]) return f[pos][cnt];
    
    int up = limit ? nums[pos] : 9, ans = 0;
    for (int i = 0; i <= up; i ++) 
    {
        int t;
        if (x != i )    t = cnt;
        else
        {
            if (!x )    t = cnt + (lead == 0); 
            else    t = cnt + 1;
        }
        ans += dfs(pos - 1, t, x, lead && i == 0, limit && i == up);
    }
    
    if (!limit && !lead)    f[pos][cnt] = ans;
    return ans;
}

int solve(int x, int num) 
{
    memset(f, -1, sizeof f);
    int n = 0;
    while (x) 
    {
        nums[++ n] = x % 10;
        x /= 10;
    }
    return dfs(n, 0, num, true, true);
}

int main() 
{
    int l, r;
    while (cin >> l >> r, l || r) 
    {
        if (l > r) swap(l, r);
        for (int i = 0; i <= 9; i ++) cout << solve(r, i) - solve(l - 1, i) << " ";
        puts("");
    }
}

P2657 Windy数

题目描述

P2657 Windy数

算法思路

此题思路大体和 POJ3252 Round Numbers 类似。

状态表示

f [ p o s ] [ p r e ] f[pos][pre] f[pos][pre]表示枚举到第pos位,其前面一位的数字是pre,最终不含前导0的方案数。

我们考虑限制条件:相邻两位绝对值大于等于2。如果前一位是前导0,那么我们不需要考虑这位与0的绝对值,也就是说0和1也是可以取到的,所以我们可以在处理下一位时把这位当做−2处理。

由于我们初始化lead=true,即存在前导0, 搜索初始条件第二个参数pre必须填一个≤-2的数来保证可以搜索下去,不然会出错,这里要初始化传入pre=-2。

算法实现

写法一(代码大体同POJ3252的代码):

#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
typedef long long LL;
int nums[15];
LL f[15][15];

// pos当前位置,pre前一位数,lead判断前面是否全是0,limit最高位限制
int dfs(int pos, int pre, bool lead, bool limit)
{
    if (!pos)   return 1;
    // 可以写成!limit && !lead && ~f[pos][pre]
    if (!limit && ~f[pos][pre] )    return f[pos][pre];
    
    int up = limit ? nums[pos] : 9;
    LL ans = 0;
    for (int i = 0; i <= up; i ++ )
    {
        // 不合题意
        if (abs(i - pre) < 2)   continue;
        // 如果有前导0,下一位随意 
        if (lead && !i )    ans += dfs(pos - 1, -2, true, limit && i == up);
        // 如果没有前导0,继续按部就班地搜 
        else    ans += dfs(pos - 1, i, false, limit && i == up);
    }
    
    // 可以写成 !limit && !lead
    if (!limit )    f[pos][pre] = ans;
    return ans;
}

int solve(LL x)
{
    memset(f, -1, sizeof f);
    int n = 0;
    while (x)
    {
        nums[ ++ n] = x % 10;
        x /= 10;
    }
    return dfs(n, -2, true, true);
}

int main()
{
    LL l, r;
    cin >> l >> r;
    cout << solve(r) - solve(l - 1) << endl;
    return 0;
}

AcWing1081 度的数量

题目描述

AcWing1081 度的数量

算法思路

K个互不相等的B的整数次幂之和等价于求数字n在B进制表示下是否由K个1且其他位为0组成。这题最关键的就是要发现这个性质!

为什么会得出这个结论呢?看分析:

比如K=2,B=2:

  • ( 15 ) 10 = ( 01111 ) 2 (15)_{10}={(01111)}_2 (15)10=(01111)2
  • ( 16 ) 10 = ( 10000 ) 2 (16)_{10}={(10000)}_2 (16)10=(10000)2
  • ( 17 ) 10 = ( 10001 ) 2 (17)_{10}=(10001)_2 (17)10=(10001)2 17 = 2 0 + 2 4 17=2^0+2^4 17=20+24
  • ( 18 ) 10 = ( 10010 ) 2 (18)_{10}=(10010)_2 (18)10=(10010)2 18 = 2 1 + 2 4 18=2^1+2^4 18=21+24
  • ( 19 ) 10 = ( 10011 ) 2 (19)_{10}=(10011)_2 (19)10=(10011)2
  • ( 20 ) 10 = ( 10100 ) 2 (20)_{10}=(10100)_2 (20)10=(10100)2 20 = 2 2 + 2 4 20=2^2+2^4 20=22+24

从中可以看出,17和18和20是符合要求的,而且从它们的B进制(二进制)可以看出,都有k个1,其他位全为0组成。

再比如K=2,B=4:

  • ( 4 ) 10 = ( 0010 ) 4 (4)_{10}=(0010)_4 (4)10=(0010)4
  • ( 5 ) 10 = ( 0011 ) 4 (5)_{10}=(0011)_4 (5)10=(0011)4 5 = 4 0 + 4 1 5=4^0+4^1 5=40+41
  • ( 6 ) 10 = ( 0012 ) 4 (6)_{10}=(0012)_4 (6)10=(0012)4
  • ( 7 ) 10 = ( 0013 ) 4 (7)_{10}=(0013)_4 (7)10=(0013)4
  • ( 8 ) 10 = ( 0020 ) 4 (8)_{10}=(0020)_4 (8)10=(0020)4
  • ( 17 ) 10 = ( 0101 ) 4 (17)_{10}=(0101)_4 (17)10=(0101)4 17 = 4 0 + 4 2 17=4^0+4^2 17=40+42
  • ( 18 ) 10 = ( 0102 ) 4 (18)_{10}=(0102)_4 (18)10=(0102)4
  • ( 19 ) 10 = ( 0103 ) 4 (19)_{10}=(0103)_4 (19)10=(0103)4
  • ( 20 ) 10 = ( 0110 ) 4 (20)_{10}=(0110)_4 (20)10=(0110)4 20 = 4 1 + 4 2 20=4^1+4^2 20=41+42
  • ( 21 ) 10 = ( 0111 ) 4 (21)_{10}=(0111)_4 (21)10=(0111)4

从中可以看出,5和17和20是符合要求的,而且从它们的B进制(四进制)可以看出,都有k个1,其他位全为0组成。

也就是说,我们只关心B进制下的0和1,其他数字都是干扰项,对答案并没有任何贡献。

我们来分析一下:前导零不影响,所以不需要lead变量。因为要记录数量,所以要增加计数变量cnt表示已经用了多少个1。不需要记录前驱,因此不用设置pre变量。判断边界时只要最后数量cnt==k,若相等,则返回1,否则就返回0。同时枚举数字时如果前面系数不为1或者或者没搜索完就已经k个了,那么就直接continue。

算法实现

#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
const int N = 35;
int nums[N], f[N][N];
int B, K;

int dfs(int pos, int cnt, bool limit)
{
    if (!pos)   return cnt == K;
    if (!limit && ~f[pos][cnt] )    return f[pos][cnt];
    
    int up = limit ? nums[pos] : B - 1, ans = 0;
    for (int i = 0; i <= up; i ++ )
    {
        if (i > 1 || (i == 1 && cnt == K))  continue;
        ans += dfs(pos - 1, cnt + (i == 1), limit && i == up);
    }
    
    if (!limit )    f[pos][cnt] = ans;
    return ans;
}

int solve(int x)
{
    memset(f, -1, sizeof f);
    int n = 0;
    while (x)
    {
        nums[ ++ n] = x % B;
        x /= B;
    }
    return dfs(n, 0, true);
}

int main()
{
    int l, r;
    scanf("%d%d%d%d", &l, &r, &K, &B);
    printf("%d\n", solve(r) - solve(l - 1));
    return 0;
}

AcWing1082 数字游戏

题目描述

AcWing1082 数字游戏

算法思路

分析:前导零不影响,所以不需要变量lead,只需要判断枚举的位数是不是非严格递增来判断是否继续搜索。由于我们想要知道前后两个位之间的大小关系,因此需要设置pre变量表示前一个位上的数是啥。

算法实现

#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
const int N = 15;
int nums[N], f[N][N];

int dfs(int pos, int pre, bool limit)
{
    if (!pos)   return 1;
    if (!limit && ~f[pos][pre]) return f[pos][pre];
    
    int up = limit ? nums[pos] : 9, ans = 0;
    for (int i = 0; i <= up; i ++ )
    {
        if (i < pre)    continue;
        ans += dfs(pos - 1, i, limit && i == up);
    }
    
    if (!limit )    f[pos][pre] = ans;
    return ans;
}

int solve(int x)
{
    memset(f, -1, sizeof f);
    int n = 0;
    while (x)
    {
        nums[ ++ n] = x % 10;
        x /= 10;
    }
    return dfs(n, 0, true);
}

int main()
{
    int l, r;
    while (~scanf("%d%d", &l, &r)) {
        printf("%d\n", solve(r) - solve(l - 1));
    }
    return 0;
}

AcWing1084 数字游戏II

题目描述

AcWing1084 数字游戏 II

算法思路

分析:前导零不影响,所以不需要变量lead。此题涉及到数字和 ,所以要用到变量sum来记录数位和。不需要记录前驱pre。故可以定义状态 f [ p o s ] [ s u m ] f[pos][sum] f[pos][sum]。边界条件就是sum % n == 0,如果是则返回1,否则返回0。

算法实现

#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
const int N = 1010;
int nums[N], f[N][N];
int l, r, p;

int dfs(int pos, int sum, bool limit)
{
    if (!pos)   return sum % p == 0;
    if (!limit && ~f[pos][sum] )    return f[pos][sum];
    
    int up = limit ? nums[pos] : 9, ans = 0;
    for (int i = 0; i <= up; i ++ )
    {
        ans += dfs(pos - 1, sum + i, limit && i == up);
    }
    
    if (!limit )    f[pos][sum] == ans;
    return ans;
}

int solve(int x)
{
    memset(f, -1, sizeof f);
    int n = 0;
    while (x)
    {
        nums[ ++ n] = x % 10;
        x /= 10;
    }
    return dfs(n, 0, true);
}

int main()
{
    while (~scanf("%d%d%d", &l, &r, &p)) {
        printf("%d\n", solve(r) - solve(l - 1));
    }
    return 0;
}

LC233 数字1的个数

题目描述

传送门

算法思路

定义记忆化搜索函数:int dfs(int pos,int cnt,int limit),本题前导0对答案没有影响,所以不需要lead。

pos是当前位,cnt是从最高位到pos-1位中1的个数,limit表示当前位的数大小是否有限制

算法实现

class Solution {
public:
    int nums[12], f[12][12];

    // pos是当前位,cnt是从最高位到pos-1位中1的个数,limit表示当前位的数大小是否有限制
    int dfs(int pos,int cnt,int limit){
        if (!pos)    return cnt;
        if (!limit && ~f[pos][cnt]) return f[pos][cnt];

        int up = limit ? nums[pos] : 9, ans = 0;
        for (int i = 0; i <= up; i ++ ) {
            ans += dfs(pos - 1, cnt + (i == 1), limit && i == up);
        }

        if(!limit)  f[pos][cnt] = ans;
        return ans;
    }

    int solve(int x)
    {
        memset(f, -1, sizeof f);
        int n = 0;
        while (x)
        {
            nums[ ++ n] = x % 10;
            x /= 10;
        }
        return dfs(n, 0, true);
    }

    int countDigitOne(int n) {
        return solve(n);
    }
};

LC2376 统计特殊整数

题目描述

LC2376 统计特殊整数

算法思路

本题要求的这个特殊正整数中每个数位都是不相同的,那么我们就需要知道前面的位中已经填了哪些数,这样后面的位才能知道该怎么填数。这里需要定义变量mask表示前面选过的数字集合。

在本题中,要求「至少有 1 位重复的数字」,此时 0001 和 1 就不等价了,0001 是一个满足要求的数字,而 1 是不满足要求的数字,而在 [0, n] 中,0001 其实是非法的,所以会出现错误,故需要变量lead。有限制,因此需要定义变量limit。

所以记忆化搜索就是:dfs(int pos, int mask, bool lead, bool limit)。

算法实现

class Solution {
public:
    int nums[11], f[11][1 << 11];

    // 求出[0, x]中满足题意的个数
    int dfs(int pos, int mask, bool lead, bool limit)
    {
        if (pos == 0)   return 1;
        if (!lead && !limit && ~f[pos][mask])   return f[pos][mask];

        int up = limit ? nums[pos] : 9, ans = 0;
        for (int i = 0; i <= up; i ++ )
        {
            if (mask >> i & 1)  continue;
            if (lead && !i )    ans += dfs(pos - 1, mask, true, limit && i == up) ;
            else    ans += dfs(pos - 1, mask | 1 << i, false, limit && i == up);
        }

        if (!limit && !lead)    f[pos][mask] = ans;
        return ans;
    }

    // 求出[1, x]中满足题意的个数
    int solve(int x)
    {
        memset(f, -1, sizeof f);
        int n = 0;
        while (x)
        {
            nums[ ++ n] = x % 10;
            x /= 10;
        }
        return dfs(n, 0, true, true) - 1;   // 不能把0计算在内 故需要减去一个
    }

    int countSpecialNumbers(int n) {
        return solve(n);
    }
};

LC600 不包含连续1的非负整数

题目描述

传送门

算法思路

分析:对于本题来说,前导0对于答案并无影响,因此不需要定义lead变量。由于是求不含连续的1,因此对于当前位pos,我们应该要知道它前面那位填啥,因此需要定义变量pre,其值是0或1。因此,记忆化搜索:int dfs(int pos, int pre, bool limit)。

在状态转移时,如果pre为1并且当前位也是1,那么就直接continue。否则就说明pre为0,那么判断i==1即可。

算法实现

class Solution {
public:
    int nums[40], f[40][2];

    int dfs(int pos, int pre, bool limit)
    {
        if (!pos)   return 1;
        if (!limit && ~f[pos][pre] )    return f[pos][pre];

        int up = limit ? nums[pos] : 1, ans = 0;
        for (int i = 0; i <= up; i ++ )
        {
            if (pre && i == 1)  continue;
            ans += dfs(pos - 1, i == 1, limit && i == up);
        } 

        if (!limit )    f[pos][pre] = ans;
        return ans;
    }

    int solve(int x)
    {
        memset(f, -1, sizeof f);
        int n = 0;
        while (x)
        {
            nums[ ++ n] = x % 2;
            x /= 2;
        }
        return dfs(n, 0, true);
    }

    int findIntegers(int n) {
        return solve(n);
    }
};

LC902 最大为N的数字组合

题目描述

传送门

算法思路

将数字x转成int数组nums[],其中nums[1]是最低位,nums[n]是最高位。

设置记忆化搜索函数dfs(int pos, bool lead, bool limit),答案就是dfs(n, true, true)。

其中:

  • pos表示数字的位数,从末位或者第一位开始,一般根据题目的数字构造性质来选择顺序。对于本题,我们选择从高位开始,因此,pos的初始值为n;
  • lead表示当前数字中是否包含前导零,如果包含,则为true,否则为false;初始化为true;
  • limit表示可填的数字的限制,如果无限制,那么可以选择 [ 0 , ⋯   , 9 ] [0,\cdots, 9] [0,,9]。否则,只能选择0到nums[pos]。如果limit为true且已经取到了能取到的最大值,那么下一个limit同样为 true;如果limit为true但是还没有取到最大值,或者limit为false,那么下一个limit为false。

算法实现

class Solution {
public:
    int nums[12];
    int f[12];
    unordered_set<int> S;

    int dfs(int pos, int lead, bool limit) {
        if (!pos) 
        {
            // 等价于这种写法:return lead ^ 1; 或者 return lead ? 0 : 1;
            if (lead)   return 0;
            return 1;       
        }

        if (!limit && !lead && ~f[pos])   return f[pos];

        int up = limit ? nums[pos] : 9, ans = 0;
        for (int i = 0; i <= up; ++i) 
        {
            if (lead && !i )    ans += dfs(pos - 1, true, limit && i == up); 
            else if (S.count(i)) {
                ans += dfs(pos - 1, false, limit && i == up);
            }
        }

        if (!limit && !lead)    f[pos] = ans;
        return ans;
    }

    int solve(int x)
    {
        memset(f, -1, sizeof f);
        int n = 0;
        while (x)
        {
            nums[ ++ n] = x % 10;
            x /= 10;
        }
        return dfs(n, true, true);
    }

    int atMostNGivenDigitSet(vector<string>& digits, int n) {
        for (auto& d : digits)  S.insert(stoi(d));
        
        return solve(n);
    }
};

LC1012 至少有1位重复的数字

题目描述

传送门

算法思路

这题其实是LC2376的对偶问题。LC2376是求 [ 1 , n ] [1,n] [1,n]中不含重复数的个数,而这题是求 [ 1 , n ] [1,n] [1,n]中含重复数的个数。我们可以利用LC2376的代码,然后用n减去不含重复数的个数就可以得到 [ 1 , n ] [1,n] [1,n]中含重复数的个数了。

在本题中,要求「至少有 1 位重复的数字」,此时 0001 和 1 就不等价了,0001 是一个满足要求的数字,而 1 是不满足要求的数字,而在 [0, n] 中,0001 其实是非法的,所以会出现错误,故需要变量lead。

算法实现

class Solution {
public:
    int nums[11], f[11][1 << 11];

    // 求出[0, x]中无重复数字的个数
    int dfs(int pos, int mask, bool lead, bool limit)
    {
        if (pos == 0)   return 1;
        if (!lead && !limit && ~f[pos][mask])   return f[pos][mask];

        int up = limit ? nums[pos] : 9, ans = 0;
        for (int i = 0; i <= up; i ++ )
        {
            if (mask >> i & 1)  continue;
            if (lead && !i )    ans += dfs(pos - 1, mask, true, limit && i == up) ;
            else    ans += dfs(pos - 1, mask | 1 << i, false, limit && i == up);
        }

        if (!limit && !lead)    f[pos][mask] = ans;
        return ans;
    }

    // 求出[1, x]中无重复数字的个数
    int solve(int x)
    {
        memset(f, -1, sizeof f);
        int n = 0;
        while (x)
        {
            nums[ ++ n] = x % 10;
            x /= 10;
        }
        return dfs(n, 0, true, true) - 1;   // 不能把0计算在内 故需要减去一个
    }

    int numDupDigitsAtMostN(int n) {
        // n个数减去无重复的个数,那么就得到重复数的个数
        return n - solve(n);
    }
};

LC788 旋转数字

题目描述

传送门

算法思路

根据题意,好数中不能含有3,4,7,且至少包含2,4,5,6,9中的一个。我们定义一个数组diffs:

int diffs[10] = {0, 0, 1, -1, -1, 1, 1, -1, 0, 1};	# 0123456789

分别对应0到9中的每个数,其中数字0,1,8其旋转后仍然是它自身,我们设为0。将数字3,4,7设为-1。将数字2,4,5,6,9设为1。

我们定义变量hasDiff表示前面填的数字是否包含2,4,5,6,9中的一个。对于本题来说,前导0并不会影响答案,所以不用lead变量。

算法实现

class Solution {
public:
    int diffs[10] = {0, 0, 1, -1, -1, 1, 1, -1, 0, 1};
    int nums[11], f[11][2];

    int dfs(int pos, bool hasDiff, bool limit)
    {
        if (!pos)   return hasDiff;
        if (!limit && ~f[pos][hasDiff])  return f[pos][hasDiff];

        int up = limit ? nums[pos] : 9, ans = 0;
        for (int i = 0; i <= up; i ++ )
            if (~diffs[i])
                ans += dfs(pos - 1, hasDiff || diffs[i], limit && i == up);

        if (!limit )    f[pos][hasDiff] = ans;
        return ans; 
    }

    int solve(int x)
    {
        memset(f, -1, sizeof f);
        int n = 0;
        while (x)
        {
            nums[ ++ n] = x % 10;
            x /= 10;
        }
        return dfs(n, false, true);
    }

    int rotatedDigits(int n) {
        return solve(n);
    }
};

LC357 统计各位数字都不同的数字个数

题目描述

传送门

算法思路

此题思路大体同 LC2376。唯一需要做的就是注释掉两段代码,见算法实现部分。

算法实现

class Solution {
public:
    int nums[12], f[12][12];

    int dfs(int pos, int mask, bool lead, bool limit)
    {
        if (!pos)   return 1;
        //if (!limit && !lead && ~f[pos][mask])   return f[pos][mask];

        int up = limit ? nums[pos] : 9, ans = 0;
        for (int i = 0; i <= up; i ++ )
        {
            if (mask >> i & 1)  continue;
            if (lead && !i )    ans += dfs(pos - 1, mask, true, limit && i == up);
            else    ans += dfs(pos - 1, mask | 1 << i, false, limit && i == up);
        }

        //if (!limit && !lead)    f[pos][mask] = ans;
        return ans;
    }

    int solve(int x)
    {
        memset(f, -1, sizeof f);
        int n = 0;
        while (x)
        {
            nums[ ++ n] = x % 10;
            x /= 10;
        }
        return dfs(n, 0, true, true);
    }

    int countNumbersWithUniqueDigits(int n) {
        int x = 1;
        for (int i = 0; i < n; i ++ )   x *= 10;
        return solve(x - 1);
    }
};

以上就是一些数位dp习题汇总,相信你做完这些题目后,帮记忆化搜索的模板背熟,以后碰到数位dp的题脑海里就有大体框架了,再根据不同题意去求解即可。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

计小酱蟹不肉

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值