Skip to main content
Back to problems
#3348
Hard Algorithms

Smallest divisible digit product ii

Math String Backtracking Greedy Number Theory
14.5% acceptance
Feb 24, 2026
57
18
You are given a string num which represents a positive integer, and an integer t. A number is called zero-free if none of its digits are 0. Return a string representing the smallest zero-free number greater than or equal to num such that the product of its digits is divisible by t. If no such number exists, return "-1".

Solution

Rust
Time O(n * m)
Space O(n * m)
LeetCode
solution.rs
fn gcd64(a: i64, b: i64) -> i64 {
  if b == 0 { a } else { gcd64(b, a % b) }
}

/// Precompute table: dp23[i][j] = minimum digits (from {2,3,4,6,8,9}) to cover
/// at least 2^i * 3^j.  Forward BFS from (0,0): each digit extends coverage.
/// Digit contributions: 2→(1,0), 3→(0,1), 4→(2,0), 6→(1,1), 8→(3,0), 9→(0,2).
/// Single pass over the grid suffices because all contributions are non-negative:
/// every state propagated to has index ≥ current in both dimensions (with
/// clamping at a_max/b_max), so it is visited later in row-major order.
fn build_dp23(a_max: usize, b_max: usize) -> Vec<Vec<usize>> {
  const INF: usize = usize::MAX / 2;
  let mut dp = vec![vec![INF; b_max + 1]; a_max + 1];
  dp[0][0] = 0;
  for i in 0..=a_max {
    for j in 0..=b_max {
      let cur = dp[i][j];
      if cur == INF { continue; }
      for &(di, dj) in &[(1usize, 0usize), (0, 1), (2, 0), (1, 1), (3, 0), (0, 2)] {
        let ni = (i + di).min(a_max);
        let nj = (j + dj).min(b_max);
        let cost = cur + 1;
        if cost < dp[ni][nj] {
          dp[ni][nj] = cost;
        }
      }
    }
  }
  dp
}

/// Factorize remaining_t (which always divides the original t) into 2^a*3^b*5^c*7^d
/// and return the minimum digits needed using the precomputed dp23 table.
#[inline]
fn min_needed(mut rt: i64, dp23: &[Vec<usize>]) -> usize {
  if rt == 1 { return 0; }
  let mut a = 0usize; while rt % 2 == 0 { rt /= 2; a += 1; }
  let mut b = 0usize; while rt % 3 == 0 { rt /= 3; b += 1; }
  let mut c = 0usize; while rt % 5 == 0 { rt /= 5; c += 1; }
  let mut d = 0usize; while rt % 7 == 0 { rt /= 7; d += 1; }
  dp23[a][b] + c + d
}

impl Solution {
  pub fn smallest_number(num: String, t: i64) -> String {
    // Only prime factors 2,3,5,7 can appear as single digits; reject others.
    let mut rem = t;
    for p in [2i64, 3, 5, 7] { while rem % p == 0 { rem /= p; } }
    if rem != 1 { return "-1".to_string(); }

    // Factorize t once to determine dp23 table dimensions.
    let mut tmp = t;
    let mut a_max = 0usize; while tmp % 2 == 0 { tmp /= 2; a_max += 1; }
    let mut b_max = 0usize; while tmp % 3 == 0 { tmp /= 3; b_max += 1; }
    // Build O(a_max * b_max) DP table (no heap allocation for BFS/VecDeque).
    let dp23 = build_dp23(a_max, b_max);

    let digits_ref: Vec<u8> = num.bytes().map(|b| b - b'0').collect();
    let n = digits_ref.len();
    let min_initial = min_needed(t, &dp23);

    let mut buf = Vec::with_capacity(n + 16);

    // Attempt tight fill only when t can be satisfied within n digits.
    if min_initial <= n && Self::fill(&digits_ref, 0, t, true, &mut buf, &dp23) {
      return buf.iter().map(|&d| (d + b'0') as char).collect();
    }

    // Try the smallest number of extra digits that makes t satisfiable.
    // t ≤ 10^14 → at most 15 extra digits suffice.
    let start_extra = min_initial.saturating_sub(n).max(1);
    for extra in start_extra..=15usize {
      buf.clear();
      if Self::fill_free(n + extra, t, &mut buf, &dp23) {
        return buf.iter().map(|&d| (d + b'0') as char).collect();
      }
    }
    "-1".to_string()
  }

  /// Tight fill: greedy with backtracking, constraining result >= num.
  /// Uses exact `min_needed` pruning to avoid dead-end paths.
  fn fill(
    digits: &[u8], pos: usize, remaining_t: i64, tight: bool,
    buf: &mut Vec<u8>, dp23: &[Vec<usize>],
  ) -> bool {
    if pos == digits.len() { return remaining_t == 1; }

    // Product requirement already met: fill the rest greedily.
    if remaining_t == 1 {
      let mut cur_tight = tight;
      for i in pos..digits.len() {
        let lo = if cur_tight { digits[i].max(1) } else { 1u8 };
        buf.push(lo);
        if cur_tight && lo != digits[i] { cur_tight = false; }
      }
      return true;
    }

    let lo = if tight { digits[pos].max(1) } else { 1u8 };
    let slots_after = digits.len() - pos - 1; // positions remaining after this one

    for d in lo..=9u8 {
      let new_rem = remaining_t / gcd64(remaining_t, d as i64);
      // Exact feasibility check: min digits to cover new_rem must fit in slots_after.
      if min_needed(new_rem, dp23) > slots_after { continue; }
      buf.push(d);
      if Self::fill(digits, pos + 1, new_rem, tight && d == digits[pos], buf, dp23) {
        return true;
      }
      buf.pop();
    }
    false
  }

  /// Non-tight fill: pick the smallest `total`-digit zero-free number whose
  /// digit product is divisible by t.  Iterative and backtrack-free:
  /// the exact `min_needed` check ensures every chosen digit leads to a
  /// valid completion, so we never need to undo a choice.
  fn fill_free(total: usize, remaining_t: i64, buf: &mut Vec<u8>, dp23: &[Vec<usize>]) -> bool {
    let mut rem = remaining_t;
    for pos in 0..total {
      if rem == 1 {
        for _ in pos..total { buf.push(1); }
        return true;
      }
      let slots_after = total - pos - 1;
      let mut placed = false;
      for d in 1u8..=9 {
        let new_rem = rem / gcd64(rem, d as i64);
        if min_needed(new_rem, dp23) <= slots_after {
          buf.push(d);
          rem = new_rem;
          placed = true;
          break;
        }
      }
      if !placed { return false; }
    }
    rem == 1
  }
}