数位DP

数位dp

我们先由一道模板题引入 :

windy 数

\(step 1\) :

考虑枚举法,枚举 \(a\)\(b\) 之间的每个数,暴力check相邻两位之间合不合法 , 时间复杂度 \(O(n\log_{}{n})\)显然TLE

\(step 2\):

考虑从最高位开始爆搜 ,如果搜完最后一位就 \(ans\)++ , 时间复杂度还是 \(O(n\log_{}{n})\) ,但是对我们后续进行dp会有所启发 。

\(step 3\):

我们来重新思考一下我们进行爆搜时的状态 ,定义四个状态分别为 , \(x\) 表示当前为第 \(x\) 位 ,\(pre\) 表示上一位是多少 ,\(zero\) 表示有没有前导零 (在此题中用来确定有没有相邻两项之间差的绝对值必须小于 \(2\) 的限制), 最后一个 \(lim\) 则表示之前几位的数有没有达到上限 。

相比于直接用当前这个数的前 \(x\)\(a_1\) , \(a_2\) , \(……\) \(a_x\) 当作状态来搜索 ,我们肉眼可见的缩小了状态集 ,同时也给了我们新的启发 : 在没有前导零和上限限制时,我们只关心当前是第几位以及前一位是多少 。此时我们可以用记忆化搜索来优化 ,是为数位dp 。

定义状态 \(dp_{i,j}\) 表示考虑了到第 \(i\) 位 ,上一位为 \(j\) 的合法数字个数 ,转移就非常显然了 ,通常使用记忆化搜索来方便实现 。

DP部分的代码:

int Dp(int x,int zero,int lim,int pre){
	//x表示当前为第几位,zero表示是否有前导零,lim表示当前是否有上限限制,pre表示上一位是多少
	if(x<1)return 1;//搜完了
	if(!lim&&dp[x][pre]!=-1)return dp[x][pre];//没有上限且已经搜过
	int ret=0;
	int Up=lim?f[x]:9;//如果没有上限限制,那么这一位可以随便选,否则即为这一位的上限
	for(int i=0;i<=Up;++i){
		if(abs(i-pre)<2)continue;//不满足题目限制条件
		if(zero&&!i)ret+=Dp(x-1,zero,lim&&(i==Up),-2);//如果是前导零,那么下一位绝对值能不能小于2没有限制
		else ret+=Dp(x-1,0,lim&&(i==Up),i);//否则必须按要求选
	}if(!zero&&!lim)dp[x][pre]=ret;//没有前导零且没有上限则记忆化
	return ret;
}
inline int solve(int x){
	int now=x;cnt=0;
	while(now)f[++cnt]=now%10,now/=10;
	memset(dp,-1,sizeof(dp));
	return Dp(cnt,1,1,-2);
}

分析这份代码的时间复杂度 ,不会分析咕咕咕 , 基本上是状态数 \(num\times 10\) (其实 \(10\) 是指进制),但是对于一些限制较多的题目 ,可能每个状态重复搜索的次数较多 ,复杂度则会有所增加 。

\(tips\) :数位dp的常见技巧,求区间 \(l\) ~ \(r\) 的答案,通常可以转化为区间\(1\) ~ \(r\) 减去区间 \(1\) ~ \(l-1\) 的答案 。

此题得以解决 。

但是,问题来了,为什么 \(step 3\) 中的数位dp会比 \(step 1\) 中的枚举每个数时间复杂度更加优秀呢 ?

答案是 ,在暴力枚举的过程中 ,我们可以发现对于两个数 \(999\)\(1999\) 来说 ,其后三位都一样 ,但是我们依然对于两个数都check了其每一位是否合法 ,而数位dp的过程则是将相同的数段(对于 \(999\)\(1999\) 来说相同的数段就是 \(999\))的方案一次性统计 。例如 ,若我们确定了两个数的前三位分别为 \(458\)\(138\) ,还剩下个位和十位没有确定 ,我们就可以快速知道合法的方案数为 “考虑了前 \(2\) 位 ,上一位为 \(8\) 的方案数” 即 \(dp_{2,8}\) 的值 ,而在暴力枚举的过程中 ,我们还需要分别去枚举这两个数的后两位。

例题(按个人认为的难度升序排序):

\(1\).数字统计

Link

还是一道模板题,我们只需要知道考虑到第几位就好,定义状态 \(dp_x\) 表示考虑前 \(x\) 位,没有上限限制的当前数字出现次数,从 \(0\) ~ \(9\) 分别dp即可。

\(2\).花神的数论题

Link

二进制下数位dp的模板,直接 \(dp_{i,j}\) 表示前 \(i\) 位,有 \(j\)\(1\)

\(3\).计数

Link

似乎是可重复元素的康托展开,可是我连排列的康托展开都忘了,贴一个排列的康托展开的公式:\(pos=(\sum_{i=1}^{n}{(n-i)!} \times\sum_{j=i}^{n}{[a_j<a_i]})+1\) ,相当于是在统计在第 \(i\) 位开始比这个排列小的排列的的个数,即前 \(i-1\) 位不变,第 \(i\) 位在 \(i+1\) 位到 \(n\) 位之间选一个比 \(a_i\) 小的数放到第 \(i\) 位,后 \(n-i\) 位随便放的个数,暴力计算是 \(O(n^2)\) 的,可以用树状数组优化到 \(O(n\log_{}{n})\)

然后考虑此题的可重复的康拓展开,其实就是多了一个可重集的排列个数:\(\frac{n!}{\prod_{i=1}^{k}{num_i}}\) ,由于数据范围较小,直接暴力枚举第 \(i\) 位选后面哪个小于 \(a_i\) 的数来替换,后面就是一个可重集的排列个数了,注意直接用上面的柿子来算会爆 long long ,随便解决一下就好了,时间复杂度 \(O(n\times 10^3)\) ,甚至还可以化柿子优化到 \(O(n\log_{}{10})\)

感觉康托展开和数位dp有点像,又感觉没什么关系。

\(4\).手机号码

Link

相比板子只是状态多了一点,看起来很吓人,其实照写,状态为 \(dp_{i,j,k,0/1,0/1,0/1}\) ,表示考虑了前 \(i\) 位,上一位数字是 \(j\) ,当前数字连续了 \(k\) 次,需要出现至少 \(3\) 个数字的限制是否达成,是否出现过 \(4\) ,是否出现过 \(8\) ,转移时枚举当前这一位 \(num\) ,直接转移:

ret+=DP(x-1,i,i==pre?last+1:1,can|((i==pre?last+1:1)>=3),if4|(i==4),if8|(i==8),lim&&i==Up,0);

但是注意,当 \(l=10^{10}\) 时,\(l-1\) 会变成 \(10\) 位数,需要注意判一下。

\(5\).同类分布

Link

此题不是很难,一眼看上去肯定是数位dp,我们要考虑的状态是当前位数 \(x\) ,当前各位数字之和 \(sum\) ,前几位对最后的 \(sum\) 取模的结果 \(……\) 欸,不过如何通过当前的 \(sum\) 下的余数知道最后的 \(sum\) 下的余数呢?不过一看数据范围,\(a,b<=10^{18}\) ,那没事了,直接暴力枚举最后的 \(totsum\) ,状态为 \(dp_{i,j,k}\) 表示考虑了前 \(i\) 位,后 \(i\) 位各位数字之和位 \(j\) ,对 \(totsum\) 取模结果为 \(k\) 的合法数字个数,然后就随便转移了。

分析时间复杂度,枚举 \(totsum\)\(10\times\log_{}{a}\) 的,每次数位dp是 \(10\times(10\times\log_{}{a})^2\times\log_{}{a}\) ,总复杂度 \(O(10^4\times\log_{}^{4}{a})\) ,算下来似乎是 \(10^9\) ,不过在 \(3 s\) 时限下还是过了,如果不放心,可以加上几个剪枝,比如如果剩下的数字都填 \(9\) 都填不满的话直接走人等。

\(6\).Beautiful numbers

Link

此题难度没有预期中的小,自己最初的做法是外层枚举二进制状态 \(sta\) 表示都选中了哪些数,然后内层 \(dp_{i,j,k}\) 表示考虑了前 \(i\) 位,前面的部分对当前的 \(lcm\) 取模结果是 \(j\) ,选取的数的二进制状态时 \(k\) 的合法数字个数,没想到复杂度是 \(O(2^{18}\times10\times2520\times\log_{}{n})\) 约为 \(10^{11}\) ,结果小样例跑 \(20 s\)

我光是想到了此题要结合状压来解决,但是忘却了一些数学知识:对于当前的 \(lcm\) 记为 \(x\) ,一定满足 \(x|lcm(1,2,3...9)=2520\) ,那么对于一个数 \(a\) ,就满足:

\(a\equiv w\pmod{2520}\) ,则有 \(a\equiv w\pmod{x}\)

所以我们直接设计状态 \(dp_{i,j,k}\) 表示考虑了前 \(i\) 位,当前数字对 \(2520\) 取模的结果为 \(j\) ,当前已选的数字的 \(lcm\)\(k\) 的合法数字个数,边界就是 \(x=0\land j\equiv 0\pmod{k}\)

但是问题又来了,直接这样做空间是 \(\frac{18\times 2520^2\times 4}{1024^2}\approx436.05 MB\) ,空间限制是 \(256 MB\) 。不过很容易发现合法的选中的数的 \(lcm\) 的个数远不到 \(2520\) ,最多也就 \(2520\) 的约数那么多,直接离散化一下将第三维开小就好了。

\(+\infty\).例题总结:

\(\bullet\) 个人感觉数位dp的难题不会太多,就算有很难的题出现概率也不大,大多数还是板子,彻底理解以上几题之后除了毒瘤题应该都问题不大。

\(\bullet\) 数位dp经常写挂,经常会出现第一次访问某个状态时该状态的值不是 \(-1\) ,这时候常常是dp状态假了。

\(\bullet\) 如果要进行多次只是上界限制不同的数位dp,最好不要每次清空,会慢很多 。

\(\bullet\) 说来数位dp还是很妙的,有时候写完自己设计的状态过了之后都会诧异:”这为什么能对啊?“,可能还是需要一段时间去更深入的理解。

posted @ 2022-11-03 19:56  LzjNotFound  阅读(66)  评论(0)    收藏  举报