Concatenated Divisibility
HardPrompt
You are given an array of positive integers nums and a positive integer k.
A permutation of nums is said to form a divisible concatenation if, when you concatenate the decimal representations of the numbers in the order specified by the permutation, the resulting number is divisible by k.
Return the lexicographically smallest permutation (when considered as a list of integers) that forms a divisible concatenation. If no such permutation exists, return an empty list.
Example 1:
Input: nums = [3,12,45], k = 5
Output: [3,12,45]
Explanation:
| Permutation | Concatenated Value | Divisible by 5 |
|---|---|---|
| [3, 12, 45] | 31245 | Yes |
| [3, 45, 12] | 34512 | No |
| [12, 3, 45] | 12345 | Yes |
| [12, 45, 3] | 12453 | No |
| [45, 3, 12] | 45312 | No |
| [45, 12, 3] | 45123 | No |
The lexicographically smallest permutation that forms a divisible concatenation is [3,12,45].
Example 2:
Input: nums = [10,5], k = 10
Output: [5,10]
Explanation:
| Permutation | Concatenated Value | Divisible by 10 |
|---|---|---|
| [5, 10] | 510 | Yes |
| [10, 5] | 105 | No |
The lexicographically smallest permutation that forms a divisible concatenation is [5,10].
Example 3:
Input: nums = [1,2,3], k = 5
Output: []
Explanation:
Since no permutation of nums forms a valid divisible concatenation, return an empty list.
Constraints:
1 <= nums.length <= 131 <= nums[i] <= 1051 <= k <= 100
Approaches
2 approaches with complexity analysis and trade-offs.
Given the small constraint on N (up to 13), this problem can be solved efficiently using dynamic programming with a bitmask. This approach avoids the factorial time complexity of backtracking by storing and reusing the results of subproblems. The state of our DP captures the subset of numbers used (via a bitmask) and the remainder of the concatenated number formed by a permutation of this subset.
Algorithm
- Sort the
numsarray to ensure that when we build the permutation, we can prioritize smaller numbers to find the lexicographically smallest result. - Use a top-down dynamic programming approach with memoization. The state of our DP will be
(mask, rem), wheremaskis a bitmask representing the subset ofnumsused, andremis the current remainder modulok. - Create a memoization table
memo[1 << N][k]to store the results of subproblems (e.g., 1 for possible, -1 for impossible). - Create a
parent[1 << N][k]table to store the index of the number chosen at each state to allow for path reconstruction. - Define a recursive function
solve(mask, rem)that returns whether a valid permutation can be formed from the unused numbers. - The base case for
solve(mask, rem)is when all numbers are used (maskis all 1s). It returns success ifrem == 0, and failure otherwise. - In the recursive step, iterate through all numbers
nums[i]. Ifnums[i]is not in the currentmask, recursively callsolvefor the new state(mask | (1 << i), new_rem). If the recursive call is successful, it means we found a valid path. We store the chosen indexiinparent[mask][rem]and return success. - After the initial call
solve(0, 0)completes, if a solution exists, reconstruct the permutation by backtracking through theparenttable from the initial state.
Walkthrough
The core idea is to solve for subproblems and store their results to avoid re-computation. A subproblem can be defined as: "Is it possible to form a valid divisible concatenation using a specific subset of the remaining numbers, given the prefix has already produced a certain remainder?"
We define a function solve(mask, rem) which determines if a valid suffix can be formed from the numbers not yet included in mask, assuming the prefix has a remainder rem. To find the lexicographically smallest permutation, we first sort nums. Then, inside solve, we iterate through available numbers nums[i] in increasing order. The first one that leads to a valid complete permutation will guarantee that we are on the path to the lexicographically smallest solution.
We use two tables:
memo[mask][rem]: Stores whether the state(mask, rem)can lead to a solution (1 for possible, -1 for impossible, 0 for not computed).parent[mask][rem]: Stores the indexiof the numbernums[i]that should be chosen next from state(mask, rem)to be on the optimal path.
After the solve function populates these tables, we can reconstruct the final permutation by starting from the initial state (mask=0, rem=0) and following the indices stored in the parent table.
class Solution { int[] nums; int n; int k; long[] p10; int[][] memo; // 0: not computed, 1: possible, -1: impossible int[][] parent; // stores the index of the number chosen public int[] concatenatedDivisibility(int[] nums, int k) { this.n = nums.length; this.nums = nums; this.k = k; Arrays.sort(this.nums); this.p10 = new long[n]; for (int i = 0; i < n; i++) { p10[i] = (long) Math.pow(10, String.valueOf(nums[i]).length()); } this.memo = new int[1 << n][k]; this.parent = new int[1 << n][k]; if (solve(0, 0) == -1) { return new int[0]; } // Reconstruct path int[] result = new int[n]; int currentMask = 0; int currentRem = 0; for (int i = 0; i < n; i++) { int index = parent[currentMask][currentRem]; result[i] = this.nums[index]; currentRem = (int) ((currentRem * p10[index] + this.nums[index]) % k); currentMask |= (1 << index); } return result; } private int solve(int mask, int rem) { if (mask == (1 << n) - 1) { return rem == 0 ? 1 : -1; } if (memo[mask][rem] != 0) { return memo[mask][rem]; } for (int i = 0; i < n; i++) { if ((mask & (1 << i)) == 0) { // If nums[i] is not used // Handle duplicates if (i > 0 && nums[i] == nums[i-1] && (mask & (1 << (i - 1))) == 0) { continue; } int newRem = (int) ((rem * p10[i] + nums[i]) % k); if (solve(mask | (1 << i), newRem) == 1) { parent[mask][rem] = i; return memo[mask][rem] = 1; } } } return memo[mask][rem] = -1; }}Complexity
Time
O(2^N * k * N + N log N). The DP state space is `2^N * k`. For each state, we iterate up to `N` times to decide the next number. Sorting the array takes an initial `O(N log N)`.
Space
O(2^N * k), where N is the length of `nums` and k is the divisor. This space is dominated by the memoization and parent tables.
Trade-offs
Pros
Efficient enough to pass the given constraints.
Guaranteed to find the optimal (lexicographically smallest) solution.
Cons
More complex to understand and implement compared to a simple backtracking solution.
Requires significant memory for the memoization and parent tables, although it's feasible for the given constraints.
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.