数位 DP 模板
一句话说明
数位 DP 的本质是“按位构造上界以内的数字”,其中
tight决定当前位能不能自由取0..9。
模板适用场景
- 统计
[0, limit]内满足数位条件的数字个数 - 数位和约束
- 相邻数字约束
- 不重复数位、包含某种模式等约束
最常见状态
pos:当前处理到第几位tight:前缀是否仍然贴着上界started:是否已经出现第一个非前导零- 其他约束状态:数位和、上一个数字、mask 等
Go 模板:统计数位和等于目标值
func CountDigitSum(limit int, target int) int {
if limit < 0 || target < 0 {
return 0
}
digits := []int{}
for _, ch := range strconv.Itoa(limit) {
digits = append(digits, int(ch-'0'))
}
type State struct {
Pos int
Sum int
Tight int
}
memo := map[State]int{}
var dfs func(pos, sum int, tight bool) int
dfs = func(pos, sum int, tight bool) int {
if sum > target {
return 0
}
if pos == len(digits) {
if sum == target {
return 1
}
return 0
}
key := State{Pos: pos, Sum: sum, Tight: boolToInt(tight)}
if !tight {
if val, ok := memo[key]; ok {
return val
}
}
upper := 9
if tight {
upper = digits[pos]
}
total := 0
for d := 0; d <= upper; d++ {
total += dfs(pos+1, sum+d, tight && d == upper)
}
if !tight {
memo[key] = total
}
return total
}
return dfs(0, 0, true)
}
func boolToInt(b bool) int {
if b {
return 1
}
return 0
}为什么区间统计总写成 f(r) - f(l-1)
因为数位 DP 最自然解决的是:
[0, limit]所以区间 [l, r] 只要拆成:
count(r) - count(l-1)就行。
易错点
数位 DP 模板最容易错的地方
tight的下一状态是tight && d == upper。- 只有不依赖具体上界前缀的状态,才适合记忆化。
- 前导零是否参与统计,要先想清题意,必要时加
started。
复杂度
复杂度取决于状态数,通常可以理解为:
位数 * 每位可选数字 * 额外状态规模相关主题
返回:算法模板