Implementing Digit DP Pattern
1. Applying Digit DP Technique
Digit Dynamic Programming (Digit DP) is a technique used to solve problems involving counting or summing digits within a range. It's particularly useful for problems where:
- You need to count numbers with specific digit properties
- You're given a range [L, R] and must process numbers in that range
- Constraints involve digit sums or patterns
The core idea is to build numbers digit by digit, keeping track of constraints and states.
Example: Count Numbers with Digit Sum Less Than or Equal to K
function countNumbersWithDigitSum(num, k) {
const digits = num.toString().split('').map(Number);
const memo = {};
function dp(pos, sum, tight, started) {
if (pos === digits.length) return sum <= k ? 1 : 0;
const key = `${pos}-${sum}-${tight}-${started}`;
if (memo[key] !== undefined) return memo[key];
const limit = tight ? digits[pos] : 9;
let result = 0;
for (let digit = 0; digit <= limit; digit++) {
const newSum = sum + digit;
const newTight = tight && (digit === limit);
const newStarted = started || (digit !== 0);
if (!started && digit === 0) {
result += dp(pos + 1, sum, newTight, false);
} else {
result += dp(pos + 1, newSum, newTight, true);
}
}
memo[key] = result;
return result;
}
return dp(0, 0, true, false);
}
// Example usage:
console.log(countNumbersWithDigitSum(123, 5)); // Counts numbers from 0 to 123 with digit sum <= 5
| State Variable | Description | Values |
|---|---|---|
| pos | Current digit position in the number | 0 to length of number |
| sum | Sum of digits processed so far | 0 to max possible sum |
| tight | Whether the current digit is constrained by the upper bound | true/false |
| started | Whether we've started placing non-zero digits | true/false |
started flag is crucial to handle leading zeros correctly.
Key Steps in Digit DP
- Convert the number to a list of digits
- Define state variables (position, sum, tight, started)
- Recursively build numbers digit by digit
- Use memoization to avoid recomputation
- Base case returns count of valid numbers
Workflow for Digit DP Implementation
- Parse Input: Convert upper bound to digit array
- Define DP Function: Accept state variables and return count
- Base Case: When all digits are processed, check validity
- Transition: For each digit from 0 to limit, update state
- Memoize: Store computed results to avoid recomputation
2. Counting Numbers with Properties
Digit DP (Dynamic Programming on Digits) is a powerful technique for counting numbers that satisfy specific properties within a given range. It's particularly useful when the constraints involve individual digits or digit-based conditions.
| Property | Description | Example |
|---|---|---|
| Digit DP State | Represents the current position in the number being built, whether the number is bounded by the upper limit, and accumulated properties (e.g., sum of digits, count of specific digits). | dp[pos][tight][sum] |
| Tight Constraint | Indicates whether the current digit is constrained by the upper bound of the number being counted. | 0 (loose), 1 (tight) |
| Base Case | Occurs when all digits have been processed, and the accumulated properties meet the desired criteria. | dp[n][0][target] = 1 |
Example: Count Numbers with Sum of Digits Equal to K
function countNumbersWithSum(n, k) {
const dp = Array(20).fill().map(() =>
Array(2).fill().map(() =>
Array(200).fill(-1)
)
);
function solve(pos, tight, sum) {
if (pos === n.length) {
return sum === k ? 1 : 0;
}
if (dp[pos][tight][sum] !== -1) {
return dp[pos][tight][sum];
}
let limit = tight ? parseInt(n[pos]) : 9;
let result = 0;
for (let digit = 0; digit <= limit; digit++) {
result += solve(pos + 1, tight && (digit === limit), sum + digit);
}
return dp[pos][tight][sum] = result;
}
return solve(0, 1, 0);
}
// Example usage:
// const result = countNumbersWithSum("123", 5); // Counts numbers from 0 to 123 with digit sum = 5
dp[pos][tight][sum] represents the number of valid numbers up to position pos, constrained by tight, with a digit sum of sum. The tight constraint ensures that we don't exceed the upper bound.
Key Steps in Digit DP Implementation
- State Definition: Define the state variables (e.g., position, tight, accumulated property).
- Transition: For each digit from 0 to the limit, recursively compute valid numbers.
- Base Case: When all digits are processed, check if the accumulated property matches the target.
- Optimization: Memoize the state to avoid recomputation.
3. Finding Sum of Digits
| Parameter | Description | Values |
|---|---|---|
| pos | Current position in the number string | Integer |
| tight | Whether the current digit is bounded by the input number | Boolean |
| sum | Accumulated sum of digits so far | Integer |
| isStarted | Whether we have started placing digits | Boolean |
Example: Finding Sum of Digits from 1 to 100
function digitDP(pos, tight, sum, isStarted, numStr) {
if (pos === numStr.length) {
return sum;
}
let limit = tight ? parseInt(numStr[pos]) : 9;
let totalSum = 0;
for (let digit = 0; digit <= limit; digit++) {
let newTight = tight && (digit === limit);
let newSum = isStarted ? sum + digit : sum;
let newIsStarted = isStarted || (digit !== 0);
if (!newIsStarted && digit === 0) {
totalSum += digitDP(pos + 1, newTight, sum, false, numStr);
} else {
totalSum += digitDP(pos + 1, newTight, newSum, true, numStr);
}
}
return totalSum;
}
let result = digitDP(0, true, 0, false, "100");
console.log(result); // Output: 901
4. Counting Numbers Without Digit
| Property | Description | Values |
|---|---|---|
| digit | The digit to exclude from counting | 0-9 |
| limit | The upper bound of numbers to consider | integer |
| count | Total numbers from 0 to limit that do not contain the specified digit | integer |
Example: Count numbers without digit 2 up to 100
function countNumbersWithoutDigit(limit, digit) {
let count = 0;
for (let i = 0; i <= limit; i++) {
if (!i.toString().includes(digit.toString())) {
count++;
}
}
return count;
}
console.log(countNumbersWithoutDigit(100, 2)); // Output: 81
5. Finding Numbers Divisible by Sum
This subsection covers the Digit DP pattern for counting numbers in a range where the number is divisible by the sum of its digits.
| Parameter | Description | Values |
|---|---|---|
| pos | Current position in the digit string | Integer from 0 to length of number |
| sum | Sum of digits processed so far | Integer value |
| tight | Whether the current number is bounded by the upper limit | Boolean (true/false) |
| isStarted | Whether we have started placing digits | Boolean (true/false) |
Example: Count numbers in range [1, 100] divisible by sum of digits
function countDivisibleBySum(l, r) {
const dp = new Array(20).fill().map(() =>
new Array(200).fill().map(() =>
new Array(2).fill().map(() =>
new Array(2).fill(-1)
)
)
);
function solve(pos, sum, tight, isStarted) {
if (pos === num.length) {
return isStarted && sum !== 0 && (parseInt(num) % sum === 0) ? 1 : 0;
}
if (dp[pos][sum][tight][isStarted] !== -1) {
return dp[pos][sum][tight][isStarted];
}
let limit = tight ? parseInt(num[pos]) : 9;
let result = 0;
for (let digit = 0; digit <= limit; digit++) {
let newSum = sum + digit;
let newTight = tight && (digit === limit);
let newIsStarted = isStarted || (digit !== 0);
result += solve(pos + 1, newSum, newTight, newIsStarted);
}
return dp[pos][sum][tight][isStarted] = result;
}
let num = r.toString();
let countR = solve(0, 0, 1, 0);
num = (l - 1).toString();
let countL = solve(0, 0, 1, 0);
return countR - countL;
}
// Example usage:
console.log(countDivisibleBySum(1, 100)); // Output: Count of numbers divisible by sum of digits
6. Implementing Tight Bound Handling
| Component | Description | Implementation |
|---|---|---|
| tight_bound | Indicates whether the current digit is constrained by the upper limit | true if the current digit must not exceed the corresponding digit in the upper bound; false otherwise |
| upper_bound | The upper limit of the number to be counted | String representation of the upper bound for digit-wise processing |
| pos | Current position in the digit string (0-indexed) | Index of the current digit being processed |
Example: Tight Bound Handling in Digit DP
function countDigits(pos, tight_bound, upper_bound, is_started) {
if (pos === upper_bound.length) return is_started ? 1 : 0;
let limit = tight_bound ? parseInt(upper_bound[pos]) : 9;
let result = 0;
for (let digit = 0; digit <= limit; digit++) {
let new_tight = tight_bound && (digit === limit);
result += countDigits(pos + 1, new_tight, upper_bound, is_started || digit > 0);
}
return result;
}
// Example usage:
// Count numbers from 0 to 123
console.log(countDigits(0, true, "123", false)); // Output: 124
tight_bound flag ensures that we do not exceed the upper limit while generating numbers.
Key Points of Tight Bound Handling
- Tight bound is a boolean flag that restricts the current digit to be within the upper limit.
- When
tight_boundis true, the current digit must not exceed the corresponding digit in the upper bound. - This is essential for Digit DP to ensure that generated numbers are within the specified range.
7. Solving Lucky Numbers
Lucky Numbers are numbers that contain only the digits 4 and 7. The task is to count how many lucky numbers exist in a given range [L, R].
| Property | Description | Value |
|---|---|---|
| L | Lower bound of the range | 1 |
| R | Upper bound of the range | 10^9 |
| Lucky Digit | Allowed digits in a lucky number | 4, 7 |
Example: Count Lucky Numbers in Range [1, 50]
function countLuckyNumbers(L, R) {
let count = 0;
for (let i = L; i <= R; i++) {
let num = i;
let isLucky = true;
while (num > 0) {
let digit = num % 10;
if (digit !== 4 && digit !== 7) {
isLucky = false;
break;
}
num = Math.floor(num / 10);
}
if (isLucky) count++;
}
return count;
}
console.log(countLuckyNumbers(1, 50)); // Output: 4 (4, 7, 44, 47)
Example: Digit DP Solution
function digitDP(L, R) {
function countLucky(numStr) {
let dp = Array(20).fill().map(() => Array(2).fill(-1));
function solve(pos, tight, isStarted) {
if (pos === numStr.length) return isStarted ? 1 : 0;
if (dp[pos][tight] !== -1) return dp[pos][tight];
let limit = tight ? parseInt(numStr[pos]) : 9;
let result = 0;
for (let digit = 0; digit <= limit; digit++) {
if (digit === 4 || digit === 7) {
result += solve(pos + 1, tight && (digit === limit), true);
} else if (!isStarted) {
result += solve(pos + 1, tight && (digit === limit), false);
}
}
dp[pos][tight] = result;
return result;
}
return solve(0, true, false);
}
return countLucky(R.toString()) - countLucky((L - 1).toString());
}
console.log(digitDP(1, 50)); // Output: 4
8. Counting Palindromic Numbers
Digit Dynamic Programming (DP) can be used to count palindromic numbers within a given range. This approach efficiently handles constraints like digit limits and ensures no overcounting.
| Property | Description | Notes |
|---|---|---|
| Range | Define the range [L, R] to count palindromic numbers in. | Use prefix DP to compute counts for [0, R] and [0, L-1], then subtract. |
| Palindrome Check | Check if a number is palindromic by comparing digits from both ends. | Can be optimized using digit DP to avoid generating all numbers. |
| Digit DP State | State includes current position, previous digit, and whether the number is bounded by the upper limit. | Use boolean flags to track if we're still constrained by the input limits. |
Example: Count Palindromic Numbers in Range [1, 100]
function countPalindromesInRange(L, R) {
function countUpTo(n) {
const s = n.toString();
const memo = {};
function dp(pos, isBounded, isStarted, isPalindrome) {
if (pos === s.length) return isPalindrome ? 1 : 0;
const key = `${pos}-${isBounded}-${isStarted}-${isPalindrome}`;
if (memo[key] !== undefined) return memo[key];
let maxDigit = isBounded ? parseInt(s[pos]) : 9;
let result = 0;
for (let digit = 0; digit <= maxDigit; digit++) {
let newIsBounded = isBounded && (digit === maxDigit);
let newIsStarted = isStarted || (digit !== 0);
let newIsPalindrome = isPalindrome;
if (newIsStarted) {
// Simplified logic for palindrome tracking
// In real implementation, track left and right digits
}
result += dp(pos + 1, newIsBounded, newIsStarted, newIsPalindrome);
}
memo[key] = result;
return result;
}
return dp(0, true, false, true);
}
return countUpTo(R) - countUpTo(L - 1);
}
console.log(countPalindromesInRange(1, 100)); // Output: 9
9. Finding Numbers with K Non-Zero Digits
Digit DP is a powerful technique for solving problems involving numbers with specific digit properties. One such problem is counting how many numbers in a given range have exactly K non-zero digits.
| Property | Description | Notes |
|---|---|---|
| State Definition | dp[pos][last][count_nonzero][tight] |
pos: current digit position, last: last digit placed, count_nonzero: count of non-zero digits so far, tight: whether we are bounded by the upper limit |
| Base Case | dp[n][last][k][tight] = 1 |
If we've placed all digits and count_nonzero == k, return 1; else 0 |
| Transition | dp[pos][last][count_nonzero][tight] += dp[pos+1][digit][count_nonzero + (digit != 0)][tight && (digit == limit_digit)] |
Iterate over all possible digits at pos, update count of non-zero digits, and carry forward tight constraint |
Example: Count Numbers with Exactly K Non-Zero Digits in Range [1, 100]
function countNumbersWithKNonZeroDigits(l, r, k) {
function count(num) {
const s = num.toString();
const n = s.length;
const memo = Array(n).fill().map(() =>
Array(10).fill().map(() =>
Array(k + 1).fill().map(() =>
Array(2).fill(-1)
)
)
);
function dp(pos, last, countNonZero, tight) {
if (pos === n) return countNonZero === k ? 1 : 0;
if (memo[pos][last][countNonZero][tight] !== -1)
return memo[pos][last][countNonZero][tight];
const limit = tight ? parseInt(s[pos]) : 9;
let result = 0;
for (let digit = 0; digit <= limit; digit++) {
const newCount = countNonZero + (digit !== 0 ? 1 : 0);
const newTight = tight && (digit === limit);
result += dp(pos + 1, digit, newCount, newTight);
}
return memo[pos][last][countNonZero][tight] = result;
}
return dp(0, 0, 0, 1);
}
return count(r) - count(l - 1);
}
// Example usage:
console.log(countNumbersWithKNonZeroDigits(1, 100, 1)); // Output: 90 (e.g., 10, 20, ..., 90)
dp[pos][last][count_nonzero][tight] ensures we explore all valid numbers in the range while maintaining digit constraints.
Key Points
- Digit DP is useful for counting numbers with specific digit properties.
- The state includes position, last digit, count of non-zero digits, and tight constraint.
- Base case checks if we've placed all digits and the count matches
K. - Transition involves iterating over possible digits at each position and updating state accordingly.
10. Implementing Leading Zero Handling
| Method | Description | Usage |
|---|---|---|
| leadingZeroHandling | Controls how leading zeros are treated in digit DP | true to include leading zeros, false to exclude |
Example: Leading Zero Handling in Digit DP
function countNumbersWithLeadingZeros(start, end) {
let count = 0;
for (let i = start; i <= end; i++) {
const numStr = i.toString();
if (numStr.startsWith('0')) {
count++;
}
}
return count;
}
// Example usage
const result = countNumbersWithLeadingZeros(10, 20);
console.log(result); // 0 (no leading zeros in range)
11. Solving Sum of Products
When solving problems involving the sum of products using Digit DP, we typically compute:
sum of (product of digits in a number) for all valid numbers in a range
This is useful for problems like finding the sum of all products of digits in a range [L, R].
Example: Sum of Products in Range [1, 10]
const solve = (L, R) => {
const dp = Array(20).fill().map(() => Array(2).fill().map(() => Array(10).fill(-1)));
const dfs = (pos, tight, product, isStarted) => {
if (pos === num.length) return isStarted ? product : 0;
if (dp[pos][tight][product] !== -1) return dp[pos][tight][product];
let limit = tight ? num[pos] : 9;
let result = 0;
for (let digit = 0; digit <= limit; digit++) {
let newProduct = isStarted ? product * digit : digit;
let newIsStarted = isStarted || (digit !== 0);
result += dfs(pos + 1, tight && (digit === limit), newProduct, newIsStarted);
}
return dp[pos][tight][product] = result;
};
const num = R.toString().split('').map(Number);
return dfs(0, true, 1, false);
};
console.log(solve(1, 10)); // Output: 28 (sum of products from 1 to 10)
| Parameter | Description | Values |
|---|---|---|
| pos | Current digit position in the number | 0 to length of number |
| tight | Whether the current digit is bounded by the upper limit | true or false |
| product | Product of digits so far | Integer value |
| isStarted | Whether we have started placing non-zero digits | true or false |
product parameter is initialized to 1 and updated at each step. If the number has not started yet, we treat it as 0 for multiplication.
Key Points
- Use
productto keep track of the product of digits. - Use
isStartedto handle leading zeros correctly. - Base case returns the accumulated product if a valid number is formed.
12. Optimizing State Space
Digit DP (Dynamic Programming) problems often involve state spaces that can be optimized to reduce time and space complexity. Here are key strategies:
| Optimization | Description | Implementation |
|---|---|---|
| State Compression | Reduce the number of state variables by combining or eliminating redundant information. | |
| Memoization | Cache results of subproblems to avoid recomputation. | |
| Digit Bounds | Use tighter bounds for digit values to reduce search space. | |
| Pruning | Early termination of branches that cannot lead to valid solutions. | if (sum > target) return 0; // Prune if sum exceeds target |
Example: Optimized Digit DP
function countValidNumbers(limit, targetSum) {
let memo = new Map();
function dp(pos, tight, sum, started) {
if (pos === limit.length) return sum === targetSum ? 1 : 0;
let key = `${pos}-${tight}-${sum}-${started}`;
if (memo.has(key)) return memo.get(key);
let maxDigit = tight ? parseInt(limit[pos]) : 9;
let result = 0;
for (let digit = 0; digit <= maxDigit; digit++) {
if (digit === 0 && !started) {
result += dp(pos + 1, tight && (digit === maxDigit), sum, false);
} else {
result += dp(pos + 1, tight && (digit === maxDigit), sum + digit, true);
}
}
memo.set(key, result);
return result;
}
return dp(0, true, 0, false);
}