From 879fa925c0efdd7a3d069ac2f169c1eb0a1afbec Mon Sep 17 00:00:00 2001 From: premmsharma122 Date: Tue, 19 May 2026 13:29:20 +0530 Subject: [PATCH 1/5] feat: add optimized Digit DP template and unit tests --- .../dynamicprogramming/DigitDP.java | 102 ++++++++++++++++++ .../dynamicprogramming/DigitDPTest.java | 54 ++++++++++ 2 files changed, 156 insertions(+) create mode 100644 src/main/java/com/thealgorithms/dynamicprogramming/DigitDP.java create mode 100644 src/test/java/com/thealgorithms/dynamicprogramming/DigitDPTest.java diff --git a/src/main/java/com/thealgorithms/dynamicprogramming/DigitDP.java b/src/main/java/com/thealgorithms/dynamicprogramming/DigitDP.java new file mode 100644 index 000000000000..d15d6a6a0989 --- /dev/null +++ b/src/main/java/com/thealgorithms/dynamicprogramming/DigitDP.java @@ -0,0 +1,102 @@ +package com.thealgorithms.dynamicprogramming; + +import java.util.Arrays; + +/** + * A generalized template for the Digit Dynamic Programming (Digit DP) technique. + * Digit DP is used to count numbers within a range [L, R] that satisfy specific digit properties. + * This specific implementation demonstrates counting the numbers whose digit sum equals a target value. + * + *

Example: + * countRangeWithDigitSum(1, 100, 5) returns 6 (numbers: 5, 14, 23, 32, 41, 50) + */ +public final class DigitDP { + + // Maximum theoretical digit sum for a 64-bit signed long integer (9 * 19 digits = 171) + private static final int MAX_DIGIT_SUM = 171; + + private DigitDP() { + // Prevent instantiation for utility/algorithm template class + } + + /** + * Counts how many numbers in the range [L, R] have a digit sum equal to the target. + * + * @param l The lower bound of the range (inclusive). + * @param r The upper bound of the range (inclusive). + * @param target The exact sum of digits required. + * @return The count of valid integers. + */ + public static long countRangeWithDigitSum(long l, long r, int target) { + if (l > r || target < 0 || target > MAX_DIGIT_SUM) { + return 0; + } + long countR = countWithDigitSum(r, target); + long countLMinus1 = countWithDigitSum(l - 1, target); + return countR - countLMinus1; + } + + private static long countWithDigitSum(long number, int target) { + if (number < 0) { + return 0; + } + String numStr = Long.toString(number); + int length = numStr.length(); + + // dp[index][current_sum][tight] + long[][][] dp = new long[length][MAX_DIGIT_SUM + 1][2]; + for (long[][] row : dp) { + for (long[] col : row) { + Arrays.fill(col, -1); + } + } + + return solve(0, 0, 1, numStr, target, dp); + } + + /** + * Recursive memoized function to explore digit placements. + * + * Time Complexity: O(number_of_digits * target_sum * 10) + * Space Complexity: O(number_of_digits * target_sum * 2) + * + * @param index Current digit position from left to right (most significant first). + * @param currentSum Cumulative sum of digits chosen so far. + * @param tight Flag indicating if current prefix matches the original number boundary. + * @param numStr String representation of the upper ceiling limit. + * @param target The exact required sum of digits. + * @param dp Memoization matrix cache table. + * @return Total valid combinations from the current state configuration. + */ + private static long solve(int index, int currentSum, int tight, String numStr, int target, long[][][] dp) { + // Base case: If we have processed all digits + if (index == numStr.length()) { + return currentSum == target ? 1 : 0; + } + + // Return memoized state if already evaluated + if (dp[index][currentSum][tight] != -1) { + return dp[index][currentSum][tight]; + } + + long ans = 0; + // Determine the maximum limit for the current position digit + int limit = (tight == 1) ? (numStr.charAt(index) - '0') : 9; + + // Iterate through all possible valid digits for this position + for (int digit = 0; digit <= limit; digit++) { + int nextSum = currentSum + digit; + + // Optimization: If the digit sum exceeds the target, prune branch + if (nextSum > target) { + continue; + } + + // Next state remains tight only if current state is tight and we place the exact limit digit + int nextTight = (tight == 1 && digit == limit) ? 1 : 0; + ans += solve(index + 1, nextSum, nextTight, numStr, target, dp); + } + + return dp[index][currentSum][tight] = ans; + } +} \ No newline at end of file diff --git a/src/test/java/com/thealgorithms/dynamicprogramming/DigitDPTest.java b/src/test/java/com/thealgorithms/dynamicprogramming/DigitDPTest.java new file mode 100644 index 000000000000..6cde366de51b --- /dev/null +++ b/src/test/java/com/thealgorithms/dynamicprogramming/DigitDPTest.java @@ -0,0 +1,54 @@ +package com.thealgorithms.dynamicprogramming; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import org.junit.jupiter.api.Test; + +/** + * Unit tests for the generalized DigitDP implementation. + */ +public class DigitDPTest { + + @Test + public void testDigitDPBasicRange() { + // Numbers between 1 and 20 with a digit sum of 5: 5, 14 + long result = DigitDP.countRangeWithDigitSum(1, 20, 5); + assertEquals(2, result); + } + + @Test + public void testDigitDPZeroBound() { + // Number 0 has a digit sum of 0 + long result = DigitDP.countRangeWithDigitSum(0, 0, 0); + assertEquals(1, result); + } + + @Test + public void testDigitDPLargeRange() { + // Count numbers between 1 and 100 with a digit sum of 9 + // 9, 18, 27, 36, 45, 54, 63, 72, 81, 90 (10 numbers) + long result = DigitDP.countRangeWithDigitSum(1, 100, 9); + assertEquals(10, result); + } + + @Test + public void testDigitDPNoMatches() { + // No numbers between 10 and 15 can have a digit sum of 20 + long result = DigitDP.countRangeWithDigitSum(10, 15, 20); + assertEquals(0, result); + } + + @Test + public void testDigitDPExceedsMaxSum() { + // Sum condition that exceeds max possible physical sum array constraints gracefully returns 0 + long result = DigitDP.countRangeWithDigitSum(1, 100, 200); + assertEquals(0, result); + } + + @Test + public void testDigitDPInvalidRange() { + // Lower bound greater than upper bound should evaluate gracefully to 0 + long result = DigitDP.countRangeWithDigitSum(50, 20, 5); + assertEquals(0, result); + } +} \ No newline at end of file From bd26753c44d618b4ff0aa4f4cfea466779211d99 Mon Sep 17 00:00:00 2001 From: premmsharma122 Date: Tue, 19 May 2026 19:57:29 +0530 Subject: [PATCH 2/5] test: add test cases for max target sum and memoization hit to achieve 100% coverage --- .../dynamicprogramming/DigitDPTest.java | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/src/test/java/com/thealgorithms/dynamicprogramming/DigitDPTest.java b/src/test/java/com/thealgorithms/dynamicprogramming/DigitDPTest.java index 6cde366de51b..2ee70cd0c818 100644 --- a/src/test/java/com/thealgorithms/dynamicprogramming/DigitDPTest.java +++ b/src/test/java/com/thealgorithms/dynamicprogramming/DigitDPTest.java @@ -40,7 +40,8 @@ public void testDigitDPNoMatches() { @Test public void testDigitDPExceedsMaxSum() { - // Sum condition that exceeds max possible physical sum array constraints gracefully returns 0 + // Sum condition that exceeds max possible physical sum array constraints + // gracefully returns 0 long result = DigitDP.countRangeWithDigitSum(1, 100, 200); assertEquals(0, result); } @@ -51,4 +52,20 @@ public void testDigitDPInvalidRange() { long result = DigitDP.countRangeWithDigitSum(50, 20, 5); assertEquals(0, result); } + + @Test + public void testDigitDPExceedsMaxSumEdgeCase() { + // Yeh test case target > MAX_DIGIT_SUM wali condition ko hit karega + long result = DigitDP.countRangeWithDigitSum(1, 100, 180); + assertEquals(0, result); + } + + @Test + public void testDigitDPMemoizationHit() { + // Badi range dene se overlapping subproblems bante hain, + // jisse memoization hit hogi aur coverage 100% ho jayegi. + long result1 = DigitDP.countRangeWithDigitSum(1, 100000, 15); + long result2 = DigitDP.countRangeWithDigitSum(1, 100000, 15); + assertEquals(result1, result2); + } } \ No newline at end of file From ea421ac25a041bce2357f8cdcc9fb94109af5968 Mon Sep 17 00:00:00 2001 From: premmsharma122 Date: Tue, 19 May 2026 20:09:50 +0530 Subject: [PATCH 3/5] style: fix indentation and code formatting for clang linter compliance --- .../dynamicprogramming/DigitDP.java | 27 ++++++++++++------- 1 file changed, 18 insertions(+), 9 deletions(-) diff --git a/src/main/java/com/thealgorithms/dynamicprogramming/DigitDP.java b/src/main/java/com/thealgorithms/dynamicprogramming/DigitDP.java index d15d6a6a0989..d353fa9b35ad 100644 --- a/src/main/java/com/thealgorithms/dynamicprogramming/DigitDP.java +++ b/src/main/java/com/thealgorithms/dynamicprogramming/DigitDP.java @@ -3,16 +3,21 @@ import java.util.Arrays; /** - * A generalized template for the Digit Dynamic Programming (Digit DP) technique. - * Digit DP is used to count numbers within a range [L, R] that satisfy specific digit properties. - * This specific implementation demonstrates counting the numbers whose digit sum equals a target value. + * A generalized template for the Digit Dynamic Programming (Digit DP) + * technique. + * Digit DP is used to count numbers within a range [L, R] that satisfy specific + * digit properties. + * This specific implementation demonstrates counting the numbers whose digit + * sum equals a target value. * - *

Example: + *

+ * Example: * countRangeWithDigitSum(1, 100, 5) returns 6 (numbers: 5, 14, 23, 32, 41, 50) */ public final class DigitDP { - // Maximum theoretical digit sum for a 64-bit signed long integer (9 * 19 digits = 171) + // Maximum theoretical digit sum for a 64-bit signed long integer (9 * 19 digits + // = 171) private static final int MAX_DIGIT_SUM = 171; private DigitDP() { @@ -20,7 +25,8 @@ private DigitDP() { } /** - * Counts how many numbers in the range [L, R] have a digit sum equal to the target. + * Counts how many numbers in the range [L, R] have a digit sum equal to the + * target. * * @param l The lower bound of the range (inclusive). * @param r The upper bound of the range (inclusive). @@ -60,9 +66,11 @@ private static long countWithDigitSum(long number, int target) { * Time Complexity: O(number_of_digits * target_sum * 10) * Space Complexity: O(number_of_digits * target_sum * 2) * - * @param index Current digit position from left to right (most significant first). + * @param index Current digit position from left to right (most significant + * first). * @param currentSum Cumulative sum of digits chosen so far. - * @param tight Flag indicating if current prefix matches the original number boundary. + * @param tight Flag indicating if current prefix matches the original + * number boundary. * @param numStr String representation of the upper ceiling limit. * @param target The exact required sum of digits. * @param dp Memoization matrix cache table. @@ -92,7 +100,8 @@ private static long solve(int index, int currentSum, int tight, String numStr, i continue; } - // Next state remains tight only if current state is tight and we place the exact limit digit + // Next state remains tight only if current state is tight and we place the + // exact limit digit int nextTight = (tight == 1 && digit == limit) ? 1 : 0; ans += solve(index + 1, nextSum, nextTight, numStr, target, dp); } From ed841503d0f0019b7368ffe847ec071aacdc6ef3 Mon Sep 17 00:00:00 2001 From: premmsharma122 Date: Tue, 19 May 2026 20:18:36 +0530 Subject: [PATCH 4/5] fix: remove checkstyle inner assignment in solve method --- .../java/com/thealgorithms/dynamicprogramming/DigitDP.java | 4 ++-- .../com/thealgorithms/dynamicprogramming/DigitDPTest.java | 1 - 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/src/main/java/com/thealgorithms/dynamicprogramming/DigitDP.java b/src/main/java/com/thealgorithms/dynamicprogramming/DigitDP.java index d353fa9b35ad..02055336ae6f 100644 --- a/src/main/java/com/thealgorithms/dynamicprogramming/DigitDP.java +++ b/src/main/java/com/thealgorithms/dynamicprogramming/DigitDP.java @@ -1,5 +1,4 @@ package com.thealgorithms.dynamicprogramming; - import java.util.Arrays; /** @@ -106,6 +105,7 @@ private static long solve(int index, int currentSum, int tight, String numStr, i ans += solve(index + 1, nextSum, nextTight, numStr, target, dp); } - return dp[index][currentSum][tight] = ans; + dp[index][currentSum][tight] = ans; + return ans; } } \ No newline at end of file diff --git a/src/test/java/com/thealgorithms/dynamicprogramming/DigitDPTest.java b/src/test/java/com/thealgorithms/dynamicprogramming/DigitDPTest.java index 2ee70cd0c818..e55a5dfaad6d 100644 --- a/src/test/java/com/thealgorithms/dynamicprogramming/DigitDPTest.java +++ b/src/test/java/com/thealgorithms/dynamicprogramming/DigitDPTest.java @@ -1,5 +1,4 @@ package com.thealgorithms.dynamicprogramming; - import static org.junit.jupiter.api.Assertions.assertEquals; import org.junit.jupiter.api.Test; From abc31a4385e524d40e159db2e21109c1890e364c Mon Sep 17 00:00:00 2001 From: premmsharma122 Date: Tue, 19 May 2026 20:24:12 +0530 Subject: [PATCH 5/5] fix: remove checkstyle inner assignment in solve method -newline at end --- src/main/java/com/thealgorithms/dynamicprogramming/DigitDP.java | 2 +- .../java/com/thealgorithms/dynamicprogramming/DigitDPTest.java | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/com/thealgorithms/dynamicprogramming/DigitDP.java b/src/main/java/com/thealgorithms/dynamicprogramming/DigitDP.java index 02055336ae6f..7dae7603fedc 100644 --- a/src/main/java/com/thealgorithms/dynamicprogramming/DigitDP.java +++ b/src/main/java/com/thealgorithms/dynamicprogramming/DigitDP.java @@ -108,4 +108,4 @@ private static long solve(int index, int currentSum, int tight, String numStr, i dp[index][currentSum][tight] = ans; return ans; } -} \ No newline at end of file +} diff --git a/src/test/java/com/thealgorithms/dynamicprogramming/DigitDPTest.java b/src/test/java/com/thealgorithms/dynamicprogramming/DigitDPTest.java index e55a5dfaad6d..762fe86d4d65 100644 --- a/src/test/java/com/thealgorithms/dynamicprogramming/DigitDPTest.java +++ b/src/test/java/com/thealgorithms/dynamicprogramming/DigitDPTest.java @@ -67,4 +67,4 @@ public void testDigitDPMemoizationHit() { long result2 = DigitDP.countRangeWithDigitSum(1, 100000, 15); assertEquals(result1, result2); } -} \ No newline at end of file +}