数位DP
数位DP模型: 问[L -- R]
这些(数字), 他们的贡献之和;
其实, 数位DP 这个名字, 并不能完全体现该算法; 因为, 该算法有时是可以不借助DP的, 而是借助(排列组合), 或直接暴力枚举;
该算法的核心, 在于: 将 这些数 (非常多) 拆分成 一些集合 (这些集合之和 等同 这些数), 一个集合里包含有很多数
然后单个集合, 通过(Dp 或 排列组合 或 暴力), 可以在(线性时间) 一次性的 求解出 该集合里所有数的贡献, 即该集合的贡献
因此, 核心在于 (集合拆分) + (处理单个集合的算法)
… 这个(集合拆分)的过程, 是一种 二叉树的过程; 因此, 我觉得, 数位DP 叫做: 数位二叉树, 更精确
数位二叉树
暴力求解, 就是FOR_( i, L, R, 1){ ans += 贡献( i);}
一般数据范围是: 0 <= L/R <= 10^n
, 这个n
, 可能是18
(LL范围), 可能是100, 1000
(比LL还大)
… 所以, 暴力肯定是不行的;
数位二叉树, 都会将 [L -- R]的答案
转换为: Prefix( R) - Prefix( L - 1)
的问题;
其中Prefix( x)
表示[0 -- x]
范围的贡献; Prefix
必须是从0
开始, 即便L >= 1
, 也要从0
开始, 下面会谈到;
问题是: 如何求[0 -- x]
的贡献呢?
有一点要说明, 图中的 (第一类的 第一集合, 即[0,00 -- 0,99]
), 不能认为他是[0 -- 99]
, 这是不可以的!!! (当然我们的目标是让他代表[0 -- 99]
)
或者说, 该二叉树的拆分数集, 是把 x = ABC
当成是一个(字符串), 然后拆分出来的所有集合, 都是 (字符串数)
… 解释下, 字符串数 与 数字 的概念; 这非常重要, 不仅在这里, 在接下来的数位DP递推, 也非常重要;
… 字符串数, 是可以带(前导零)的; 而数字 不可以带(前导零); 题目一般求解的都是(数字), 而数位DP求解的是(字符串数)
… 两者是可以对应的;
… 任意一个(数字), 一定对应(等价于) 无数个 (字符串数);
… … 比如, 数字x = 123
, 则等价于: 字符串123, 0123, 00123, 000123, 0000123, ...
… 任意一个(字符串数), 一定对应(等价于) 一个 (数字);
… … 比如, ( 字符串数x = 00123
, 则等价于: 数字123
), ( 字符串数x = 0001
, 则等价于: 数字1
), ( 字符串数x = 00000
, 则等价于: 数字0
)
因为 (任何的字符串数
, 一定对应 唯一的数字
), (不带前导零的字符串数
, 与 其数字
, 完全相同), 则有:
数位二叉树中, 所有的集合 (即 字符串数的集合):
… 除了(第一类的第一集合), 所有集合, 都不带前导零; 所以, 其与 (数字) 是完全相同的
… … 比如, [A,1,0 -- A,1,9]
这个集合 中 任意一个字符串数, 和 其对应的数字 完全相同; 因为不含(前导零);
… … 即对于"A13"
这个字符串数, 你求他对答案的贡献, 和求123
这个数字, 是完全一样的, 因为形态完全一样;
… 但是, (第一类的第一集合) [0,00 -- 0,99]
这个字符串数集合, 全部都带有(前导零);
… … 你求 (001
这字符串数 对答案贡献) 与 (1
这个数字 对答案贡献), 两者就不同了; 因为形态就不同
… … (假如答案贡献是 判断是否含有相同数位
, 001
有两个0
含有相同数位; 而1
是没有的)
… … 处理这个(带有前导零的)集合, 会通过继续分类的方式, 把前导零给消除掉, 下面会讲到;
按照上面的二叉树方式, 举个例子: x = 340
(第一类: [000 -- 099]
, [100 -- 199]
, [200 -- 299]
)
(第二类: [300 -- 309]
, [310 -- 319]
, [320 -- 329]
, [330 -- 339]
)
(第三类: 空)
(单独数: 340
)
… 暂时把(第一类的第一集合) 认为是[0 -- 99]
, 即去掉前导零; 其他的集合, 都不含有前导零, 就把他转换为(数字);
… 即, 此时, 所有集合, 都代表(数字); (数位二叉树得到的是: 字符串数)
一共划分为 (3 + 4 + 0 = 7
个集合) + (一个数340
), 此时他们 等价于 [0 -- 340]
每一类 的 集合个数, 等价于: (该类对应的数位数值)
比如, 第一类对应3
, 他就有3
个集合; 第三类对应0
, 他有0
个集合
暂时不考虑: (第一类的第一集合) 和 (单独数), 看这些集合, 他们都形如: [pre,k个0 -- pre,k个9]
(即一共是10^k
个数)
其中, 如果把pre
当作是个(数字), 则 pre
不含前导零, 且pre > 0
; k = [0 -- n-1]
, 即, 这些集合, 表示[100 -- 339]
这些数
即这个集合, 都是一个统一的形式: [pre, k个(自由数位)]
, 有一个固定前缀pre
(所有数的前缀都相同)
后缀, 长度为k
, 是从00 -- 99
的递增, 我们称 这个后缀, 是由k
个自由数位 组成的 , 一个自由数位 即[0 - 9]
的任意
即, 一个固定前缀(pre), 后面有k
个自由数位 的形式
每一个集合, 去调用一个Aligned( pre, k)
的函数, 这个函数, 表示[pre,k个0 -- pre,k个9]
这些 (字符串数) 的答案; (也就表示: 该集合的答案)
起名为Aligned
, 就是表示(对齐), 表示, 这些数 都是位数相同的;
Aligned
代表的是数字
还是字符串数
, 其实都一个意思;
因为, pre没有前导零
且 pre > 0
, 这两个条件 就会决定 此时, (他所代表的 (数字 与 字符串数)是一样的)
Aligned
怎么实现, 暂且不论;
对于(单独数340
), 直接写一个Single_number( x)
函数, 特判;
关键是, 对于(第一类的 第一集合), 即[000 -- 099]
这些字符串数;
他肯定不能像其他集合一样, 调用Aligned( {0}, 2)
, 因为字符串数001
与 数字1
形态不相同;
因为我们的目的是: 让他去表示[0 -- 99]
, 将这个集合拆分为: [0 -- 0]
[1 -- 9]
[10 -- 99]
[100 -- 999]
…
为啥要这样划分呢? 这样, 每个集合 都是(Aligned
) 数位相同; 这个思想在数位DP里非常重要
… 特别的[0 -- 0]
他表示0
这个数, 让他, 去调用Single_number( 0)
此时剩下[1 -- 9]
[10 -- 99]
… 每个集合, 都是Aligned. 但他不符合Aligned( pre, k)
函数所要求的 形式
我们想办法, 让他变成去调用Aligned(pre, k)
函数
对每个集合, 继续划分: [1 - 9]
划分为: [1-1] [2-2] ... [9-9]
[10 -- 99]
划分为: [10-19] [20-29] ... [90-99]
此时, 所有区间, 不仅是(对齐的), 而且, 符合 (一个固定前缀) (后面若干自由数位)的Aligned()
函数形式;