This problem can be solved using the bottom-up Dynamic Programming approach with backtracking.
Let's consider a recurrence relation f(i1, i2)
, which helps to check whether the tail of the sequence arr2
can be removed from the tail of the sequence arr1
:
f(i1, i2) = true, if(i1 == length(arr1) AND i2 == length(arr2))
f(i1, i2) = f(i1 + 1, i2) OR f(i1 + 1, i2 + 1), if(arr1[i1] == arr2[i2])
f(i1, i2) = f(i1 + 1, i2), if(arr1[i1] != arr2[i2])
solution = f(0, 0)
I use the term tail to denote the subsequence of arr1
which starts at the index i1
and spans to the end of arr1
(and the same for arr2
- tail of arr2
starts at the index i2
and spans to the end of arr2
).
Let's start with top-down implementation of the given recurrence relation (yet without memoization, in order to keep the explanation simple). Below is the snippet of Java code, which prints all possible subsequences of arr1
after removal of arr2
:
void remove(int[] arr1, int[] arr2) {
boolean canBeRemoved = remove(arr1, arr2, 0, 0, new Stack<>());
System.out.println(canBeRemoved);
}
boolean remove(int[] arr1, int[] arr2, int i1, int i2, Stack<Integer> stack) {
if (i1 == arr1.length) {
if (i2 == arr2.length) {
// print yet another version of arr1, after removal of arr2
System.out.println(stack);
return true;
}
return false;
}
boolean canBeRemoved = false;
if ((i2 < arr2.length) && (arr1[i1] == arr2[i2])) {
// current item can be removed
canBeRemoved |= remove(arr1, arr2, i1 + 1, i2 + 1, stack);
}
stack.push(arr1[i1]);
canBeRemoved |= remove(arr1, arr2, i1 + 1, i2, stack);
stack.pop();
return canBeRemoved;
}
Th provided snippet of code does not use any memoization technique, and has the exponential runtime complexity for all instances of given problem.
However, we can see that the variable i1
can only have the value from the interval [0..length(arr1)]
, also the variable i2
can only have the value from the interval [0..length(arr2)]
.
Hence, it is possible to check, whether arr2
can be removed from arr1
with polynomial runtime complexity: O(length(arr1) * length(arr2))
.
On the other hand, even if we find with polynomial runtime complexity that arr2
can be removed from arr1
- there still might be an exponential amount of possible ways to remove arr2
from arr1
.
For example, consider the instance of the problem: when it is needed to remove arr2 = [1,1,1]
from arr1 = [1,1,1,1,1,1,1]
. There are 7!/(3! * 4!) = 35
ways to do it.
Nevertheless, below is the bottom-up Dynamic Programming solution with backtracking, which is still for many instances of given problem will have better runtime complexity than exponential:
void remove_bottom_up(int[] arr1, int[] arr2) {
boolean[][] memoized = calculate_memoization_table(arr1, arr2);
backtrack(arr1, arr2, 0, 0, memoized, new Stack<>());
}
/**
* Has a polynomial runtime complexity: O(length(arr1) * length(arr2))
*/
boolean[][] calculate_memoization_table(int[] arr1, int[] arr2) {
boolean[][] memoized = new boolean[arr1.length + 1][arr2.length + 1];
memoized[arr1.length][arr2.length] = true;
for (int i1 = arr1.length - 1; i1 >= 0; i1--) {
for (int i2 = arr2.length; i2 >= 0; i2--) {
if ((i2 < arr2.length) && (arr1[i1] == arr2[i2])) {
memoized[i1][i2] = memoized[i1 + 1][i2 + 1];
}
memoized[i1][i2] |= memoized[i1 + 1][i2];
}
}
return memoized;
}
/**
* Might have exponential runtime complexity.
*
* E.g. consider the instance of the problem, when it is needed to remove
* arr2 = [1,1,1] from arr1 = [1,1,1,1,1,1,1].
*
* There are 7!/(3! * 4!) = 35 ways to do it.
*/
void backtrack(int[] arr1, int[] arr2, int i1, int i2, boolean[][] memoized, Stack<Integer> stack) {
if (!memoized[i1][i2]) {
// arr2 can't be removed from arr1
return;
}
if (i1 == arr1.length) {
// at this point, instead of printing the variant of arr1 after removing of arr2
// we can just collect this variant into some other container
// e.g. allVariants.add(stack.clone())
System.out.println(stack);
return;
}
if ((i2 < arr2.length) && (arr1[i1] == arr2[i2])) {
backtrack(arr1, arr2, i1 + 1, i2 + 1, memoized, stack);
}
stack.push(arr1[i1]);
backtrack(arr1, arr2, i1 + 1, i2, memoized, stack);
stack.pop();
}
JavaScript Implementation of described solution
function remove_bottom_up(base_arr, removed_arr) {
// Initialize memoization table
var memoized = new Array(base_arr.length + 1);
for (var i = 0; i < base_arr.length + 1; i++) {
memoized[i] = new Array(removed_arr.length + 1);
}
memoized[base_arr.length][removed_arr.length] = true;
// Calculate memoization table
for (var i1 = base_arr.length - 1; i1 >= 0; i1--) {
for (var i2 = removed_arr.length; i2 >= 0; i2--) {
if ((i2 < removed_arr.length) && (base_arr[i1] == removed_arr[i2])) {
memoized[i1][i2] = memoized[i1 + 1][i2 + 1];
}
memoized[i1][i2] |= memoized[i1 + 1][i2];
}
}
// Collect all variants
var all_variants = [];
backtrack(base_arr, removed_arr, 0, 0, memoized, [], all_variants);
return all_variants;
}
function backtrack(base_arr, removed_arr, i1, i2, memoized, stack, all_variants) {
if (!memoized[i1][i2]) {
// arr2 can't be removed from arr1
return;
}
if (i1 == base_arr.length) {
all_variants.push(stack.slice(0));
return;
}
if ((i2 < removed_arr.length) && (base_arr[i1] == removed_arr[i2])) {
backtrack(base_arr, removed_arr, i1 + 1, i2 + 1, memoized, stack, all_variants);
}
stack.push(base_arr[i1]);
backtrack(base_arr, removed_arr, i1 + 1, i2, memoized, stack, all_variants);
stack.pop();
}
console.log(JSON.stringify(remove_bottom_up([1, 2, 1, 3, 1, 4, 4], [1, 4, 4])));
console.log(JSON.stringify(remove_bottom_up([8, 6, 4, 4], [6, 4, 8])));
console.log(JSON.stringify(remove_bottom_up([1, 1, 2], [1])));
console.log(JSON.stringify(remove_bottom_up([1, 1, 1, 1, 1, 1, 1], [1, 1, 1])));