Skip to main content

Unit - 2

TITLE

Divide & Conquer Algorithms

1. Introduction

1.1 Overview

  • When faced with a large problem without an immediate overall solution, a common approach is to take a part of it and solve it.
  • If the partial solution works, it can be applied to the remaining parts of the larger problem.
  • Example: To plant a 10,00010,000 sq. ft farm, if one person can plant 250250 sq. ft/hr, you need 1010 people to complete the entire farm within 44 hours.

2. Recurrence Equations

2.1 Definition

  • Many algorithms, particularly Divide and Conquer, are recursive in nature.
  • When analyzing their time complexity, we derive a recurrence relation.
  • A recurrence relation expresses the running time as a function of the input size nn in terms of the running time on inputs of smaller sizes.
  • It is a recursive description of a function.

3. Methods to Solve Recurrences

3.1 Common Methods

  • Substitution Method
  • Homogeneous (characteristic equation)
  • Inhomogeneous
  • Master Method
  • Recurrence Tree Method
  • Intelligent guesswork
  • Change of variable
  • Range transformations

4. Substitution Method

4.1 Concept

  • Make a guess for the solution and then use mathematical induction to prove whether the guess is correct or incorrect.
  • Example: T(n)=T(n1)+nT(n) = T(n-1) + n
    • Replacing nn with n1n-1 and n2n-2:
      • T(n1)=T(n2)+n1T(n-1) = T(n-2) + n - 1
      • T(n2)=T(n3)+n2T(n-2) = T(n-3) + n - 2
    • Substituting back:
      • T(n)=T(n3)+n2+n1+nT(n) = T(n-3) + n - 2 + n - 1 + n
    • Generalizing for kk steps:
      • T(n)=T(nk)+(nk+1)+(nk+2)++nT(n) = T(n-k) + (n-k+1) + (n-k+2) + \dots + n
    • If we take k=nk = n:
      • T(n)=T(0)+1+2++nT(n) = T(0) + 1 + 2 + \dots + n
      • T(n)=n(n+1)2=O(n2)T(n) = \frac{n(n+1)}{2} = O(n^2)

5. Master Method

5.1 Overview

  • The Master Method is a "cookbook" method for solving recurrences of the form: T(n)=aT(n/b)+f(n)T(n) = aT(n/b) + f(n)
  • aa: Number of sub-problems.
  • n/bn/b: Size of each sub-problem.
  • f(n)f(n): Time required to divide the problem and recombine the results.

5.2 Three Cases

  1. Case 1: If f(n)nlogbaf(n) \le n^{\log_b a} (specifically f(n)f(n) is in O(nlogbaϵ)O(n^{\log_b a - \epsilon}))
    • Then T(n)=Θ(nlogba)T(n) = \Theta(n^{\log_b a})
  2. Case 2: If f(n)=nlogbaf(n) = n^{\log_b a} (specifically f(n)f(n) is in Θ(nlogba)\Theta(n^{\log_b a}))
    • Then T(n)=Θ(nlogbalogn)T(n) = \Theta(n^{\log_b a} \log n)
  3. Case 3: If f(n)nlogbaf(n) \ge n^{\log_b a} (specifically f(n)f(n) is in Ω(nlogba+ϵ)\Omega(n^{\log_b a + \epsilon}))
    • Then T(n)=Θ(f(n))T(n) = \Theta(f(n))

5.3 Examples

  • Merge Sort: T(n)=2T(n/2)+Θ(n)T(n) = 2T(n/2) + \Theta(n)
    • a=2,b=2    nlog22=n1=na=2, b=2 \implies n^{\log_2 2} = n^1 = n. Here f(n)=Θ(n)f(n) = \Theta(n).
    • Case 2 applies: T(n)=Θ(nlogn)T(n) = \Theta(n \log n)
  • Binary Search: T(n)=T(n/2)+Θ(1)T(n) = T(n/2) + \Theta(1)
    • a=1,b=2    nlog21=n0=1a=1, b=2 \implies n^{\log_2 1} = n^0 = 1. Here f(n)=Θ(1)f(n) = \Theta(1).
    • Case 2 applies: T(n)=Θ(logn)T(n) = \Theta(\log n)

6. Recurrence Tree Method

6.1 Concept

  • Each node in the tree represents the cost of a single sub-problem.
  • We sum the costs across each level of the tree to obtain per-level costs.
  • Finally, sum all per-level costs to determine the total cost of the recursion.
  • Example for T(n)=2T(n/2)+nT(n) = 2T(n/2) + n:
    • Level 0 cost: nn
    • Level 1 cost: n/2+n/2=nn/2 + n/2 = n
    • Total levels: log2n\log_2 n
    • Total cost: i=0log2n1n+2log2nT(1)=nlogn+n=O(nlogn)\sum_{i=0}^{\log_2 n - 1} n + 2^{\log_2 n} T(1) = n \log n + n = O(n \log n)

7. Divide & Conquer (D&C) Technique

7.1 Three Steps

  1. Divide: Break the problem into several smaller sub-problems similar to the original problem.
  2. Conquer: Solve the sub-problems recursively. If they are small enough, solve them in a straightforward (base case) manner.
  3. Combine: Merge the sub-problem solutions to create the solution for the original problem.

7.2 Running Time Analysis

  • The total time T(n)T(n) is generally T(n)=aT(n/b)+f(n)T(n) = a T(n/b) + f(n).

8. Multiplying Large Integers

8.1 Problem Statement

  • Multiplying two nn-digit large integers using divide and conquer.
  • Example: 981×1234981 \times 1234
  • Splitting into halves:
    • w=09,x=81,y=12,z=34w=09, x=81, y=12, z=34
    • 981=102w+x981 = 10^2 w + x
    • 1234=102y+z1234 = 10^2 y + z

8.2 Standard vs. Optimized

  • Standard approach requires 4 multiplications: wyw \cdot y, wzw \cdot z, xyx \cdot y, xzx \cdot z.
  • Karatsuba optimization reduces it to 3 multiplications:
    • p=wyp = w \cdot y
    • q=xzq = x \cdot z
    • r=(w+x)(y+z)r = (w+x)(y+z)
    • The middle term wz+xy=rpqw \cdot z + x \cdot y = r - p - q.
  • Time Complexity: Reduces from O(n2)O(n^2) to O(nlog23)O(n1.585)O(n^{\log_2 3}) \approx O(n^{1.585}).

9.1 Concept

  • Finds an element xx in a sorted array T[1n]T[1 \dots n].
  • Compares xx with the midpoint. If x<midx < \text{mid}, search the left half. If x>midx > \text{mid}, search the right half.

9.2 Step-by-Step Explanation

  1. Initialize two pointers, left at the start (0) and right at the end (n1n-1) of the array.
  2. Loop while left is less than or equal to right.
  3. Calculate Midpoint: Find the middle index mid = left + (right - left) / 2.
  4. Compare:
    • If the element at mid equals xx, return mid.
    • If the element at mid is less than xx, xx must be in the right half, so set left = mid + 1.
    • If the element at mid is greater than xx, xx must be in the left half, so set right = mid - 1.
  5. Not Found: If the loop ends without returning, xx is not in the array. Return -1.

9.3 Algorithm Complexity

  • Recurrence: T(n)=T(n/2)+Θ(1)T(n) = T(n/2) + \Theta(1)
  • Time Complexity:
    • Best Case: O(1)O(1)
    • Average & Worst Case: O(logn)O(\log n)
  • Space Complexity: O(1)O(1) for iterative, O(logn)O(\log n) for recursive due to call stack.

9.4 Implementation

int binarySearch(int arr[], int left, int right, int x) {
while (left <= right) {
int mid = left + (right - left) / 2;
if (arr[mid] == x) return mid;
if (arr[mid] < x) left = mid + 1;
else right = mid - 1;
}
return -1;
}

10. Merge Sort

10.1 Concept

  • Divides the unsorted list into nn sub-lists of 11 element each.
  • Repeatedly merges adjacent sub-lists to produce new sorted sub-lists until only 11 sorted list remains.

10.2 Step-by-Step Explanation

  1. Divide: Check if the array has more than 1 element. If so, find the middle point to divide the array into two halves, L (left) and R (right).
  2. Conquer: Recursively call mergeSort on the first half L and then on the second half R.
  3. Combine (Merge):
    • Initialize three pointers: ii (for L), jj (for R), and kk (for the original array).
    • Compare elements L[i] and R[j]. Place the smaller element into the original array at index kk and increment the respective pointer.
    • Once either L or R is exhausted, copy the remaining elements of the other half into the original array.

10.3 Algorithm Complexity

  • Separating takes linear time; merging takes linear time.
  • Recurrence: T(n)=2T(n/2)+Θ(n)T(n) = 2T(n/2) + \Theta(n)
  • Time Complexity: Θ(nlogn)\Theta(n \log n) for all cases (Best, Average, Worst).
  • Space Complexity: O(n)O(n) because it requires an auxiliary array for merging.

10.4 Implementation

void merge(int arr[], int l, int m, int r) {
int n1 = m - l + 1, n2 = r - m;
int L[n1], R[n2];
for (int i = 0; i < n1; i++) L[i] = arr[l + i];
for (int j = 0; j < n2; j++) R[j] = arr[m + 1 + j];

int i = 0, j = 0, k = l;
while (i < n1 && j < n2) {
if (L[i] <= R[j]) arr[k++] = L[i++];
else arr[k++] = R[j++];
}
while (i < n1) arr[k++] = L[i++];
while (j < n2) arr[k++] = R[j++];
}

void mergeSort(int arr[], int l, int r) {
if (l >= r) return;
int m = l + (r - l) / 2;
mergeSort(arr, l, m);
mergeSort(arr, m + 1, r);
merge(arr, l, m, r);
}

11. Quick Sort

11.1 Concept

  • Chooses a pivot element.
  • Partitions the array so elements less than the pivot move to the left, and elements greater move to the right.
  • Recursively applies the same logic to the left and right partitions.

11.2 Step-by-Step Explanation

  1. Choose Pivot: Select an element from the array to act as the pivot (often the last element).
  2. Partition: Rearrange the array so that all elements smaller than the pivot are on its left, and all elements greater are on its right. The pivot is now in its final sorted position.
    • Maintain a pointer ii to track the boundary of elements smaller than the pivot.
    • Iterate through the array with jj; if an element is smaller than the pivot, increment ii and swap elements at ii and jj.
    • Finally, swap the pivot with the element at i+1i+1.
  3. Recursion: Recursively apply the above steps to the sub-array of elements smaller than the pivot, and the sub-array of elements greater than the pivot.

11.3 Algorithm Complexity

  • Worst Case: The partition produces one sub-array of n1n-1 elements and one of 00 elements (e.g., array is already sorted).
    • Recurrence: T(n)=T(n1)+Θ(n)T(n) = T(n-1) + \Theta(n)
    • Time Complexity: Θ(n2)\Theta(n^2)
  • Best / Average Case: Partition produces two roughly equal halves.
    • Recurrence: T(n)=2T(n/2)+Θ(n)T(n) = 2T(n/2) + \Theta(n)
    • Time Complexity: Θ(nlogn)\Theta(n \log n)
  • Space Complexity: O(logn)O(\log n) due to the recursive call stack (if tail-call optimized), worst-case O(n)O(n) if heavily unbalanced.

11.4 Implementation

int partition(int arr[], int low, int high) {
int pivot = arr[high];
int i = (low - 1);
for (int j = low; j <= high - 1; j++) {
if (arr[j] < pivot) {
i++;
std::swap(arr[i], arr[j]);
}
}
std::swap(arr[i + 1], arr[high]);
return (i + 1);
}

void quickSort(int arr[], int low, int high) {
if (low < high) {
int pi = partition(arr, low, high);
quickSort(arr, low, pi - 1);
quickSort(arr, pi + 1, high);
}
}

12. Matrix Multiplication (Strassen's Algorithm)

12.1 Concept

  • Multiplying two 2×22 \times 2 matrices traditionally requires 8 scalar multiplications.
  • For n×nn \times n matrices, classic complexity is Θ(n3)\Theta(n^3).
  • Strassen's Algorithm uses algebraic tricks to compute the product using only 7 scalar multiplications for a 2×22 \times 2 block.

12.2 Step-by-Step Explanation

  1. Divide: Split the input matrices AA and BB into 4 sub-matrices of size (n/2×n/2)(n/2 \times n/2).
  2. Compute 7 Products: Using specific algebraic combinations of these sub-matrices (involving additions and subtractions), compute 7 intermediate matrix products (P1P_1 to P7P_7) instead of the usual 8.
  3. Combine: Use additions and subtractions of the P1P7P_1 \dots P_7 matrices to form the 4 sub-matrices of the final result matrix CC.
  4. Recursion: If the sub-matrices are larger than 1×11 \times 1, recursively apply Strassen's algorithm to compute the 7 products.

12.3 Algorithm Complexity

  • Recurrence: T(n)=7T(n/2)+Θ(n2)T(n) = 7T(n/2) + \Theta(n^2)
  • Using Master Method (Case 3): T(n)=Θ(nlog27)Θ(n2.81)T(n) = \Theta(n^{\log_2 7}) \approx \Theta(n^{2.81})

13. Exponentiation

13.1 Sequential Approach

  • Compute x=anx = a^n by multiplying aa by itself n1n-1 times.
  • Loop executes n1n-1 times: r = a * r
  • Total time (considering large numbers): T(m,n)Θ(m2n2)T(m,n) \in \Theta(m^2 n^2) where mm is the size of the operand.

13.2 Divide & Conquer Approach

  • an=(an/2)2a^n = (a^{n/2})^2 if nn is even.
  • an=a×an1a^n = a \times a^{n-1} if nn is odd.
  • Recurrence: N(n)=N(n/2)+1N(n) = N(n/2) + 1 (if even)
  • Total time (considering large numbers): T(m,n)Θ(mlog23nlog23)T(m,n) \in \Theta(m^{\log_2 3} n^{\log_2 3})

13.3 Step-by-Step Explanation (D&C)

  1. Base Case: If n=0n = 0, return 1. If n=1n = 1, return aa.
  2. Divide: Calculate the exponent for half the power, recursively calling the function for an/2a^{n/2}.
  3. Combine:
    • If nn is even: Square the result of an/2a^{n/2} (i.e., return an/2×an/2a^{n/2} \times a^{n/2}).
    • If nn is odd: Square the result of an/2a^{n/2} and multiply by an extra aa (i.e., return a×an/2×an/2a \times a^{n/2} \times a^{n/2}).