bt365验证不通过

深度干货

发布时间: 2025-07-06 01:37:20 作者: admin 阅读量: 7082 评论数: 913

数位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。

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

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

数字分解:nums[1]=4,nums[2]=2。从高位开始,当前位是2,前面一位不是6,有限制up=nums[2]=2,枚举i=0…2,递归求解。

i=0:以0开头的一位数,无限制,up=9,枚举i =0…9,包含00 ~09,除去04,

f

[

1

]

[

0

]

=

9

f[1][0]=9

f[1][0]=9i=1:以1开头的一位数,无限制且

f

[

1

]

[

0

]

f[1][0]

f[1][0]已赋值,直接返回 9。i=2:以2开头的一位数,有限制,up=num[1]=4,执行i=0…4,包含20~24,除去24,共有4个数满足条件。 累加结果,返回[0, 24]区间不包含4也不包含62的22个数, 求解过程如下图所示。

示例2:请尝试计算[0, 386]区间不包含4也不包含62的数有多少个?

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

算法实现

#include

#include

#include

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”,过程如下。

数字分解:nums[1]=0,nums[2]=0,nums[3]=5。从高位开始,当前位是第三位,前面一位不是4,有限制。up=nums[3]=5,枚举i=0…5。

i=0:以0开头的两位数,无限制,up=9,枚举i=0…9,只有一个数包含“49”,即049,故

f

[

2

]

[

0

]

=

1

f[2][0]=1

f[2][0]=1。i=1:以1开头的两位数,无限制且

f

[

2

]

[

0

]

f[2][0]

f[2][0]已赋值,返回该值,即149。i=2:以2开头的两位数,无限制且

f

[

2

]

[

0

]

f[2][0]

f[2][0]已赋值,返回该值,即249。i=3:以3开头的两位数,无限制且

f

[

2

]

[

0

]

f[2][0]

f[2][0]已赋值,返回该值,即349。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。i=5:以5开头的两位数,有限制,up=nums[2]=0,执行i=0, 递归求解。pos=0时,返回0。 累加结果,返回[1, 500]区间包含“49”的数15个,求解过程如下图所示。

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

算法实现

思路一

#include

#include

#include

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

#include

#include

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的个数。

将5转换为二进制数101,nums[0]=1,nums[1]=0,nums[2]=1。首先从高位开始,当前位为第二位(下标为2),有前导零;有数位限制,枚举i=0…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]=2i=1:无前导0,有限制,包含100、101两个数,其中100为Round Numbers,故

f

[

1

]

[

0

]

[

1

]

=

1

f[1][0][1]=1

f[1][0][1]=1 返回

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

#include

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的出现次数,过程如下。

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

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

i=0:有前导零,无限制,枚举i=0…9,包含00~09,去掉前导零后为1~9,数字0出现0次。i=1:无前导零,无限制,枚举i=0…9,包含10~19,数字0出现1次,故

f

[

1

]

[

0

]

[

0

]

f[1][0][0]

f[1][0][0]。i=2:无前导零,无限制且

f

[

1

]

[

0

]

[

0

]

f[1][0][0]

f[1][0][0]已赋值,直接返回该值1。i=3:无前导零,有限制,up=nums[1]=5,枚举i =0…5,包含30 ~35,数字0出现1次。 累加结果,返回

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的出现次数,过程如下。

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

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

i=0:有前导零,无限制,up=9,执行i=0…9,包含00~09, 数字3出现1次。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。i=2:无前导零,无限制且

f

[

1

]

[

3

]

[

0

]

f[1][3][0]

f[1][3][0]已赋值,直接返回该值1。i=3:无前导零,有限制,up=nums[1]=5,枚举i =0…5,包含 30~35,数字3的个数为7个。 累加结果,

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

#include

#include

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

#include

#include

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

#include

#include

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

#include

#include

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

#include

#include

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

#include

#include

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 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& 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的题脑海里就有大体框架了,再根据不同题意去求解即可。

相关文章