Skip to content

Divide And Conquer

divide-and-conquer: The Ultimate Cheatsheet

Section titled “divide-and-conquer: The Ultimate Cheatsheet”

Divide and conquer is a powerful algorithmic paradigm that solves problems by recursively breaking them down into smaller subproblems of the same type, solving the subproblems recursively, and then combining their solutions to solve the original problem. This “divide” and “conquer” process continues until the subproblems become so small that they can be solved trivially.

Why is it important? Divide and conquer is crucial because it can significantly improve the efficiency of algorithms for many problems that would be intractable with brute-force approaches. It leverages the power of recursion to tackle complex tasks in a manageable, modular way.

Core Concepts:

  • Divide: Break the problem into smaller, independent subproblems.
  • Conquer: Recursively solve the subproblems. Base cases are crucial to stop the recursion.
  • Combine: Combine the solutions of the subproblems to obtain the solution to the original problem.

Key Terminology:

  • Base Case: The smallest subproblem that can be solved directly without further recursion.
  • Recursive Step: The step where the problem is divided into subproblems and the solutions are combined.
  • Subproblem: A smaller instance of the original problem.

2. When to Use divide-and-conquer (and When Not To)

Section titled “2. When to Use divide-and-conquer (and When Not To)”

Use Divide and Conquer when:

  • The problem can be naturally divided into smaller, independent subproblems.
  • The subproblems are of the same type as the original problem (allowing recursion).
  • The solutions to the subproblems can be efficiently combined to solve the original problem.
  • The time complexity of the divide-and-conquer approach is significantly better than brute-force methods.

Don’t use Divide and Conquer when:

  • The overhead of dividing and combining the subproblems outweighs the benefits. This can happen if the subproblems are highly interdependent or the combination step is very expensive.
  • The problem is inherently sequential and cannot be easily parallelized.
  • A simpler, more efficient algorithm already exists.

3. Core Algorithm / Data Structure Template

Section titled “3. Core Algorithm / Data Structure Template”
Algorithm DivideAndConquer(problem):
IF (problem is small enough):
return solution to the problem // Base Case
ELSE:
Divide problem into smaller subproblems: subproblem1, subproblem2, ..., subproblemk
FOR each subproblem i:
solution_i = DivideAndConquer(subproblem_i) // Recursive call
Combine solutions solution1, solution2, ..., solutionk to get the solution to the original problem
return solution

This example demonstrates merge sort, a classic divide-and-conquer algorithm.

def merge_sort(arr):
if len(arr) <= 1:
return arr # Base case: already sorted
mid = len(arr) // 2
left = arr[:mid]
right = arr[mid:]
left = merge_sort(left)
right = merge_sort(right)
return merge(left, right)
def merge(left, right):
merged = []
i = j = 0
while i < len(left) and j < len(right):
if left[i] < right[j]:
merged.append(left[i])
i += 1
else:
merged.append(right[j])
j += 1
merged.extend(left[i:])
merged.extend(right[j:])
return merged
import java.util.Arrays;
class MergeSort {
public static void mergeSort(int[] arr) {
if (arr.length <= 1) return; // Base case
int mid = arr.length / 2;
int[] left = Arrays.copyOfRange(arr, 0, mid);
int[] right = Arrays.copyOfRange(arr, mid, arr.length);
mergeSort(left);
mergeSort(right);
merge(arr, left, right);
}
public static void merge(int[] arr, int[] left, int[] right) {
int i = 0, j = 0, k = 0;
while (i < left.length && j < right.length) {
if (left[i] <= right[j]) {
arr[k++] = left[i++];
} else {
arr[k++] = right[j++];
}
}
while (i < left.length) arr[k++] = left[i++];
while (j < right.length) arr[k++] = right[j++];
}
}
#include <iostream>
#include <vector>
using namespace std;
vector<int> mergeSort(vector<int>& arr) {
if (arr.size() <= 1) return arr; // Base case
int mid = arr.size() / 2;
vector<int> left(arr.begin(), arr.begin() + mid);
vector<int> right(arr.begin() + mid, arr.end());
left = mergeSort(left);
right = mergeSort(right);
return merge(left, right);
}
vector<int> merge(vector<int>& left, vector<int>& right) {
vector<int> merged;
int i = 0, j = 0;
while (i < left.size() && j < right.size()) {
if (left[i] <= right[j]) {
merged.push_back(left[i++]);
} else {
merged.push_back(right[j++]);
}
}
while (i < left.size()) merged.push_back(left[i++]);
while (j < right.size()) merged.push_back(right[j++]);
return merged;
}
CaseTime ComplexitySpace Complexity
BestO(n log n)O(n)
AverageO(n log n)O(n)
WorstO(n log n)O(n)

The space complexity is O(n) due to the recursive calls creating new arrays in each step. The time complexity is always O(n log n) because the algorithm consistently divides the problem in half and merges the sorted subarrays.

  • Base Case is Crucial: Incorrect or missing base cases lead to infinite recursion and stack overflow errors.
  • Subproblem Independence: Ensure subproblems are truly independent to avoid redundant computations.
  • Efficient Combination: The combine step is critical; an inefficient combination can negate the benefits of dividing the problem.
  • Tail Recursion: Where possible, use tail recursion (the recursive call is the last operation) to optimize for some compilers/interpreters. This can prevent stack overflow issues for very deep recursion.
  • Avoid unnecessary copies: In some cases (like in-place merge sort variants), careful planning can reduce memory usage by avoiding unnecessary copies of subarrays.

High-level Approach:

This problem can be solved efficiently using a divide-and-conquer strategy. First, divide the k lists into pairs. Then, merge each pair using a standard merge algorithm. This results in k/2 merged lists. Repeat this process recursively until only one sorted list remains. The merging of two lists is a fundamental divide-and-conquer step itself (as seen in the merge sort implementation). The base case is when there’s only one list left. This approach has a time complexity of O(N log k), where N is the total number of nodes in all lists and k is the number of lists. A priority queue based approach can also be used, but this divide-and-conquer method demonstrates the recursive nature well.