时隔多日,我终于再次开始写博客了!!数组
上午听了数位 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 什么的,就是个板子(对于记搜来讲)~~
洛谷 P2657 [SCOI2009] windy 数(上面第一道例题)
洛谷 P2602 [ZJOI2010]数字计数(上面第二道例题)
剩下的本身搜~~