数位dp笔记(详解+例题+解析)

数位dp笔记

简单介绍

数位:把一个数字按照个、十、百、千等等一位一位地拆开,关注它每一位上的数字。如果拆的是十进制数,那么每一位数字都是 0~9,其他进制可类比十进制。

数位 DP:用来解决一类特定问题,这种问题比较好辨认,一般具有这几个特征:

​ 1.要求统计满足一定条件的数的数量(即,最终目的为计数);

​ 2.这些条件经过转化后可以使用「数位」的思想去理解和判断;

​ 3.输入会提供一个数字区间(有时也只提供上界)来作为统计的限制;

​ 4.上界很大(比如 1018),暴力枚举验证会超时。

考虑人类计数的方式,最朴素的计数就是从小到大开始依次加一。但我们发现对于位数比较多的数,这样的过程中有许多重复的部分。例如,从 7000 数到 7999、从 8000 数到 8999、和从 9000 数到 9999 的过程非常相似,它们都是后三位从 000 变到 999,不一样的地方只有千位这一位,所以我们可以把这些过程归并起来,将这些过程中产生的计数答案也都存在一个通用的数组里。此数组根据题目具体要求设置状态,用递推/DP 的方式进行状态转移。

数位 DP 中通常会利用常规计数问题技巧,比如把一个区间内的答案拆成两部分相减(即ans[l,r]=ans[0,r]-ans[0,l-1])

那么有了通用答案数组,接下来就是统计答案。统计答案可以选择记忆化搜索,也可以选择循环迭代递推。为了不重不漏地统计所有不超过上限的答案,要从高到低枚举每一位,再考虑每一位都可以填哪些数字,最后利用通用答案数组统计答案。


模板(DFS)

ll dfs(int pos, int limit, int zero, 状态) {
    //可以根据题意进行灵活剪枝
    
	if (!pos) {
		边界的判断条件
	}
	if (!limit && !zero && f[pos][状态] != -1) return f[pos][状态];
	ll res = 0;
	int end = limit ? a[pos] : 进制-1;
	for (int i = 0; i <= end; ++i)
		res += dfs(pos - 1, limit && i == end, zero && i == 0, 状态更新);
	if (!limit && !zero)f[pos][状态] = res;
	return res;
}
ll dp(ll x) {
	memset(f, -1, sizeof f);
	int len = 0;
	while (x)a[++len] = x % 进制, x /= 进制;  //可以len++ ,看个人习惯
	return dfs(len, 1, 1, 状态);
}
int main(void)
{
	ll l, r; cin >> l >> r;
	cout << dp(r) - dp(l - 1) << endl;
	return 0;
}

dp函数

注意初始化为-1,长度len=0

dfs函数

res 记录答案 初始化为0

end 表示枚举的最高上限数字

记忆化搜索

if (!limit && !zero && f[pos][状态] != -1) return f[pos][状态];

只有无限制,无前导零才可以进行return,不然都是未搜索完,不完整的情况

if (!limit && !zero)f[pos][状态] = res;
return res;

如果最后还有限制,那么返回res,否则更新f[pos] [状态]

基本参数

len 数位长度,一般根据这个来确定数组范围

a[i] 每个数位的具体数字

必填参数

pos 表示数字的位数

从高位或者低位开始,根据题目的数字构造性质选择枚举顺序,一般情况是从高位开始,到低位结束,开始往高位递归计数

初始从len开始的话,边界条件为!pos 每一位的数字上限为 a[pos], 个人比较喜欢这一种,也是最常用的一种枚举方式

limit 表示该为填数的限制,若无限制(limit=0)0~9随便填 ,若有限制(limit=1) 0~a[pos] ,传参时参数值为1,即初始状态为有限制

如果搜索到a[1]…a[pos]…a[n],原数位为a[1]…a[k]…a[n],那么我们必须对接下来要搜索的数加以限制,也就是枚举的数位上的数不能超过原有数位上数,所有引入limit参数,如果limit=1,那么最高位end=a[k],如果没有限制,那么最高位end=9(十进制表示 ,可以理解为进制-1)

int end = limit ? a[pos] : 进制-1;

如果limit=1且已经取到了能取到的最高位时(a[pos]=a[k]),那么下一个limit=1;
如果limit=1且没有取到能取到的最高位时(a[pos] 如果limit=0,那么下一个limit=0,因为前一位没有限制后一位必定没有限制
把这3种情况合成一个语句进行下一次搜索:limit && i == end(i为当前枚举的数字)

limit && i == end

可选参数

pre 表示上一位数字是多少

比如不要62

zero 表示前导零是否存在,zero=1表示存在前导零,zero=0表示不存在前导零

一般来说有些题目不加限制前导零会影响数字结构,(比如统计数字0出现的次数,前导零不计算),所以zero是一个很重要的参数
如果zero=1且枚举的数字i=0,那么下一个zero=1;
如果zero=1且枚举的数字i!=0,那么下一个zero=0;
如果zero=0,那么下一个zero=0,因为前一位没有前导零后一位必定没有前导零
把这3种情况合成一个语句进行下一次搜索:zero && i == 0(i为当前枚举的数字)

zero && i == 0

cnt 某个数字出现的次数

例如统计0~9每一种数字出现的次数

sum 搜索到当前所有数字之和

基本参数应该就这些,但是也会有其他方式表示状态,比如状压


例题

[HDU - 2089] 不要62

题面大意: 统计一个区间内数位上不能有 4 也不能有连续的 62 的数有多少

题目链接

博客解析


[黑暗爆炸 - 1026] windy数

题面大意: 统计区间内,不含前导零且相邻两个数字之差至少为2的正整数的个数

题目链接

博客解析


[HDU - 3555] Bomb

题面大意: 统计区间内,包含连续49的数字个数

题目链接

博客解析


[黑暗爆炸 - 1833] count 数字计数

题面大意: 给定两个正整数 a和b,求在[a,b]的所有整数中,每个数码(0~9)各出现了多少次

题目链接

博客解析


[LibreOJ - 10164] 数字游戏

题面大意: 统计区间内,不降数的个数(不降数:这种数字必须满足从左到右各位数字成小于等于的关系,如 123,446)

题目链接

博客解析


[LibreOJ - 10166] 数字游戏

题面大意: 统计区间内,取模数的个数(取模数:这种数字必须满足各位数字之和 mod N 为 0)

题目链接

博客解析


[codeforces] Salazar Slytherin’s Locket

题面大意: 统计区间[l,r]中,在b进制下每种0~b-1的数的个数都出现偶数次的个数,不包含前导零

题目链接

博客解析


[codeforces] Classy Numbers

题面大意: 统计区间[l,r]中,在十进制下表示,出现不超过三个非零数字的个数,例如4,200000,10203

题目链接

博客解析


[黑暗爆炸 - 1799] self 同类分布

题面大意: 统计区间[l,r]中各位数字之和能整除原数的数的个数

题目链接

博客解析


[LibreOJ - 2215] 方伯伯的商场之旅

题面大意: 给出l,r,k,求将l与r之间的数进行x操作的最小代价.
x操作指将一个数转化为k进制,表示有几堆石块,每堆石块恰有该数位上的数个石子,相邻两堆距离为1,将它们并成一堆,代价为石头数量*距离.

题目链接

博客解析


[codeforces] Beautiful numbers

题面大意: 统计区间[l,r]内的数能整除它所有位上的非零整数的个数

题目链接

博客解析


[POJ - 3252] Round Numbers

题面大意: 统计区间[l,r]内的数二进制形式下,0的个数>=1的个数的数的个数

题目链接

博客解析


[HDU - 3709] Balanced Number

题面大意: 给定区间[a,b],求区间内平衡数的个数 (平衡数:即有一位做平衡点,左右两边数字的力矩相等)

题目链接

博客解析


[HDU - 3652] B-number

题面大意: 统计区间 [1,n] 中含有 ‘13’ 且模 13 为 0 的数字有多少个

题目链接

博客解析


[HDU - 4734] F(x)

题面大意:f(x) = a(n)*2(n-1)+a(n-1)*2(n-2)+…a(2)*21+a(1)*20,( a(i)表示十进制数x中第i位的数字 )

给出a,b,求出0~b有多少个不大于f(a)的数

题目链接

博客解析


[SPOJ - BALNUM)] Balanced Numbers

题面大意:统计区间内,一个数的数位上每个奇数出现偶数次,每个偶数出现奇数次,这样数的个数

题目链接

博客解析

你可能感兴趣的:(#,数位dp,算法,动态规划)