数位 dp 的特征与解题思路
题目特征
要求统计满足一定条件的数的数量(即,最终目的为计数,若要结果则只能回溯爆搜得到);
这些条件经过转化后可以使用「数位」的思想去理解和判断;
输入会提供一个数字区间(有时也只提供上界)来作为统计的限制;
上界很大(比如 10^{18}),暴力枚举验证会超时。
思路
从高到低枚举每一位,统计符合 target 的个数,并记录到 dp 数组中。枚举完毕之后则得到答案。
因此数位 dp 的第一个状态都是数位的位置,第二个状态由题意来定
模板
以 leetcode1012 为例,统计小于等于 n 的数字中每一位的数字至少重复一次的个数。
模板时灵神的模板。难点主要是 mask,isLimit,isNum 这几个标识
- mask 即 dp 的第二个状态,这边用到了状态压缩的思想,将 0 到 9 选过的状态压缩成一个数字(否则要 10 个状态)
- isLimit 标识了本次(i)选择的范围,是否受到 n 的影响。如果不引进这个变量,则需要考虑当前数字的最高位来决定本次的范围(最高位==n 的最高位时,本次的范围是[0,s[i]],最高位<n 的最高位时,本次的范围是[0,9])。可以发现这个限制是有传递的性质的,因此引入这个变量能简化范围的选择过程。
- isNum 标识了本次(i)之前是否有数字,换句话说本次(i)是否是第一个数字(最高位)。这个标识主要是解决前导 0 的问题,否则答案里会重复(前导两个 0 和前导三个 0 虽然是同个数字,但都会被记入答案)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59
| func numDupDigitsAtMostN(n int) (ans int) { s := strconv.Itoa(n)
m := len(s) dp := make([][1 << 10]int, m) for i := range dp { for j := range dp[i] { dp[i][j] = -1 } } var f func(int, int, bool, bool) int f = func(i, mask int, isLimit, isNum bool) (res int) { if i == m { if isNum { return 1 } return } if !isLimit && isNum { dv := &dp[i][mask] if *dv >= 0 { return *dv } defer func() { *dv = res }() } if !isNum { res += f(i+1, mask, false, false) } d := 0 if !isNum { d = 1 } up := 9 if isLimit { up = int(s[i] - '0') } for ; d <= up; d++ { if mask>>d&1 == 0 { res += f(i+1, mask|1<<d, isLimit && d == up, true) } } return } return n - f(0, 0, true, false) }
|