ALGORITHMS: Understanding Divide and Conquer Technique

ALGORITHMS: Understanding Divide and Conquer Technique

Introduction

Divide and conquer is a well-known recursion technique for solving problems. It solves a problem by breaking it into smaller, easier-to-solve problems, solving each problem separately and combining their solutions to address the original problem. Think of Divide and Conquer as efficiently cleaning a huge, cluttered home. You break the task down by cleaning the rooms independently and then combining everything together to create a perfectly organized home.

Prerequisites

To understand Divide and Conquer, it is critical to have a good grasp of some fundamental programming concepts. You should be comfortable working with functions, as well as loops and conditionals which are used to control program flow. A basic understanding of recursion is also essential. Additionally, you should be familiar with arrays because this knowledge will help you work with algorithms that work by splitting and combining data. A solid understanding of algorithm analysis and design is important.

Key Concepts

  • Divide: Break the main problem into smaller, more solvable subproblems. For Example, splitting an array into two halves when sorting.

  • Conquer: Solve each of the easier-to-solve subproblems. This is usually done by applying recursion. For example, sorting each half of the array recursively in QuickSort.

  • Base Case: This is the simplest possible version of the problem that can be solved directly without further division. For example, when the array has only one element, it is sorted

  • Combine: Add the solutions of the smaller problems to form the solution to the original problem. For example, merging the sorted halves of the array into one fully sorted array.

💡 If you are using divide and conquer on an array. The base case is probably an empty array or an array with one element.

Imagine you are preparing a multi-course meal. First, you separate the ingredients for each dish (divide), then you cook each dish individually in a pot or pan (conquer), ensuring each dish is perfectly cooked before stopping (base case), and finally, you plate everything together for a complete, delicious meal (combine).

How Divide and Conquer Works

To solve a problem using the divide and conquer technique there are four steps:

  • Figure out the base case: You are to identify the simplest possible case that can be solved directly without dividing it further.

  • Divide the problem: Decrease or Break the original problem into smaller, independent subproblems.

  • Conquer the subproblems: Solve each of the smaller subproblems. This is usually done using recursion.

  • Combine the results: Merge the solutions of the subproblems to solve the original problem.

Example

Remember the first algorithm we looked at in this series? Binary search algorithm. In the implementation of the algorithm, the example I used was written with an iterative approach.

How Binary Search Uses Divide And Conquer

  1. Base Case:

    • If the array is empty or the low pointer is greater than the high pointer, the target does not exist in the array.

    • If the middle element matches the target, return its index.

  2. Divide:

    • Split the array into two halves by calculating the middle index.
  3. Conquer:

    • Compare the target value with the middle element:

      • If the target is smaller, search the left half of the array.

      • if the target is larger, search the right half of the array.

  4. Combine: In binary search, the index of the target is directly returned as the recursion unwinds. Hence, no need for a separate combining step.

Code implementation: (use an example with more than 2 steps)

const binary_search = (array, target, low = 0, high = array.length - 1) => {

    if (low > high) return -1

    const mid = Math.floor((low + high) / 2)

    if (array[mid] === target) {
        return mid;
    } else if (array[mid] > target) {
        return binary_search(array, target, low, mid -1)
    } else {
        return binary_search(array, target, mid + 1, high)
    }
}

const array1 = [1, 3, 5, 7, 9, 29, 35, 39, 62];
console.log(binary_search(array1, 7)); // Output: 3
console.log(binary_search(array1, 2)); // Output: -1

Step-by-Step Walkthrough

Input:

array: [1, 3, 5, 7, 9, 29, 35, 39, 62] , target: 7

when you see x in an array below, it means that element is not part of the currently searched.

  1. Initial Call:

    • low = 0 , high = 8 (length of array - 1)

    • mid = Math.floor((0 + 8) / 2) = 4

    • array[mid] = array[4] = 9.

    • Since array[mid] > target (9 > 7)

      • The function is called recursively with low=0 and high = (mid - 1) = 3

      • search the left half of the array

      • current search space: [1, 3, 5, 7, 9, x, x, x, x]

  2. Second call:

    • low = 0, high = 4

    • mid = Math.floor((0 + 4) / 2) = 2

    • array[mid] = array[2] = 5.

    • Since array[mid] < target (5 < 7)

      • The function is called recursively with low = (mid + 1) = 3 and high = 3

      • search the right half of the array

      • current search space: [x, x, x, 7, 9, x, x, x, x]

  3. Third call:

    • low = 3, high = 4

    • mid = Math.floor((3 + 4) / 2) = 4

    • array[mid] = array[4] = 9.

    • Since array[mid] > target (9 > 7)

      • The function is called recursively with low = 3 and high = (mid - 1) = 3

      • search the left half of the array

      • current search space: [x, x, x, 7, x, x, x, x, x]

  4. Final call:

  • low = 3, high = 3

  • mid = Math.floor((3 + 3) / 2) = 3

  • array[mid] = array[3] = 7.

  • Since array[mid] === target (7 === 7)

    • return array[mid] which is 3

    • we have found the target

Advantages of Divide and Conquer

The strength of the divide and conquer technique lies in:

  1. Simplifying Problem-Solving: Divide and conquer solves problems by breaking them into smaller, more solvable subproblems. Making it easier to tackle

  2. Improving Efficiency: Algorithms like QuickSort, MergeSort and binary search can achieve near-optimal time complexities. For example, MergeSort has a time complexity of O(nlogn), this is way faster than simple iterative sorting methods like selection sort which has a time complexity of O(n²).

  3. Enabling Parallel Processing: Independent subproblems can be solved in parallel. Divide and Conquer leverages multi-core processors to improve performance.

Common Algorithms Based on Divide and Conquer

  • Binary Search Algorithm: This algorithm finds the target element in an already sorted array by dividing the original array in half at each step. Binary search has a time complexity of (O(logn)) as opposed to O(n) time complexity of simple search.

  • Quick Sort Algorithm: This sorting algorithm sorts an array by partitioning it around a pivot element. Then recursively sort the subarrays on either side of the partition. Quick sort algorithm has a time complexity of (O(logn)) on average, as opposed to the O(n²) time complexity of iterative sorting algorithms like Selection sort and Insertion sort.

  • Merge Sort Algorithm: This is another sorting algorithm algorithm that can be implemented with divide and conquer. Merge sort sorts an array by dividing it into two halves, sorting each half recursively and merging them back together. Merge sort has a time complexity of (O(logn)) as opposed to O(n²) time complexity of iterative sorting algorithms like Selection sort and Insertion sort.

Challenges and Pitfalls of Divide and Conquer

As with many techniques in computer science, there are trade-offs involved. The Divide and Conquer pattern is no exception.

  • Overhead Of Recursive Calls: Divide and Conquer solutions built using recursion can incur additional memory overhead due to the function call stack. This may lead to inefficiency, particularly with deep recursion or large inputs.

  • Complexity of Combining Solutions: The process of combining the solutions of subproblems can be be complex and resource-heavy, potentially slowing down the algorithm.

💡 Divide and conquer is less suitable if the subproblems are not independent or if the problem cannot be broken down recursively.

When to Use Divide and Conquer

Divide and Conquer pattern can be used when:

  • The problems can be broken down into smaller subproblems that are easier to solve. For example, Merge Sort divides arrays into two halves, sorts each half independently, and then combines them.

  • The solution to the problem requires a structured approach to reduce redundant computations or search through large datasets. For example, Binary search optimizes search by repeatedly dividing the search space in half.

Conclusion

Divide and Conquer is a problem-solving strategy that breaks a problem into smaller, easier-to-solve problems, solves them independently and combines their results. This approach to problem-solving improves computational efficiency. Divide and Conquer is ideal for problems that can be broken into smaller, independent tasks. It excels at solving problems that require optimization, such as sorting algorithms like QuickSort and search algorithms like Binary search because breaking down the problem allows for faster, more structured solutions.
To have a better understanding of the Divide and Conquer technique, start by implementing some classic Divide and Conquer algorithms like Quick Sort, and meticulously visualize how each problem is divided, solved individually, and then elegantly combined. Doing this will help you better understand how breaking down complex problems can lead to more elegant, efficient solutions for several computational problems.
Try implementing a divide-and-conquer algorithm such as Quick Sort to have a better grasp of the concept.