Count Substrings Divisible By Last Digit
HardPrompt
You are given a string s consisting of digits.
Return the number of substrings of s divisible by their non-zero last digit.
Note: A substring may contain leading zeros.
Example 1:
Input: s = "12936"
Output: 11
Explanation:
Substrings "29", "129", "293" and "2936" are not divisible by their last digit. There are 15 substrings in total, so the answer is 15 - 4 = 11.
Example 2:
Input: s = "5701283"
Output: 18
Explanation:
Substrings "01", "12", "701", "012", "128", "5701", "7012", "0128", "57012", "70128", "570128", and "701283" are all divisible by their last digit. Additionally, all substrings that are just 1 non-zero digit are divisible by themselves. Since there are 6 such digits, the answer is 12 + 6 = 18.
Example 3:
Input: s = "1010101010"
Output: 25
Explanation:
Only substrings that end with digit '1' are divisible by their last digit. There are 25 such substrings.
Constraints:
1 <= s.length <= 105sconsists of digits only.
Approaches
2 approaches with complexity analysis and trade-offs.
This approach iterates through all possible substrings of the given string s. For each substring, it checks if it's divisible by its last digit. To handle potentially very large numbers represented by substrings, it uses modular arithmetic to calculate the remainder efficiently, thus avoiding overflow issues with standard integer types.
Algorithm
- Initialize a counter
countto 0. - Iterate through the string with an index
jfrom0ton-1, wherejrepresents the ending position of a substring. - For each
j, get the last digitd = s.charAt(j) - '0'. Ifdis 0, continue to the nextjsince division by zero is not allowed. - For the fixed ending position
jand last digitd, iterate with an indexifromjdown to0. Thisirepresents the starting position of the substring. - For each substring
s[i..j], we need to calculate the number it represents modulod. To do this without causing an overflow with large numbers, we compute it iteratively. - Inside the inner loop (for
i), we build the number from right to left. We maintain thecurrentNumRem(remainder of the number formed bys[i..j]) and the currentpowerOf10, both modulod. - In each step of the inner loop, we update
currentNumRemby adding the contribution of the current digits[i]. The formula iscurrentNumRem = (currentNumRem + (s.charAt(i) - '0') * powerOf10) % d. - If
currentNumRembecomes 0, it means the number represented bys[i..j]is divisible byd, so we incrementcount. - We also update
powerOf10for the next digit to the left:powerOf10 = (powerOf10 * 10) % d. - After all loops complete,
countwill hold the total number of such substrings.
Walkthrough
The algorithm uses a nested loop structure to generate all substrings. The outer loop fixes the ending character of the substring, and the inner loop iterates backwards to define the starting character.
For a substring ending at index j, the last digit is d = s.charAt(j) - '0'. We are interested in counting how many start indices i <= j result in the number represented by s[i..j] being divisible by d.
Instead of converting the substring to a number directly (which can be very large), we calculate its value modulo d on the fly. As we iterate from i = j down to 0, we build the number from right to left, keeping track of the running remainder and the corresponding power of 10. If the final remainder for a substring s[i..j] is 0, we've found a valid substring and increment our counter.
class Solution { public int countSubstrings(String s) { int n = s.length(); long count = 0; for (int j = 0; j < n; j++) { int lastDigit = s.charAt(j) - '0'; if (lastDigit == 0) { continue; } long currentNumRem = 0; long powerOf10 = 1; // Iterate through all substrings ending at j for (int i = j; i >= 0; i--) { int digit = s.charAt(i) - '0'; // Calculate the number represented by s[i..j] modulo lastDigit // We build the number from right to left (from s[j] to s[i]) currentNumRem = (currentNumRem + (long)digit * powerOf10) % lastDigit; if (currentNumRem == 0) { count++; } powerOf10 = (powerOf10 * 10) % lastDigit; } } return (int)count; }}Complexity
Time
O(n^2) - There are two nested loops. The outer loop runs `n` times (for the end position `j`), and the inner loop runs `j+1` times (for the start position `i`). This results in a quadratic time complexity.
Space
O(1) - We only use a few variables to store the count and intermediate values for the modular arithmetic calculation, regardless of the input size.
Trade-offs
Pros
Relatively simple to understand and implement.
Correctly handles arbitrarily large numbers by using modular arithmetic, avoiding overflow.
Requires minimal extra space.
Cons
The time complexity of O(n^2) is too slow for the given constraints (n up to 10^5), and this solution will likely result in a 'Time Limit Exceeded' error on platforms like LeetCode.
A more efficient solution uses dynamic programming. We can process the string from left to right, and at each position i, we calculate the number of valid substrings that end at i. To do this efficiently, we maintain DP states that keep track of remainder counts for all possible divisors (1 through 9).
Algorithm
- Initialize
totalCount = 0. - Initialize a 2D array,
dp[10][10], wheredp[d][rem]will store the count of substrings ending at the current positionithat have a remainder ofremwhen divided byd. - Iterate through the string
sfromi = 0ton-1. - In each iteration, get the current
digit = s.charAt(i) - '0'. - Create a temporary
newDptable for the current positioni. This is because the calculation for positionidepends on the results fromi-1. - For each possible divisor
dfrom 1 to 9: a. Transition the counts from the previous state (dptable for positioni-1) to the current state (newDptable for positioni). For each previous remainderrfrom0tod-1, thedp[d][r]substrings are extended with the currentdigit. Their new remainder will be(r * 10 + digit) % d. We adddp[d][r]tonewDp[d][newRem]. b. Account for the new single-digit substrings[i]. IncrementnewDp[d][digit % d]by 1. - After populating
newDpfor all divisors, update the maindptable:dp = newDp. - If the current
digitis not zero, it is the last digit of all substrings ending ati. The number of these substrings that are divisible bydigitis exactly the count we just computed for remainder 0, i.e.,dp[digit][0]. Add this value tototalCount. - After the loop finishes, return
totalCount.
Walkthrough
The core idea is to build up the solution by iterating through the string once. For each character s[i], we want to know how many substrings ending at i are divisible by the digit s[i]. A substring ending at i, say s[k..i], is formed by appending s[i] to a substring ending at i-1 (i.e., s[k..i-1]), or it's the single-digit substring s[i].
The remainder of the new number N(k,i) modulo some divisor d can be calculated from the remainder of N(k,i-1): N(k,i) % d = ( (N(k,i-1) % d) * 10 + (s[i]-'0') ) % d.
This suggests a DP approach. We maintain a table dp[d][rem] which stores the count of substrings ending at the previous position i-1 having a remainder rem when divided by d. We do this for all possible non-zero last digits d from 1 to 9.
When we are at position i, we compute a new DP table based on the previous one. For each divisor d, we transition the counts based on the formula above. We also add 1 for the new single-digit substring s[i]. After computing the DP table for the current position i, the number of valid substrings ending at i is simply the count of substrings with remainder 0 when divided by s[i] - '0'. We add this to our total count.
class Solution { public int countSubstrings(String s) { int n = s.length(); long totalCount = 0; // dp[d][rem] = count of substrings ending at the *previous* position // with remainder rem when divided by d. long[][] dp = new long[10][10]; for (int i = 0; i < n; i++) { int digit = s.charAt(i) - '0'; long[][] newDp = new long[10][10]; // Update DP states for all possible divisors for (int d = 1; d <= 9; d++) { // Case 1: Substrings formed by appending the current digit // to substrings ending at the previous position. for (int rem = 0; rem < d; rem++) { int newRem = (rem * 10 + digit) % d; newDp[d][newRem] += dp[d][rem]; } // Case 2: The new single-digit substring s[i]. newDp[d][digit % d]++; } dp = newDp; if (digit != 0) { // Add the count of substrings ending at i that are divisible by `digit`. // This is the count of substrings with remainder 0 for divisor `digit`. totalCount += dp[digit][0]; } } // The problem's return type is int, so we cast. return (int) totalCount; }}Complexity
Time
O(n) - The algorithm iterates through the string once. The work inside the loop is constant because the nested loops for divisors `d` and remainders `rem` run a fixed number of times (at most `1+2+...+9 = 45` inner operations per `d`-loop).
Space
O(1) - The DP table `dp` has a fixed size of `10x10`, which is constant and does not depend on the length of the input string `n`.
Trade-offs
Pros
Highly efficient with a linear time complexity, making it suitable for large inputs.
Systematically handles all divisors without needing complex, separate logic for each divisibility rule.
Uses constant extra space, as the DP table size is fixed.
Cons
The logic is more abstract and can be harder to understand initially compared to a direct brute-force approach.
Video walkthrough
Newsletter
One sharp idea, every week
System design and interview prep — short enough to finish.
No spam. Unsubscribe anytime.
Practice
Same difficulty — related problems to reinforce the pattern.