Skip to content

数位 DP (Digit DP)

[!abstract] 核心思想 按数字的每一位进行 DP,通常用于「统计 [0, n] 范围内满足某种条件的数字个数」。


模板框架

typescript
function digitDP(n: number): number {
  // 1. 将数字转为数字数组
  const digits = n.toString().split('').map(Number);

  // 2. DFS + 记忆化
  const memo: Map<string, number> = new Map();

  function dfs(pos: number, tight: boolean, leadZero: boolean, ...state): number {
    // pos: 当前处理到第几位
    // tight: 是否受上界限制
    // leadZero: 是否有前导零

    // 终止条件
    if (pos === digits.length) return 1;

    const key = `${pos}-${tight}-${leadZero}-${state.join(',')}`;
    if (memo.has(key)) return memo.get(key);

    let ans = 0;
    const limit = tight ? digits[pos] : 9;

    for (let d = 0; d <= limit; d++) {
      const newTight = tight && d === limit;
      const newLeadZero = leadZero && d === 0;

      ans += dfs(pos + 1, newTight, newLeadZero, ...newState);
    }

    memo.set(key, ans);
    return ans;
  }

  return dfs(0, true, true);
}

1. 统计 1 的个数

问题

  • 1~n 中数字 1 出现的次数
typescript
function countDigitOne(n: number): number {
  if (n <= 0) return 0;

  let ans = 0;
  let high = n, cur = 0, low = 0;
  let digit = 1;

  while (high !== 0) {
    high = Math.floor(n / (digit * 10));
    cur = Math.floor((n / digit) % 10);
    low = n % digit;

    // 当前位的贡献
    if (cur === 0) {
      ans += high * digit;
    } else if (cur === 1) {
      ans += high * digit + low + 1;
    } else {
      ans += (high + 1) * digit;
    }

    digit *= 10;
  }

  return ans;
}

// DP 解法
function countDigitOneDP(n: number): number {
  const s = n.toString();
  const digits = s.split('').map(Number);
  const memo: Map<string, number> = new Map();

  function dfs(pos: number, count: number, tight: boolean): number {
    if (pos === digits.length) return count;

    const key = `${pos}-${count}-${tight}`;
    if (!tight && memo.has(key)) return memo.get(key);

    let ans = 0;
    const limit = tight ? digits[pos] : 9;

    for (let d = 0; d <= limit; d++) {
      ans += dfs(pos + 1, count + (d === 1 ? 1 : 0), tight && d === limit);
    }

    if (!tight) memo.set(key, ans);
    return ans;
  }

  return dfs(0, 0, true);
}

2. 数字游戏

问题

  • 给定数字 n,求第 k 小的「幸运数」
typescript
function findKthNumber(n: number, k: number): number {
  let cur = 1;
  k--; // 已经选了 cur

  while (k > 0) {
    const steps = getSteps(n, cur, cur + 1);
    if (steps <= k) {
      cur++;       // 移到兄弟节点
      k -= steps;
    } else {
      cur *= 10;    // 移到第一个子节点
      k--;
    }
  }

  return cur;
}

function getSteps(n: number, n1: number, n2: number): number {
  let steps = 0;
  while (n1 <= n) {
    steps += Math.min(n + 1, n2) - n1;
    n1 *= 10;
    n2 *= 10;
  }
  return steps;
}

3. 包含特定数字的数

问题

  • 统计 1~n 中包含数字 2 的数的个数
typescript
function numberCount(n: number): number {
  if (n <= 0) return 0;

  let ans = 0;
  let digit = 1;
  let high = Math.floor(n / 10);
  let cur = n % 10;
  let low = 0;

  while (high !== 0 || cur !== 0) {
    if (cur === 0) {
      // 当前位为 0,不贡献
    } else if (cur === 1) {
      ans += low + 1;
    } else {
      ans += digit;
    }

    // 继续处理高位
    if (high > 0 && cur === 2) {
      ans += low + 1;
    } else if (high > 0 && cur > 2) {
      ans += digit;
    }

    ans += high * digit;

    // 更新
    low += cur * digit;
    cur = high % 10;
    high = Math.floor(high / 10);
    digit *= 10;
  }

  return ans;
}

4. 数字和为 K 的数

问题

  • 统计 1~n 中各位数字之和等于 k 的数的个数
typescript
function numberCountKSum(n: number, k: number): number {
  const digits = n.toString().split('').map(Number);
  const memo: Map<string, number> = new Map();

  function dfs(pos: number, sum: number, tight: boolean): number {
    if (sum > k) return 0;
    if (pos === digits.length) return sum === k ? 1 : 0;

    const key = `${pos}-${sum}-${tight}`;
    if (!tight && memo.has(key)) return memo.get(key);

    let ans = 0;
    const limit = tight ? digits[pos] : 9;

    for (let d = 0; d <= limit; d++) {
      ans += dfs(pos + 1, sum + d, tight && d === limit);
    }

    if (!tight) memo.set(key, ans);
    return ans;
  }

  return dfs(0, 0, true);
}

5. 不含前导零且数字和为 K

问题

  • 统计 [L, R] 范围内数字和为 K 的数的个数
typescript
function rangeSum(a: number, b: number, k: number): number {
  return countKSum(b, k) - countKSum(a - 1, k);
}

function countKSum(n: number, k: number): number {
  if (n <= 0) return 0;

  const digits = n.toString().split('').map(Number);
  const memo: Map<string, number> = new Map();

  function dfs(pos: number, sum: number, tight: boolean, leadZero: boolean): number {
    if (sum > k) return 0;
    if (pos === digits.length) return sum === k && !leadZero ? 1 : 0;

    const key = `${pos}-${sum}-${tight}-${leadZero}`;
    if (!tight && memo.has(key)) return memo.get(key);

    let ans = 0;
    const limit = tight ? digits[pos] : 9;

    for (let d = 0; d <= limit; d++) {
      ans += dfs(
        pos + 1,
        sum + (leadZero && d === 0 ? 0 : d),
        tight && d === limit,
        leadZero && d === 0
      );
    }

    if (!tight) memo.set(key, ans);
    return ans;
  }

  return dfs(0, 0, true, true);
}

6. 经典模板题

洛谷 P2602 - 数字计数

typescript
function countDigits(l: number, r: number, target: number): number {
  return countDigit(r, target) - countDigit(l - 1, target);
}

function countDigit(n: number, target: number): number {
  if (n <= 0) return 0;

  let ans = 0;
  let digit = 1;
  let high = Math.floor(n / 10);
  let cur = n % 10;
  let low = 0;

  while (high !== 0 || cur !== 0) {
    if (cur === 0 && target === 0) {
      ans += (high - 1) * digit;
    } else if (cur === 0) {
      ans += high * digit;
    } else {
      ans += high * digit;
      if (target === 0) ans -= digit;
      if (cur > target) ans += digit;
      else if (cur === target) ans += low + 1;
    }

    low += cur * digit;
    cur = high % 10;
    high = Math.floor(hire / 10);
    digit *= 10;
  }

  return ans;
}

数位 DP 状态设计

状态说明示例
pos当前位0~len
tight是否受上界true/false
leadZero是否有前导零true/false
sum数字和0~9*len
cnt目标数字个数如 1 的个数
diff与目标差值加权求和

参见: **DP算法索引** | **DP优化**

Knowledge4J — Java 知识库