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 sq. ft farm, if one person can plant sq. ft/hr, you need people to complete the entire farm within 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 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:
- Replacing with and :
- Substituting back:
- Generalizing for steps:
- If we take :
- Replacing with and :
5. Master Method
5.1 Overview
- The Master Method is a "cookbook" method for solving recurrences of the form:
- : Number of sub-problems.
- : Size of each sub-problem.
- : Time required to divide the problem and recombine the results.
5.2 Three Cases
- Case 1: If (specifically is in )
- Then
- Case 2: If (specifically is in )
- Then
- Case 3: If (specifically is in )
- Then
5.3 Examples
- Merge Sort:
- . Here .
- Case 2 applies:
- Binary Search:
- . Here .
- Case 2 applies:
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 :
- Level 0 cost:
- Level 1 cost:
- Total levels:
- Total cost:
7. Divide & Conquer (D&C) Technique
7.1 Three Steps
- Divide: Break the problem into several smaller sub-problems similar to the original problem.
- Conquer: Solve the sub-problems recursively. If they are small enough, solve them in a straightforward (base case) manner.
- Combine: Merge the sub-problem solutions to create the solution for the original problem.
7.2 Running Time Analysis
- The total time is generally .
8. Multiplying Large Integers
8.1 Problem Statement
- Multiplying two -digit large integers using divide and conquer.
- Example:
- Splitting into halves:
8.2 Standard vs. Optimized
- Standard approach requires 4 multiplications: , , , .
- Karatsuba optimization reduces it to 3 multiplications:
- The middle term .
- Time Complexity: Reduces from to .
9. Binary Search
9.1 Concept
- Finds an element in a sorted array .
- Compares with the midpoint. If , search the left half. If , search the right half.
9.2 Step-by-Step Explanation
- Initialize two pointers,
leftat the start (0) andrightat the end () of the array. - Loop while
leftis less than or equal toright. - Calculate Midpoint: Find the middle index
mid = left + (right - left) / 2. - Compare:
- If the element at
midequals , returnmid. - If the element at
midis less than , must be in the right half, so setleft = mid + 1. - If the element at
midis greater than , must be in the left half, so setright = mid - 1.
- If the element at
- Not Found: If the loop ends without returning, is not in the array. Return -1.
9.3 Algorithm Complexity
- Recurrence:
- Time Complexity:
- Best Case:
- Average & Worst Case:
- Space Complexity: for iterative, for recursive due to call stack.
9.4 Implementation
- C++
- Python
- Java
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;
}
def binary_search(arr, x):
left, right = 0, len(arr) - 1
while left <= right:
mid = left + (right - left) // 2
if arr[mid] == x: return mid
if arr[mid] < x: left = mid + 1
else: right = mid - 1
return -1
int binarySearch(int arr[], int x) {
int left = 0, right = arr.length - 1;
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 sub-lists of element each.
- Repeatedly merges adjacent sub-lists to produce new sorted sub-lists until only sorted list remains.
10.2 Step-by-Step Explanation
- 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) andR(right). - Conquer: Recursively call
mergeSorton the first halfLand then on the second halfR. - Combine (Merge):
- Initialize three pointers: (for
L), (forR), and (for the original array). - Compare elements
L[i]andR[j]. Place the smaller element into the original array at index and increment the respective pointer. - Once either
LorRis exhausted, copy the remaining elements of the other half into the original array.
- Initialize three pointers: (for
10.3 Algorithm Complexity
- Separating takes linear time; merging takes linear time.
- Recurrence:
- Time Complexity: for all cases (Best, Average, Worst).
- Space Complexity: because it requires an auxiliary array for merging.
10.4 Implementation
- C++
- Python
- Java
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);
}
def merge_sort(arr):
if len(arr) > 1:
mid = len(arr) // 2
L = arr[:mid]
R = arr[mid:]
merge_sort(L)
merge_sort(R)
i = j = k = 0
while i < len(L) and j < len(R):
if L[i] <= R[j]:
arr[k] = L[i]
i += 1
else:
arr[k] = R[j]
j += 1
k += 1
while i < len(L):
arr[k] = L[i]
i += 1
k += 1
while j < len(R):
arr[k] = R[j]
j += 1
k += 1
void merge(int arr[], int l, int m, int r) {
int n1 = m - l + 1;
int n2 = r - m;
int L[] = new int[n1];
int R[] = new int[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];
i++;
} else {
arr[k] = R[j];
j++;
}
k++;
}
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) {
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
- Choose Pivot: Select an element from the array to act as the pivot (often the last element).
- 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 to track the boundary of elements smaller than the pivot.
- Iterate through the array with ; if an element is smaller than the pivot, increment and swap elements at and .
- Finally, swap the pivot with the element at .
- 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 elements and one of elements (e.g., array is already sorted).
- Recurrence:
- Time Complexity:
- Best / Average Case: Partition produces two roughly equal halves.
- Recurrence:
- Time Complexity:
- Space Complexity: due to the recursive call stack (if tail-call optimized), worst-case if heavily unbalanced.
11.4 Implementation
- C++
- Python
- Java
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);
}
}
def partition(arr, low, high):
pivot = arr[high]
i = low - 1
for j in range(low, high):
if arr[j] < pivot:
i += 1
arr[i], arr[j] = arr[j], arr[i]
arr[i + 1], arr[high] = arr[high], arr[i + 1]
return i + 1
def quick_sort(arr, low, high):
if low < high:
pi = partition(arr, low, high)
quick_sort(arr, low, pi - 1)
quick_sort(arr, pi + 1, high)
int partition(int arr[], int low, int high) {
int pivot = arr[high];
int i = (low - 1);
for (int j = low; j < high; j++) {
if (arr[j] < pivot) {
i++;
int temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
}
}
int temp = arr[i + 1];
arr[i + 1] = arr[high];
arr[high] = temp;
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 matrices traditionally requires 8 scalar multiplications.
- For matrices, classic complexity is .
- Strassen's Algorithm uses algebraic tricks to compute the product using only 7 scalar multiplications for a block.
12.2 Step-by-Step Explanation
- Divide: Split the input matrices and into 4 sub-matrices of size .
- Compute 7 Products: Using specific algebraic combinations of these sub-matrices (involving additions and subtractions), compute 7 intermediate matrix products ( to ) instead of the usual 8.
- Combine: Use additions and subtractions of the matrices to form the 4 sub-matrices of the final result matrix .
- Recursion: If the sub-matrices are larger than , recursively apply Strassen's algorithm to compute the 7 products.
12.3 Algorithm Complexity
- Recurrence:
- Using Master Method (Case 3):
13. Exponentiation
13.1 Sequential Approach
- Compute by multiplying by itself times.
- Loop executes times:
r = a * r - Total time (considering large numbers): where is the size of the operand.
13.2 Divide & Conquer Approach
- if is even.
- if is odd.
- Recurrence: (if even)
- Total time (considering large numbers):
13.3 Step-by-Step Explanation (D&C)
- Base Case: If , return 1. If , return .
- Divide: Calculate the exponent for half the power, recursively calling the function for .
- Combine:
- If is even: Square the result of (i.e., return ).
- If is odd: Square the result of and multiply by an extra (i.e., return ).