ScholarQuill logoScholarQuillUniversity Notes
  • Notes
  • Past Papers
  • Blogs
  • Todo
Login
ScholarQuill logoScholarQuillUniversity Notes
Login
NotesPast PapersBlogsTodo
More
SubjectsDiscussionCGPA CalculatorGPA CalculatorStudent PortalCourse Outline
About
About usPrivacy PolicyReportContact
Notes
Past Papers
Blogs
Todo
Analytics
    Current Subject
    🧩
    Design & Analysis of Algorithms
    DC-321
    Progress0 / 28 topics
    Topics
    1. Introduction2. Role of Algorithms in Computing3. Analysis on Nature of Input and Size of Input4. Asymptotic Notations5. Big-O Notation6. Big-Ω Notation7. Big-Θ Notation8. Little-o Notation9. Little-ω Notation10. Sorting Algorithm Analysis11. Loop Invariants12. Recursion and Recurrence Relations13. Algorithm Design Techniques14. Brute Force Approach15. Divide-and-Conquer Approach16. Merge Sort17. Quick Sort18. Greedy Approach19. Dynamic Programming20. Elements of Dynamic Programming21. Search Trees22. Heaps23. Hashing24. Graph Algorithms25. Shortest Paths26. Sparse Graphs27. String Matching28. Introduction to Complexity Classes
    DC-321›Divide-and-Conquer Approach
    Design & Analysis of AlgorithmsTopic 15 of 28

    Divide-and-Conquer Approach

    8 minread
    1,282words
    Intermediatelevel

    Divide-and-Conquer Approach

    The Divide-and-Conquer (D&C) approach is a fundamental algorithm design paradigm where a problem is broken down into smaller subproblems, solved independently, and then combined to produce the solution to the original problem. This technique is especially useful for problems that can be recursively divided into smaller subproblems, which can be solved more easily and then combined.

    Divide-and-conquer is commonly used in many well-known algorithms like Merge Sort, Quick Sort, and Binary Search.


    Key Steps of the Divide-and-Conquer Approach:

    1. Divide: Split the problem into smaller subproblems. The subproblems should be smaller instances of the same problem (recursively defined).
    2. Conquer: Solve each of the subproblems independently. If the subproblem is small enough, solve it directly. Otherwise, recursively apply the divide-and-conquer technique to break it down further.
    3. Combine: Merge or combine the solutions of the subproblems to form the final solution to the original problem.

    General Structure of Divide-and-Conquer Algorithms:

    1. Recursion: Divide the problem into smaller instances and solve them recursively.
    2. Combination of Results: After solving the subproblems, combine the results in an efficient manner to obtain the final solution.

    Example 1: Merge Sort

    Merge Sort is a classic example of the divide-and-conquer paradigm. The algorithm divides an array into two halves, recursively sorts each half, and then merges the sorted halves to get the final sorted array.

    Steps:

    1. Divide: Split the array into two halves.
    2. Conquer: Recursively sort each half.
    3. Combine: Merge the two sorted halves to produce a fully sorted array.

    Algorithm (Merge Sort):

    def merge_sort(arr):
        if len(arr) <= 1:
            return arr
        
        # Divide: Find the middle point and divide the array into two halves
        mid = len(arr) // 2
        left = merge_sort(arr[:mid])  # Recursively sort the left half
        right = merge_sort(arr[mid:])  # Recursively sort the right half
        
        # Combine: Merge the sorted halves
        return merge(left, right)
    
    def merge(left, right):
        sorted_arr = []
        i = j = 0
        
        # Merge two sorted arrays
        while i < len(left) and j < len(right):
            if left[i] < right[j]:
                sorted_arr.append(left[i])
                i += 1
            else:
                sorted_arr.append(right[j])
                j += 1
        
        # Append remaining elements (if any)
        sorted_arr.extend(left[i:])
        sorted_arr.extend(right[j:])
        
        return sorted_arr
    

    Time Complexity:

    • Divide step takes O(log⁡n)O(\log n)O(logn) as we divide the array in half repeatedly.
    • Conquer step involves recursively sorting each subarray, and this contributes to a total time complexity of O(nlog⁡n)O(n \log n)O(nlogn).
    • Combine step takes O(n)O(n)O(n) to merge two sorted arrays.

    So, the overall time complexity of Merge Sort is O(n log n).


    Example 2: Quick Sort

    Quick Sort is another well-known algorithm that uses the divide-and-conquer strategy. It picks a pivot element from the array, partitions the array into two subarrays (those smaller and greater than the pivot), and then recursively sorts the subarrays.

    Steps:

    1. Divide: Choose a pivot element and partition the array such that elements smaller than the pivot are on one side, and elements greater than the pivot are on the other side.
    2. Conquer: Recursively sort the subarrays on either side of the pivot.
    3. Combine: Since the array is being sorted in place, no explicit combination is needed. The subarrays will be sorted in place.

    Algorithm (Quick Sort):

    def quick_sort(arr):
        if len(arr) <= 1:
            return arr
        
        pivot = arr[len(arr) // 2]  # Choose the middle element as pivot
        left = [x for x in arr if x < pivot]
        middle = [x for x in arr if x == pivot]
        right = [x for x in arr if x > pivot]
        
        # Recursively sort the left and right subarrays
        return quick_sort(left) + middle + quick_sort(right)
    

    Time Complexity:

    • The time complexity depends on how well the pivot divides the array. In the best and average case, the pivot splits the array into two roughly equal halves, leading to a time complexity of O(n log n).
    • In the worst case, when the pivot does not divide the array evenly (e.g., when the array is already sorted), the time complexity becomes O(n²).

    Example 3: Binary Search

    Binary Search is a simple and efficient divide-and-conquer algorithm used to search for an element in a sorted array. The algorithm works by repeatedly dividing the search interval in half.

    Steps:

    1. Divide: Calculate the middle index of the current search interval.
    2. Conquer: If the middle element is equal to the target, return the index. If the target is smaller than the middle element, search the left half; otherwise, search the right half.
    3. Combine: Since the result is found immediately, no combining is necessary.

    Algorithm (Binary Search):

    def binary_search(arr, target):
        low, high = 0, len(arr) - 1
        while low <= high:
            mid = (low + high) // 2
            if arr[mid] == target:
                return mid
            elif arr[mid] < target:
                low = mid + 1
            else:
                high = mid - 1
        return -1  # Target not found
    

    Time Complexity:

    • Divide step reduces the problem size by half in each iteration.
    • The search is complete when the size of the search interval is reduced to 1.
    • Time complexity is O(log⁡n)O(\log n)O(logn) for both the best, average, and worst case, as the array is halved at each step.

    Example 4: Strassen's Matrix Multiplication

    Strassen's Matrix Multiplication is a divide-and-conquer algorithm for matrix multiplication that reduces the number of multiplications required.

    Instead of performing standard matrix multiplication, Strassen divides each matrix into smaller submatrices, recursively multiplies them, and combines the results.

    Steps:

    1. Divide: Divide the matrices into smaller submatrices.
    2. Conquer: Perform matrix multiplications on these smaller submatrices.
    3. Combine: Combine the results to obtain the final product.

    Time Complexity:

    • The standard matrix multiplication takes O(n3)O(n^3)O(n3), but Strassen's algorithm reduces it to O(nlog⁡27)≈O(n2.81)O(n^{\log_2 7}) \approx O(n^{2.81})O(nlog2​7)≈O(n2.81), which is more efficient for large matrices.

    Advantages of Divide-and-Conquer:

    1. Efficiency: It often leads to algorithms that have better time complexities compared to simpler techniques like brute force.
    2. Parallelism: Since subproblems are independent, divide-and-conquer algorithms can be parallelized, making them suitable for distributed computing.
    3. Optimal Substructure: Problems that can be divided into smaller subproblems often have an optimal substructure, making divide-and-conquer a natural fit for these kinds of problems.

    Disadvantages of Divide-and-Conquer:

    1. Recursive Overhead: Recursive calls introduce overhead, such as additional memory usage for the call stack.
    2. Complexity: Some divide-and-conquer algorithms, like Strassen’s matrix multiplication, can be difficult to implement and understand.
    3. Suboptimal for Certain Problems: Not all problems can be divided into smaller subproblems efficiently. Some problems may not benefit from divide-and-conquer and may require other approaches.

    Summary

    The Divide-and-Conquer approach is a powerful technique used to solve problems by recursively breaking them into smaller, more manageable subproblems, solving each subproblem, and then combining the results. This technique is widely used in algorithms like Merge Sort, Quick Sort, and Binary Search. While it often results in efficient solutions, it can also introduce recursive overhead and complexity in some cases. Divide-and-conquer is particularly useful when the problem can be broken down into independent subproblems, and the subproblems are easier to solve than the original problem.

    Previous topic 14
    Brute Force Approach
    Next topic 16
    Merge Sort

    Past Papers

    Open this section to load past papers

    Click on Show Past Papers to see past papers.
    On This Page
      Reading Stats
      Est. reading time8 min
      Word count1,282
      Code examples0
      DifficultyIntermediate