数位DP,从入门到放弃
PS:这篇博客主要是浅谈数位DP,但是博主不能保证看一遍都能看懂,建议多读几遍(最好多换几台电脑进行阅读)
从一道例题讲起:
例1.
给出整数m、n, 求m ~ n 中不含子串‘62’和‘4’的整数的个数,比如621就不满足,612满足,124不满足。
想法:
这道题我们首先可以想到暴力, 从m到n循环,如果满足条件,则ans++
, 这个想法很好,也很好实现, 但是看一下一般的题的数据范围:
【约束条件】1 ≤ a ≤ b ≤ 10^18
这时,循环显得也很费时间,可以先预处理一遍,打表,我计算了一下,也就是18626451492G这么大的文件,GoodLuck!
还有一种方法就是 数位DP
所谓数位DP,就是把一个十进制数的每一位拆开,在把每一位的信息作为状态来推出答案。
比如:12345这个数可拆为1、2、3、4、5。
推状态的方法有很多,我采用的是记忆化搜索(就是dfs时记录一个节点是否访问过来节省时间),我个人认为记忆化搜索是做数位DP的题的最简便、最通用的方法, 因为记忆化搜索代码短,便于理解,每道题只用把模板稍微改一下就能A题(至少我现在这样认为)
再回到例一,我们可以用dp[pos][pre]来保存在第pos位(就是刚刚提到的把数拆开),以pre结尾时满足题意的数的个数。
这个dp函数是解题的关键, 我就不在这里讲这么想出来的想出dp函数代表的意义以后,就可以套模板了。
解释模板:
int solve( int x ){
int pos = 0;
for (; x; a[pos] = x % 10, x /= 10, pos++);
return dfs( pos-1, 0, true);
}
solve函数主要作用是把每个数拆分开(如果看不懂for循环,建议学习一下后缀数组)
int dfs( int pos, int sta, bool lim ){
if ( pos < 0 ) return /*大多为1*/;
if ( !lim && dp[pos][/* 根据题意改变*/] != -1 ) return dp[pos][sta];
int up = lim ? a[pos] : 9, ret = 0;
for (register int i = 0; i <= up; i++ ){
if ( sta && /*根据题意*/ ) continue;
ret += dfs( pos-1, i == 6, lim && i == a[pos]);
}
if ( !lim ) dp[pos][sta] = ret;
return ret;
}
dfs函数是最主要的函数,也是整个程序中代码量最大的函数,它的作用是求出答案。其中:
lim && i == a[pos]