Skip to main content

Using Meet in the Middle Pattern

1. Applying Meet in the Middle Technique

The Meet in the Middle pattern is a divide-and-conquer algorithmic technique used to optimize problems that can be split into two independent parts. It is particularly effective for problems where a brute-force approach would be too slow, but the problem can be decomposed into two subproblems that are solved separately and then combined.

Step Description Implementation
1. Split the problem Divide the input into two halves or independent subproblems. For example, in a subset sum problem, split the array into two halves.
2. Solve each half independently Compute all possible solutions or intermediate results for each half. For subset sum, compute all possible sums from the first half and store in a set.
3. Combine results Use the intermediate results to find a solution that satisfies the problem constraints. For subset sum, check if any element from the second half's sums can combine with an element from the first half to reach the target.

Example: Subset Sum Problem

Given an array of integers, determine if there exists a subset that sums to zero.

function canPartition(nums) {
    const total = nums.reduce((a, b) => a + b, 0);
    if (total % 2 !== 0) return false;
    const target = total / 2;

    // Split into two halves
    const leftHalf = nums.slice(0, nums.length / 2);
    const rightHalf = nums.slice(nums.length / 2);

    // Compute all possible sums for left half
    const leftSums = new Set();
    const n = leftHalf.length;
    for (let i = 0; i < (1 << n); i++) {
        let sum = 0;
        for (let j = 0; j < n; j++) {
            if ((i & (1 << j)) !== 0) {
                sum += leftHalf[j];
            }
        }
        leftSums.add(sum);
    }

    // For each sum in right half, check if complement exists in left sums
    const m = rightHalf.length;
    for (let i = 0; i < (1 << m); i++) {
        let sum = 0;
        for (let j = 0; j < m; j++) {
            if ((i & (1 << j)) !== 0) {
                sum += rightHalf[j];
            }
        }
        if (leftSums.has(target - sum)) {
            return true;
        }
    }

    return false;
}

// Example usage:
const nums = [1, -1, 2, -2];
console.log(canPartition(nums)); // true
Note: The Meet in the Middle technique reduces time complexity from O(2^n) to O(2^(n/2)), which is a significant improvement for large inputs.

Key Characteristics

  • Effective for problems with exponential time complexity.
  • Reduces the search space by splitting the problem into two manageable parts.
  • Requires additional space to store intermediate results (e.g., a hash set).

2. Solving Subset Sum

The Meet-in-the-Middle (MITM) algorithm pattern is particularly useful for solving the Subset Sum problem efficiently when the input size is large. Instead of checking all 2n subsets, MITM splits the array into two halves and computes all possible subset sums for each half.

Property Description Notes
Input Array of integers nums, target sum target Array length is even for simplicity; can be adapted for odd lengths
Time Complexity O(2n/2 × log(2n/2)) Reduces from O(2n) to O(2n/2 × n)
Space Complexity O(2n/2) Storing subset sums for both halves

Example: Subset Sum with MITM

function subsetSumMITM(nums, target) {
  const n = nums.length;
  const half = Math.floor(n / 2);

  // Split into two halves
  const left = nums.slice(0, half);
  const right = nums.slice(half);

  // Generate all subset sums for left half
  const leftSums = new Set();
  for (let mask = 0; mask < (1 << left.length); mask++) {
    let sum = 0;
    for (let i = 0; i < left.length; i++) {
      if ((mask & (1 << i)) !== 0) sum += left[i];
    }
    leftSums.add(sum);
  }

  // For each subset sum in right half, check if complement exists
  for (let mask = 0; mask < (1 << right.length); mask++) {
    let sum = 0;
    for (let i = 0; i < right.length; i++) {
      if ((mask & (1 << i)) !== 0) sum += right[i];
    }
    if (leftSums.has(target - sum)) return true;
  }

  return false;
}

// Example usage:
const nums = [1, 3, 5, 7, 9, 11];
const target = 16;
console.log(subsetSumMITM(nums, target)); // true (e.g., 1 + 3 + 5 + 7 = 16)
Note: This approach reduces the exponential time complexity from O(2n) to O(2n/2 × n) by leveraging the two halves of the array. It is especially effective when n is large.

3. Finding Closest Sum

Given an array of integers and a target sum, find two elements whose sum is closest to the target.

Property Description Example
Input Array of integers and target sum [1, 3, 5, 7, 9], target = 10
Output Pair of elements with sum closest to target [3, 7]

Example: Finding Closest Sum

function findClosestSum(arr, target) {
  let left = 0;
  let right = arr.length - 1;
  let closestSum = arr[left] + arr[right];
  let minDiff = Math.abs(target - closestSum);

  while (left < right) {
    const currentSum = arr[left] + arr[right];
    const diff = Math.abs(target - currentSum);

    if (diff < minDiff) {
      minDiff = diff;
      closestSum = currentSum;
    }

    if (currentSum < target) {
      left++;
    } else {
      right--;
    }
  }

  return closestSum;
}

const arr = [1, 3, 5, 7, 9];
const target = 10;
console.log(findClosestSum(arr, target)); // Output: 10
Note: This pattern is efficient for sorted arrays and uses two pointers to reduce time complexity to O(n).

Bidirectional search is a graph traversal algorithm that runs two simultaneous searches: one from the source node and another from the target node, meeting in the middle. This approach can significantly reduce the search space compared to unidirectional search.

Aspect Description Notes
Search Direction Runs two searches: forward from source, backward from target Reduces search space by half in many cases
Data Structures Two queues/stacks, two visited sets Forward and backward search states maintained separately
Meeting Point Searches meet at a common node Path is reconstructed from both directions

Example: Bidirectional BFS Implementation

function bidirectionalBFS(graph, start, target) {
  let forwardQueue = [start];
  let backwardQueue = [target];
  let forwardVisited = new Set([start]);
  let backwardVisited = new Set([target]);
  let forwardParent = new Map();
  let backwardParent = new Map();

  while (forwardQueue.length && backwardQueue.length) {
    // Expand forward search
    let current = forwardQueue.shift();
    if (backwardVisited.has(current)) {
      return reconstructPath(forwardParent, backwardParent, current);
    }
    for (let neighbor of graph[current]) {
      if (!forwardVisited.has(neighbor)) {
        forwardVisited.add(neighbor);
        forwardParent.set(neighbor, current);
        forwardQueue.push(neighbor);
      }
    }

    // Expand backward search
    current = backwardQueue.shift();
    if (forwardVisited.has(current)) {
      return reconstructPath(forwardParent, backwardParent, current);
    }
    for (let neighbor of graph[current]) {
      if (!backwardVisited.has(neighbor)) {
        backwardVisited.add(neighbor);
        backwardParent.set(neighbor, current);
        backwardQueue.push(neighbor);
      }
    }
  }

  return null; // No path found
}

function reconstructPath(forwardParent, backwardParent, meetingPoint) {
  let path = [];
  let current = meetingPoint;
  while (current !== null) {
    path.unshift(current);
    current = forwardParent.get(current);
  }
  current = backwardParent.get(meetingPoint);
  while (current !== null) {
    path.push(current);
    current = backwardParent.get(current);
  }
  return path;
}
Note: The bidirectional search is most effective when the graph has a symmetric structure or when the target node is known in advance.

5. Solving 4Sum Problem

The Meet-in-the-Middle (MITM) pattern is particularly useful for problems where we can split the input into two parts and reduce the time complexity by precomputing one half.

For the 4Sum problem, we are given an array of integers and need to find all unique quadruplets that sum to a target value.

Using the MITM approach:

  1. Split the array into two halves.
  2. Precompute all possible sums of pairs from the first half and store them in a hash map with their indices.
  3. For each pair from the second half, compute the complement (target - sum) and look it up in the hash map.
  4. Combine valid pairs to form quadruplets and ensure uniqueness.

Example: 4Sum using MITM

function fourSum(nums, target) {
    const n = nums.length;
    if (n < 4) return [];

    // Split into two halves
    const firstHalf = nums.slice(0, n / 2);
    const secondHalf = nums.slice(n / 2);

    // Precompute sums from first half
    const sumMap = new Map();
    for (let i = 0; i < firstHalf.length; i++) {
        for (let j = i + 1; j < firstHalf.length; j++) {
            const sum = firstHalf[i] + firstHalf[j];
            if (!sumMap.has(sum)) {
                sumMap.set(sum, []);
            }
            sumMap.get(sum).push([i, j]);
        }
    }

    const result = new Set();
    for (let i = 0; i < secondHalf.length; i++) {
        for (let j = i + 1; j < secondHalf.length; j++) {
            const complement = target - (secondHalf[i] + secondHalf[j]);
            if (sumMap.has(complement)) {
                const pairs = sumMap.get(complement);
                for (const [a, b] of pairs) {
                    const quad = [
                        firstHalf[a], firstHalf[b],
                        secondHalf[i], secondHalf[j]
                    ].sort((x, y) => x - y);
                    result.add(JSON.stringify(quad));
                }
            }
        }
    }

    return Array.from(result).map(JSON.parse);
}

// Example usage:
const nums = [1, 0, -1, 0, -2, 2];
const target = 0;
console.log(fourSum(nums, target)); // [[-2, -1, 1, 2], [-2, 0, 0, 2], [-1, 0, 0, 1]]
Step Description Time Complexity Space Complexity
Splitting Divide array into two halves of size n/2 O(n) O(n)
Precompute Sums Compute all pair sums from first half and store in hash map O(n²) O(n²)
Lookup Complements For each pair in second half, look up complement in hash map O(n²) O(1)
Combine Quadruplets Generate unique quadruplets from valid pairs O(n²) O(n)
Note: This approach reduces the time complexity from O(n⁴) to O(n²) by leveraging precomputation and hash lookups.

MITM Pattern for 4Sum

  • Split array into two halves.
  • Precompute all pair sums from the first half and store in a hash map.
  • For each pair in the second half, find complement sums in the hash map.
  • Combine valid pairs to form unique quadruplets.

6. Finding Minimum XOR

The "Finding Minimum XOR" problem involves finding two numbers in an array such that their XOR value is minimized. This pattern is often solved using a Trie data structure to efficiently search for the closest number that would minimize the XOR.

Property Description Notes
XOR Operation XOR of two numbers is minimized when the numbers are as close as possible in binary representation. Minimizing XOR is equivalent to minimizing the difference between two numbers in binary form.
Trie Implementation Each bit of a number is inserted into a Trie, starting from the most significant bit. For each number in the array, we traverse the Trie to find the closest number that minimizes XOR.

Example: Finding Minimum XOR Using Trie

class TrieNode {
    constructor() {
        this.children = {};
        this.value = null;
    }
}

class Trie {
    constructor() {
        this.root = new TrieNode();
    }

    insert(num) {
        let node = this.root;
        for (let i = 31; i >= 0; i--) {
            const bit = (num & (1 << i)) !== 0 ? 1 : 0;
            if (!node.children[bit]) {
                node.children[bit] = new TrieNode();
            }
            node = node.children[bit];
        }
        node.value = num;
    }

    findMinXor(num) {
        let node = this.root;
        let xorValue = 0;

        for (let i = 31; i >= 0; i--) {
            const bit = (num & (1 << i)) !== 0 ? 1 : 0;
            const toggledBit = bit === 1 ? 0 : 1;

            if (node.children[toggledBit]) {
                xorValue |= (1 << i);
                node = node.children[toggledBit];
            } else {
                node = node.children[bit];
            }
        }

        return xorValue;
    }
}

function minXOR(arr) {
    const trie = new Trie();
    let minXor = Infinity;

    for (let num of arr) {
        if (trie.root.children[0] || trie.root.children[1]) {
            const xor = trie.findMinXor(num);
            minXor = Math.min(minXor, xor);
        }
        trie.insert(num);
    }

    return minXor;
}

// Example usage:
const arr = [9, 5, 3];
console.log(minXOR(arr)); // Output: 6 (from 9 XOR 5)
Note: The algorithm uses a Trie to store binary representations of numbers. For each number, it finds the closest number in the Trie that minimizes XOR.

7. Solving Knapsack Variants

Variant Description Solution Approach Time Complexity
0/1 Knapsack Each item can be used at most once. Maximize value within weight capacity. Dynamic Programming (2D array) O(nW)
Unbounded Knapsack Each item can be used unlimited times. Maximize value within weight capacity. Dynamic Programming (1D array) O(nW)
Bounded Knapsack Each item has a limited count. Maximize value within weight capacity. Dynamic Programming with item counts O(nW)
Subset Sum Determine if a subset of items sums to a target value. Dynamic Programming (1D array) O(nS)
Partition Problem Partition array into two subsets with equal sum. Subset Sum variant O(nS)

Example: 0/1 Knapsack

const weights = [2, 1, 3, 2];
const values = [3, 2, 4, 2];
const capacity = 4;

function knapsack(weights, values, capacity) {
  const n = weights.length;
  const dp = Array(n + 1).fill().map(() => Array(capacity + 1).fill(0));

  for (let i = 1; i <= n; i++) {
    for (let w = 0; w <= capacity; w++) {
      dp[i][w] = dp[i - 1][w];
      if (weights[i - 1] <= w) {
        dp[i][w] = Math.max(dp[i][w], dp[i - 1][w - weights[i - 1]] + values[i - 1]);
      }
    }
  }

  return dp[n][capacity];
}

console.log(knapsack(weights, values, capacity)); // Output: 7

Example: Subset Sum

const nums = [1, 2, 3, 4];
const target = 5;

function canPartition(nums, target) {
  const dp = Array(target + 1).fill(false);
  dp[0] = true;

  for (const num of nums) {
    for (let i = target; i >= num; i--) {
      dp[i] = dp[i] || dp[i - num];
    }
  }

  return dp[target];
}

console.log(canPartition(nums, target)); // Output: true (2 + 3 = 5)

8. Implementing Hash-Based Merging

Hash-based merging is a technique used to efficiently combine data structures or solve problems by leveraging hash tables for fast lookups and merges. It's particularly useful in scenarios where you need to find common elements or merge sorted arrays efficiently.

Operation Description Time Complexity Space Complexity
Hash Table Construction Build a hash table from one dataset for O(1) lookups. O(n) O(n)
Merging with Lookup Iterate through the second dataset and check for matches in the hash table. O(m) O(1)

Example: Finding Common Elements in Two Arrays

const arr1 = [1, 2, 3, 4, 5];
const arr2 = [3, 4, 5, 6, 7];

// Step 1: Build hash table from first array
const hashSet = new Set(arr1);

// Step 2: Find common elements by checking second array
const common = arr2.filter(item => hashSet.has(item));

console.log(common); // [3, 4, 5]

Example: Merging Sorted Arrays Using Hash Map

const arr1 = [1, 3, 5, 7];
const arr2 = [2, 4, 6, 8];

// Create a map to store elements from first array
const map = new Map();
for (let i = 0; i < arr1.length; i++) {
    map.set(arr1[i], true);
}

// Merge by checking second array elements
const merged = [...arr1, ...arr2].filter(item => map.has(item) || !map.has(item));

console.log(merged); // [1, 3, 5, 7, 2, 4, 6, 8]
Note: The hash-based merge approach is efficient for unsorted data or when you need to find intersection elements. For sorted arrays, a two-pointer technique is more efficient.

9. Solving Partition Equal Subset

The Partition Equal Subset Sum problem asks whether a given array of positive integers can be partitioned into two subsets with equal sums.

This problem is a variant of the subset sum problem and can be solved efficiently using the Meet-in-the-Middle technique to reduce time complexity.

Problem Description Solution Approach
Input Array of positive integers Partition into two subsets with equal sum
Output Boolean indicating if partition is possible Meet-in-the-Middle + Subset Sum

Example: Partition Equal Subset Sum

function canPartition(nums) {
    const totalSum = nums.reduce((sum, num) => sum + num, 0);
    if (totalSum % 2 !== 0) return false;
    const target = totalSum / 2;

    // Generate all subset sums for first half
    const leftSums = new Set();
    const n = nums.length;
    for (let mask = 0; mask < (1 << Math.floor(n / 2)); mask++) {
        let sum = 0;
        for (let i = 0; i < Math.floor(n / 2); i++) {
            if ((mask & (1 << i)) !== 0) sum += nums[i];
        }
        leftSums.add(sum);
    }

    // For second half, check if complement exists in left sums
    for (let mask = 0; mask < (1 << Math.ceil(n / 2)); mask++) {
        let sum = 0;
        for (let i = Math.floor(n / 2); i < n; i++) {
            if ((mask & (1 << (i - Math.floor(n / 2)))) !== 0) sum += nums[i];
        }
        if (leftSums.has(target - sum)) return true;
    }

    return false;
}

// Example usage:
console.log(canPartition([1, 5, 11, 5])); // true
console.log(canPartition([1, 2, 3, 5]));  // false
Approach Time Complexity Space Complexity Description
Meet-in-the-Middle O(2^(n/2)) O(2^(n/2)) Split array into two halves, compute subset sums for first half and check if complement exists in second half
Note: The Meet-in-the-Middle approach reduces the exponential time complexity from O(2^n) to O(2^(n/2)), making it feasible for larger inputs.

10. Optimizing Time Complexity

Operation Time Complexity (Before) Time Complexity (After) Notes
Brute Force Search O(2^n) O(n * 2^(n/2)) Meet-in-the-middle reduces exponential search space by splitting problem into two halves
Subset Sum O(2^n) O(n * 2^(n/2)) Generate all subsets for left half, right half, then combine using two pointers
Shortest Path in Graph O(V^2) with Floyd-Warshall O(V * 2^(V/2)) Split nodes into two groups and compute paths between them

Example: Subset Sum Optimization

function subsetSumOptimized(arr, target) {
  const n = arr.length;
  const leftHalf = [];
  const rightHalf = [];

  // Split array into two halves
  for (let i = 0; i < n / 2; i++) {
    leftHalf.push(arr[i]);
  }
  for (let i = n / 2; i < n; i++) {
    rightHalf.push(arr[i]);
  }

  // Generate all subset sums for left half
  const leftSums = new Set();
  for (let i = 0; i < (1 << leftHalf.length); i++) {
    let sum = 0;
    for (let j = 0; j < leftHalf.length; j++) {
      if ((i & (1 << j)) !== 0) {
        sum += leftHalf[j];
      }
    }
    leftSums.add(sum);
  }

  // For each subset sum in right half, check if complement exists
  for (let i = 0; i < (1 << rightHalf.length); i++) {
    let sum = 0;
    for (let j = 0; j < rightHalf.length; j++) {
      if ((i & (1 << j)) !== 0) {
        sum += rightHalf[j];
      }
    }
    if (leftSums.has(target - sum)) {
      return true;
    }
  }

  return false;
}
Note: This approach reduces time complexity from O(2^n) to O(n * 2^(n/2)), which is significantly better for large inputs.

Was this article helpful?