【算法】数位 dp

时隔多日,我终于再次开始写博客了!!数组

上午听了数位 dp,感受没听懂,因而在网上进行一番愉 ♂ 快 ♀ 的学习后,写篇博来加深一下印象~~学习

前置的没用的知识

数位

不一样计数单位,按照必定顺序排列,它们所占位置叫作数位。spa

在整数中的数位是从右往左,逐渐变大:第一位是个位,第二位是十位,第三位是百位,第四位是千位,第五位是万位,第六位是十万位,第七位是百万位,第八位是千万位,以此类推。code

同一个数字,因为所在数位不一样,计数单位不一样,所表示数值也就不一样。递归

对于每个数都应当有一个名称,以天然数来讲,天然数是无限多的,若是每个天然数都用一个独立的名称来读出它,这是很是不方便的,也是不可能作到的。get

为了解决这个问题,人们创造出一种计数制度,就是如今咱们使用的十进制计数法。博客

                                        ———— 360 百科数学

感谢 360 百科朋友的友情兹瓷table

顾名思义,数位 dp 就是在数位上的 dp。class

主要应用

常常用来解决计数类的、范围很大的题目,

固然一般会有一些条件。

特色

很板很板,几乎跟背包 dp 同样一个板子走天下,

用句话来误导人一下:记个板子而后不用理解都行……

正题

先放个例题

不含前导零且相邻两个数字之差至少为 \(2\) 的正整数被称为 \(\text{windy}\) 数。

windy 想知道,在 \(a\)\(b\) 之间,包括 \(a\)\(b\) ,总共有多少个 \(\text{windy}\) 数?

范围:\(a,b\in [1,2\times10^{9}]\)

我相信,若是没有那个巨大的范围的话,正常人都能作得出来 = =

但这个范围就神奇……灭掉了一群正常人……

总不能从 \(1\) 一直枚举到 \(10^{9}\) 吧……

这时候就是 超级飞侠 数位 dp 出现的时候了!!

分析

咱们能够想一下:\(2\times10^{9}\) 一共就只有 \(9\) 位,毫无疑问的是,若是针对这个数的每一位开一个数组,最多就只开 \(9\) 个。

而后,再想一下:

  • 若是在每一位枚举 \(0\)\(9\),那时间复杂度确定不会炸掉。

  • 若是规定了一个数的最高位,那么剩下的位数有多少种组合,而后再规定剩下的数的最高位,而后再……以后这个数就被完全地拆分了~~

结论

咱们能够用万能 yuechi ———— 大法师直接打过去!!

而后就 T 了~~

因此用记搜啊~~~(本蒟蒻太弱了,不会递推)

代码实现

设一个 \(f[N][1]\) 来记录状态,其中的 \(f[i][j]\) 表示位数最高位为 \(i\),该位数的上一个数为 \(j\)

每搜一个数位,枚举这个数位的数字(从 \(0\)\(9\)),而后判断这个数合不合法(好比这个例题就是判断是否与上个数之差不小于 \(2\))。

  • 若是合法,就继续搜下一个数位;

  • 若是不合法,就剪枝。

判断边界:

判断最高位时看看有没有到最大值,而后一位一位的推。

举个栗子:枚举 \(7456\) 的时候最高位先枚举 \([0,6]\),等以 \([0,6]\) 开头的数都被判断完了,就使 Qd (判断的变量)变成一,而后一一排查千位不能超过 \(7\),百位不能超过 \(4\),十位不能超过 \(5\),个位不能超过 \(6\)等等……

这时候大约轮廓就出来了:(以这个题为例)

变量名 意义
Ws 位数
pre 前一个数
Qd1 判断是否有前导零
Qd2 判断是否到了边界
top 枚举的数不能超过 top
ans 记录答案
int dp(int Ws, int pre, bool Qd1, bool Qd2) {
	if (!Ws) return 1; // 若是位数减到了 0,说明以前的数都是合法的,返回 1 。
	if (!Qd1 && !Qd2 && f[Ws][pre] != -1) return f[Ws][pre]; // 若是没有前导零而且未到达边界,且记忆数组 f 不为空,就返回 f 。
	int top = Qd2 ? a[Ws] : 9, ans = 0; // 判断上限
	for (int i = 0; i <= top; ++i) { // 枚举数
		if (abs(pre - i) < 2 && !Qd1) continue; // 不合法,剪枝
		ans += (i || !Qd1) ? dp(Ws - 1, i, 0, Qd2 && i == a[Ws]) : dp(Ws - 1, i, 1, Qd2 && i == a[Ws]); // 判断是否有前导零,如有,则使 Qd2 变成 1 ,若是到达边界,则使下一个数的 Qd2 变成 1 。
	}
	if (!Qd1 && !Qd2) f[Ws][pre] = ans; // 记忆 ans 。
	return ans;
}

我相信到如今仍是有一些疑问的,好比,为何要先判断 Qd1 和 Qd2 后才能进行记忆化,才能返回 f 值。

解释一下

咱们能够想象,\(f[i][j]\) 数组记录的是第 \(i\) 位数,前一个数为 \(j\) 的数的方案数,

那么若是这个数组已经在还未到达边界,还未有前导零的时候已经被更新过了,这时候递归到边界, \(f[i][j]\) 记录的是后面的数从 \([0,9]\) 的方案数,但边界不必定能取到 \([0,9]\),因此不判边界和前导零显然是不对的。

再放个例题

给定两个正整数 \(a\)\(b\),求在 \([a,b]\) 中的全部整数中,每一个数码各出现了多少次。

范围:\(a,b\in[1,10^{12}]\)

当你看到这个数据范围的时候,毫无疑问这绝对又是个数位 dp 题,

分析

与上一题同样,但条件变成计数了(每一个数码有多少个)。

代码实现

设一个 \(f[N][20]\) 来记录状态,其中的 \(f[i][j]\) 表示位数最高位为 \(i\),该数码在这个数中出现了几回为 \(j\)

而后跟上一题同样。

判断边界:

跟上一题同样,并且仍是要判断前导零(毕竟不能把前导零计入 \(0\) 的计数中)。

这时候代码就出来了:(以这个题为例)

变量名 意义
Ws 位数
Qd1 判断是否有前导零
Qd2 判断是否到了边界
shu 数码
sum 记录该数码出现多少次
top 枚举的数不能超过 top
ans 记录答案
int dp(int Ws, bool Qd1, bool Qd2, int sum) {
	if (!Ws) return sum;
	if (!Qd1 && !Qd2 && f[Ws][sum] != -1) return f[Ws][sum];
	int top = Qd2 ? a[Ws] : 9, ans = 0;
	for (int i = 0; i <= top; ++i)
		ans += dp(Ws - 1, !(i || !Qd1), Qd2 && i == a[Ws], sum + ((i == shu) && (i || !Qd1)));
	// 这里解释一下,当 i 为该数码时,判断该数是否为前导零。
	if (!Qd1 && !Qd2) f[Ws][sum] = ans;
	return ans;
}

如今应该能看出来了,数位 dp 什么的,就是个板子(对于记搜来讲)~~

例题

  1. 洛谷 P2657 [SCOI2009] windy 数(上面第一道例题)

  2. 洛谷 P2602 [ZJOI2010]数字计数(上面第二道例题)

  3. 洛谷 P4999 烦人的数学做业

剩下的本身搜~~

相关文章
相关标签/搜索