Divide and Conquer Algorithms

Divide and conquer is a fundamental algorithmic paradigm that solves problems by breaking them into smaller subproblems, solving them recursively, and combining the results. This approach leads to elegant solutions with optimal time complexities for many classic problems.

Merge Sort - Classic Divide and Conquer

Merge sort exemplifies the divide-and-conquer approach: divide the array into halves, recursively sort each half, then merge the sorted halves.

Merge Sort Implementation

#include <iostream>
#include <vector>
using namespace std;

class MergeSort {
public:
    void mergeSort(vector<int>& arr, int left, int right) {
        if (left >= right) return;
        
        int mid = left + (right - left) / 2;
        
        // Divide: recursively sort both halves
        mergeSort(arr, left, mid);
        mergeSort(arr, mid + 1, right);
        
        // Conquer: merge the sorted halves
        merge(arr, left, mid, right);
    }
    
private:
    void merge(vector<int>& arr, int left, int mid, int right) {
        vector<int> temp(right - left + 1);
        int i = left, j = mid + 1, k = 0;
        
        // Merge two sorted arrays
        while (i <= mid && j <= right) {
            if (arr[i] <= arr[j]) {
                temp[k++] = arr[i++];
            } else {
                temp[k++] = arr[j++];
            }
        }
        
        // Copy remaining elements
        while (i <= mid) temp[k++] = arr[i++];
        while (j <= right) temp[k++] = arr[j++];
        
        // Copy back to original array
        for (int i = 0; i < k; i++) {
            arr[left + i] = temp[i];
        }
    }
};

int main() {
    vector<int> arr = {64, 34, 25, 12, 22, 11, 90, 88, 76, 50, 42};
    
    cout << "Original array: ";
    for (int x : arr) cout << x << " ";
    cout << endl;
    
    MergeSort ms;
    ms.mergeSort(arr, 0, arr.size() - 1);
    
    cout << "Sorted array: ";
    for (int x : arr) cout << x << " ";
    cout << endl;
    
    return 0;
}

Merge sort has O(n log n) time complexity and O(n) space complexity. It is a stable sorting algorithm and particularly useful for sorting linked lists.

Quick Sort and Quick Select

Quick sort partitions the array around a pivot element, then recursively sorts the partitions. Quick select finds the kth smallest element efficiently.

Quick Sort and Quick Select

#include <iostream>
#include <vector>
#include <random>
using namespace std;

class QuickSort {
public:
    void quickSort(vector<int>& arr, int left, int right) {
        if (left >= right) return;
        
        // Randomize pivot to avoid worst case
        int randomIndex = left + rand() % (right - left + 1);
        swap(arr[left], arr[randomIndex]);
        
        int pivotIndex = partition(arr, left, right);
        
        quickSort(arr, left, pivotIndex - 1);
        quickSort(arr, pivotIndex + 1, right);
    }
    
    // Quick Select - find kth smallest element
    int quickSelect(vector<int>& arr, int left, int right, int k) {
        if (left == right) return arr[left];
        
        int randomIndex = left + rand() % (right - left + 1);
        swap(arr[left], arr[randomIndex]);
        
        int pivotIndex = partition(arr, left, right);
        
        if (pivotIndex == k) return arr[pivotIndex];
        else if (pivotIndex > k) return quickSelect(arr, left, pivotIndex - 1, k);
        else return quickSelect(arr, pivotIndex + 1, right, k);
    }
    
private:
    int partition(vector<int>& arr, int left, int right) {
        int pivot = arr[left];
        int i = left, j = right;
        
        while (i < j) {
            while (i < j && arr[j] >= pivot) j--;
            if (i < j) arr[i++] = arr[j];
            while (i < j && arr[i] <= pivot) i++;
            if (i < j) arr[j--] = arr[i];
        }
        
        arr[i] = pivot;
        return i;
    }
};

int main() {
    srand(time(nullptr));
    
    vector<int> arr = {64, 34, 25, 12, 22, 11, 90, 88, 76, 50, 42};
    
    cout << "Original array: ";
    for (int x : arr) cout << x << " ";
    cout << endl;
    
    QuickSort qs;
    vector<int> sortArr = arr;
    qs.quickSort(sortArr, 0, sortArr.size() - 1);
    
    cout << "Sorted array: ";
    for (int x : sortArr) cout << x << " ";
    cout << endl;
    
    // Test quick select
    int k = 5; // 6th smallest element (0-indexed)
    int kthSmallest = qs.quickSelect(arr, 0, arr.size() - 1, k);
    cout << "The " << (k + 1) << "th smallest element: " << kthSmallest << endl;
    
    return 0;
}

Quick sort has O(n log n) average time complexity, O(nΒ²) worst case. Quick select can find the kth smallest element in O(n) average time.

Maximum Subarray Problem

The maximum subarray problem can be solved elegantly using divide and conquer, achieving O(n log n) complexity.

Maximum Subarray - Divide and Conquer

#include <iostream>
#include <vector>
#include <algorithm>
#include <climits>
using namespace std;

class MaxSubarray {
public:
    struct Result {
        int maxSum;
        int left;
        int right;
        
        Result(int sum = INT_MIN, int l = -1, int r = -1) 
            : maxSum(sum), left(l), right(r) {}
    };
    
    Result maxSubarrayDC(vector<int>& arr, int left, int right) {
        // Base case: single element
        if (left == right) {
            return Result(arr[left], left, right);
        }
        
        int mid = left + (right - left) / 2;
        
        // Recursively find max subarray in left and right halves
        Result leftResult = maxSubarrayDC(arr, left, mid);
        Result rightResult = maxSubarrayDC(arr, mid + 1, right);
        
        // Find max subarray crossing the middle
        Result crossResult = maxCrossingSubarray(arr, left, mid, right);
        
        // Return the maximum of three cases
        if (leftResult.maxSum >= rightResult.maxSum && 
            leftResult.maxSum >= crossResult.maxSum) {
            return leftResult;
        } else if (rightResult.maxSum >= leftResult.maxSum && 
                   rightResult.maxSum >= crossResult.maxSum) {
            return rightResult;
        } else {
            return crossResult;
        }
    }
    
private:
    Result maxCrossingSubarray(vector<int>& arr, int left, int mid, int right) {
        // Find max sum extending left from mid
        int leftSum = INT_MIN;
        int sum = 0;
        int maxLeft = mid;
        
        for (int i = mid; i >= left; i--) {
            sum += arr[i];
            if (sum > leftSum) {
                leftSum = sum;
                maxLeft = i;
            }
        }
        
        // Find max sum extending right from mid+1
        int rightSum = INT_MIN;
        sum = 0;
        int maxRight = mid + 1;
        
        for (int i = mid + 1; i <= right; i++) {
            sum += arr[i];
            if (sum > rightSum) {
                rightSum = sum;
                maxRight = i;
            }
        }
        
        return Result(leftSum + rightSum, maxLeft, maxRight);
    }
};

// Kadane's algorithm for comparison (O(n))
int kadane(vector<int>& arr) {
    int maxSoFar = arr[0];
    int maxEndingHere = arr[0];
    
    for (int i = 1; i < arr.size(); i++) {
        maxEndingHere = max(arr[i], maxEndingHere + arr[i]);
        maxSoFar = max(maxSoFar, maxEndingHere);
    }
    
    return maxSoFar;
}

int main() {
    vector<int> arr = {-2, -3, 4, -1, -2, 1, 5, -3};
    
    cout << "Array: ";
    for (int x : arr) cout << x << " ";
    cout << endl;
    
    MaxSubarray ms;
    MaxSubarray::Result result = ms.maxSubarrayDC(arr, 0, arr.size() - 1);
    
    cout << "Divide & Conquer - Max sum: " << result.maxSum << endl;
    cout << "Subarray indices: [" << result.left << ", " << result.right << "]" << endl;
    cout << "Subarray: ";
    for (int i = result.left; i <= result.right; i++) {
        cout << arr[i] << " ";
    }
    cout << endl;
    
    cout << "Kadane's algorithm: " << kadane(arr) << endl;
    
    return 0;
}

The divide-and-conquer approach considers three cases: max subarray in left half, right half, or crossing the middle. Time complexity is O(n log n).

Closest Pair of Points

Finding the closest pair of points in 2D plane using divide and conquer achieves O(n log n) complexity, better than the brute force O(nΒ²) approach.

Closest Pair of Points

#include <iostream>
#include <vector>
#include <algorithm>
#include <cmath>
#include <float.h>
using namespace std;

struct Point {
    double x, y;
    
    Point(double x = 0, double y = 0) : x(x), y(y) {}
    
    double distanceTo(const Point& other) const {
        return sqrt((x - other.x) * (x - other.x) + (y - other.y) * (y - other.y));
    }
};

class ClosestPair {
public:
    double findClosestPair(vector<Point>& points) {
        // Sort points by x-coordinate
        sort(points.begin(), points.end(), [](const Point& a, const Point& b) {
            return a.x < b.x;
        });
        
        return closestPairRec(points, 0, points.size() - 1);
    }
    
private:
    double closestPairRec(vector<Point>& points, int left, int right) {
        int n = right - left + 1;
        
        // Base case: brute force for small arrays
        if (n <= 3) {
            return bruteForce(points, left, right);
        }
        
        int mid = left + (right - left) / 2;
        Point midPoint = points[mid];
        
        // Recursively find closest pairs in left and right halves
        double leftMin = closestPairRec(points, left, mid);
        double rightMin = closestPairRec(points, mid + 1, right);
        
        double minDist = min(leftMin, rightMin);
        
        // Find points close to the dividing line
        vector<Point> strip;
        for (int i = left; i <= right; i++) {
            if (abs(points[i].x - midPoint.x) < minDist) {
                strip.push_back(points[i]);
            }
        }
        
        // Sort strip by y-coordinate
        sort(strip.begin(), strip.end(), [](const Point& a, const Point& b) {
            return a.y < b.y;
        });
        
        // Find closest points in strip
        return min(minDist, stripClosest(strip, minDist));
    }
    
    double bruteForce(vector<Point>& points, int left, int right) {
        double minDist = DBL_MAX;
        for (int i = left; i <= right; i++) {
            for (int j = i + 1; j <= right; j++) {
                minDist = min(minDist, points[i].distanceTo(points[j]));
            }
        }
        return minDist;
    }
    
    double stripClosest(vector<Point>& strip, double minDist) {
        double min_val = minDist;
        
        for (int i = 0; i < strip.size(); i++) {
            // Check at most 7 points (proven geometrically)
            for (int j = i + 1; j < strip.size() && 
                 (strip[j].y - strip[i].y) < min_val; j++) {
                min_val = min(min_val, strip[i].distanceTo(strip[j]));
            }
        }
        
        return min_val;
    }
};

int main() {
    vector<Point> points = {
        Point(2, 3), Point(12, 30), Point(40, 50), Point(5, 1),
        Point(12, 10), Point(3, 4), Point(1, 1), Point(8, 7)
    };
    
    cout << "Points:" << endl;
    for (int i = 0; i < points.size(); i++) {
        cout << "(" << points[i].x << ", " << points[i].y << ")" << endl;
    }
    
    ClosestPair cp;
    double closestDistance = cp.findClosestPair(points);
    
    cout << "Closest pair distance: " << closestDistance << endl;
    
    return 0;
}

The algorithm divides points by x-coordinate, finds closest pairs in each half, then checks points near the dividing line. The key insight is that we only need to check at most 7 points in the strip.

Key Takeaways

  • Divide and conquer breaks problems into smaller subproblems, solves them recursively, and combines results
  • Time complexity often follows the recurrence T(n) = aT(n/b) + f(n), analyzed using Master Theorem
  • Common applications: sorting algorithms, closest pair, maximum subarray, fast multiplication
  • Often provides optimal solutions with elegant recursive structure