funccountSpecialNumbers(n int)int { s := strconv.Itoa(n) l := len(s) memo := make([][1 << 10]int, l) for i := range memo { for j := range memo[i] { memo[i][j] = -1 } } var f func(int, int, bool, bool)int f = func(i int, mark int, isLimit bool, isNum bool) (res int) { if i == l { if isNum { return1 } return } if !isLimit && isNum { p := &memo[i][mark] if *p >= 0 { return *p } deferfunc() { *p = res }() } if !isNum { // 跳过当前数位 res += f(i+1, mark, false, false) } d := 0 if !isNum { d = 1// 前面没数字,最低填1 } up := 9 if isLimit { up = int(s[i] - '0') } for ; d <= up; d++ { if mark>>d&1 == 0 { // d没选中过 res += f(i+1, mark|1<<d, isLimit && d == up, true) } } return } return f(0, 0, true, false) }
强化训练(数位 DP)
233. 数字 1 的个数
根据上面模版定义 f(i,cnt,isLimit,isNum),表示构造到从左到右第 i 位,已经出现了 cnt 个 1,在这种情况下,继续构造最终会得到的 1 的个数 (你可以直接从回溯的角度理解这个过程,只不过是多了个记忆化)。
funccountDigitOne(n int)int { s := strconv.Itoa(n) l := len(s) dp := make([][]int, l) for i := range dp { dp[i] = make([]int, l) for j := range dp[i] { dp[i][j] = -1 } } var f func(int, int, bool)int f = func(i int, cnt int, isLimit bool)int { if i == l { return cnt } var res int if !isLimit { p := &dp[i][cnt] if *p >= 0 { return *p } deferfunc() { *p = res }() } up := 9 if isLimit { up = int(s[i] - '0') } for d := 0; d <= up; d++ { // 枚举要填入的数字 d c := cnt if d == 1 { // 满足要求结果+1 c++ } res += f(i+1, c, isLimit && d == up) } return res } return f(0, 0, true) }
funcnumberOf2sInRange(n int)int { s := strconv.Itoa(n) l := len(s) dp := make([][]int, l) for i := range dp { dp[i] = make([]int, l) for j := range dp[i] { dp[i][j] = -1 } }
var f func(int, int, bool)int f = func(i, cnt int, isLimit bool) (res int){ if i == l { return cnt }
if !isLimit { p := &dp[i][cnt] if *p >= 0 { return *p } deferfunc() { *p = res}() }
up := 9 if isLimit { up = int(s[i]-'0') }
for d := 0; d <= up; d++ { c := cnt if d == 2 { c++ } res += f(i+1, c, isLimit && d == up) } return } return f(0, 0, true) }
600. 不含连续1的非负整数
根据上述通用模版 f(i,pre,isLimt,isNum) 表示构造从左往右第 i 位及其之后数位的合法方案数,其中 pre 表示第 i−1 位是否为 1,如果为真则当前位不能填 1。
funcfindIntegers(n int)int { s := strconv.FormatInt(int64(n), 2) l := len(s) dp := make([][2]int, l) for i := range dp { dp[i] = [2]int{-1, -1} } var f func(int, int8, bool)int f = func(i int, pre int8, isLimit bool) (res int) { if i == l { return1 } if !isLimit { p := &dp[i][pre] if *p >= 0 { return *p } deferfunc() { *p = res }() } up := 1 if isLimit { up = int(s[i] & 1) } res = f(i+1, 0, isLimit && up == 0) // 填入0 if pre == 0 && up == 1 { // 前一位是0,并且这一位最高可以填1 res += f(i+1, 1, isLimit) } return } return f(0, 0, true) }
902. 最大为 N 的数字组合
因为这道题填入的数字是直接从 digits 中选择,所以可以胜利第二个参数,然后只记忆化 i 就可以了,因为:
funcatMostNGivenDigitSet(digits []string, n int)int { s := strconv.Itoa(n) l := len(s) dp := make([]int, l) for i := range dp { dp[i] = -1 } var f func(int, bool, bool)int f = func(i int, isLimit bool, isNum bool) (res int) { if i == l { if isNum { return1 } return } if !isLimit && isNum { // 不受到任何约束 p := &dp[i] if *p >= 0 { return *p } deferfunc() { *p = res }() } if !isNum { // isLimit 因为当前位置没有填数字,可以跳过 res += f(i+1, false, false) } // 根据是否收到约束,决定可以填入数字的上线 up := byte('9') if isLimit { up = s[i] } // 对于一般的题目而言,如果此时 isNum 为 false, 则必须从1开始枚举,本体中没有0,所以无需处理这种情况 for _, d := range digits { if d[0] > up { break } res += f(i+1, isLimit && up == d[0], true) } return } return f(0,true, false) }
1067. 范围内的数字计数
这题思路和第一题和第二题一致,因为 high>=low 所以 high 中 d 个数,包含了 low 中 d 的个数,计算出 high 中的个数即可
注意,low 可能是包含 d 的整数,所以求 low 时需要将 low−1 再求,避免最终结果未统计到 low
funccount(n, x int)int { s := strconv.Itoa(n) l := len(s) dp := make([][]int, l) for i := range dp { dp[i] = make([]int, l) for j := range dp[i] { dp[i][j] = -1 } } var f func(int, int, bool)int f = func(i int, cnt int, isLimit bool) (res int) { if i == l { return cnt } if !isLimit { p := &dp[i][cnt] if *p >= 0 { return *p } deferfunc() { *p = res }() } up := 9 if isLimit { up = int(s[i] - '0') } for d := 0; d <= up; d++ { c := cnt if d == x { c++ } res += f(i+1, c, isLimit && d == up) } return } return f(0, 0, true) }
1397. 找到所有好字符串
依旧是套用上面的模版,不同的是在递归时,进入递归的时机,由kmp算法提供,注意几个小细节即可:
由于 next 数组中并没有 −1,进入下一次递归时,当 kmp 指针在字符串头部时,也会出现 d=evil[nxt] 的情况,若直接将 nxt+1 进入递归,是认为两个字符串匹配上了,实际则可能不是,这里需要将 nxt 减 1 后进入递归。使用多一位的 next 数组也可以,将循环条件改为 for nxt > -1 && d != evil[nxt] 即可。
funcfindGoodStrings(n int, s1 string, s2 string, evil string)int { m := len(evil) dp := make([][]int, n) for i := range dp { dp[i] = make([]int, m) for j := range dp[i] { dp[i][j] = -1 } } next := make([]int, m) for i := 1; i < m; i++ { j := next[i-1] for j > 0 && evil[i] != evil[j] { j = next[j-1] } if evil[i] == evil[j] { j++ } next[i] = j } var f func(int, int, bool, string)int f = func(i int, pos int, isLimit bool, s string) (res int) { if pos == m { return } if i == n { return1 } if !isLimit { p := &dp[i][pos] if *p >= 0 { return *p } deferfunc() { *p = res }() } d := byte(97) up := byte(122) if isLimit { up = s[i] } for ; d <= up; d++ { nxt := pos for nxt > 0 && d != evil[nxt] { nxt = next[nxt-1] } // 此处要注意,当 nxt == 0 的时候,会存在 d != evil[nxt] 的情况 // 若直接 nxt + 1 进入递归,是认为此时的两个字符一定是匹配上了,实际上可能并没有 if nxt == 0 && d != evil[nxt] { nxt = -1 } res += f(i+1, nxt+1, isLimit && d == up, s) res %= mod } return } res := f(0, 0, true, s2) - f(0, 0, true, s1) if res < 0 { res += mod } if strings.Index(s1, evil) == -1 { res += 1 } return res }