Concatenated Divisibility
HARDDescription
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
Checkout 2 different approaches to solve Concatenated Divisibility. Click on different approaches to view the approach and algorithm in detail.
Dynamic Programming with Bitmasking
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.
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 Analysis
Pros and Cons
- Efficient enough to pass the given constraints.
- Guaranteed to find the optimal (lexicographically smallest) solution.
- 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 Solution
Watch the video walkthrough for Concatenated Divisibility
Similar Questions
5 related questions you might find useful
Patterns:
Data Structures:
Subscribe to Scale Engineer newsletter
Learn about System Design, Software Engineering, and interview experiences every week.
No spam, unsubscribe at any time.