Unit - 2
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 Algorithm
- Simple (Easy to Remember)
- Formal Algorithm (Code)
💡 The Big Idea in Plain English
Think of looking up a word in a dictionary: you don't read page-by-page from the start. You open right in the middle!
- If your word comes after the middle page, you throw away the left half and check the middle of the right half.
- If your word comes before, you throw away the right half and check the left half.
- You repeat this until you land on your word! Because the search space is cut in half every time, even searching 1,000,000 items takes at most 20 checks!
👣 4 Easy Steps to Remember
- Set Boundaries: Put two pointers at the ends —
lowat the start (),highat the end (). - Find the Middle: Calculate midpoint index: .
- Check the Middle Element:
- Match! If , return immediately.
- Target is Bigger (): The answer must be in the right half move .
- Target is Smaller (): The answer must be in the left half move .
- Stop Condition: Loop while . If , the item does not exist return .
🧠 Memory Trick: Array must be sorted check middle target bigger? go right (
low = mid + 1) target smaller? go left (high = mid - 1).
Algorithm BinarySearch(A, low, high, x)
// Searches for element x in a sorted array A[low..high]
// Input: Sorted array A, starting index low, ending index high, search key x
// Output: Index of x if found, else -1
{
while (low <= high) do
{
mid <- floor(low + (high - low) / 2);
if (A[mid] = x) then
return mid; // Element found at index mid
else if (A[mid] < x) then
low <- mid + 1; // Search right half
else
high <- mid - 1; // Search left half
}
return -1; // Element not found
}
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 Algorithm
- Simple (Easy to Remember)
- Formal Algorithm (Code)
💡 The Big Idea in Plain English
Think of sorting a messy pile of numbered cards: Instead of sorting everything at once, you chop the pile in half again and again until you hold single cards (a single card is already sorted!). Then, you zipper merge pairs of sorted piles: compare their top cards, place the smaller one down first, and repeat. You keep merging upward until you have one perfectly sorted pile!
👣 3 Easy Steps to Remember
- Divide (Split in Half): If the subarray has more than 1 element (), calculate .
- Conquer (Sort Each Half):
- Recursively sort the left half:
MergeSort(A, low, mid). - Recursively sort the right half:
MergeSort(A, mid + 1, high).
- Recursively sort the left half:
- Combine (Zipper Merge):
- Look at the front of both sorted halves.
- Take whichever number is smaller and place it into the main array.
- Once one half runs out, copy over any leftover numbers from the other half.
🧠 Memory Trick: Split down to 1 Zipper merge back up! Compare the tops of both piles, take the smaller one, and advance. Always runs in time.
Algorithm MergeSort(A, low, high)
// Sorts subarray A[low..high] using divide and conquer
// Input: Array A, starting index low, ending index high
// Output: Array A sorted in non-decreasing order
{
if (low < high) then
{
mid <- floor(low + (high - low) / 2); // Divide
MergeSort(A, low, mid); // Conquer left half
MergeSort(A, mid + 1, high); // Conquer right half
Merge(A, low, mid, high); // Combine sorted halves
}
}
Algorithm Merge(A, low, mid, high)
// Merges two sorted subarrays A[low..mid] and A[mid+1..high] into A[low..high]
{
n1 <- mid - low + 1;
n2 <- high - mid;
// Create temporary arrays L[1..n1] and R[1..n2]
for i <- 1 to n1 do
L[i] <- A[low + i - 1];
for j <- 1 to n2 do
R[j] <- A[mid + j];
i <- 1;
j <- 1;
k <- low;
while (i <= n1 and j <= n2) do
{
if (L[i] <= R[j]) then
{
A[k] <- L[i];
i <- i + 1;
}
else
{
A[k] <- R[j];
j <- j + 1;
}
k <- k + 1;
}
// Copy remaining elements of L[], if any
while (i <= n1) do
{
A[k] <- L[i];
i <- i + 1;
k <- k + 1;
}
// Copy remaining elements of R[], if any
while (j <= n2) do
{
A[k] <- R[j];
j <- j + 1;
k <- k + 1;
}
}
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 Algorithm
- Simple (Easy to Remember)
- Formal Algorithm (Code)
💡 The Big Idea in Plain English
Think of organizing students by height: You pick one student to be the pivot (say, the last student in line). Then you instruct: "Anyone shorter than the pivot, step to the left! Anyone taller, step to the right!" Now, the pivot student is standing in their exact, permanent sorted spot! You then simply repeat the same process on the left group and the right group until everyone is sorted.
👣 3 Easy Steps to Remember
- Pick a Pivot: Choose an element (standard choice: the last element
pivot = A[high]). - Partition (The Traffic Cop):
- Walk through the array from left to right.
- Whenever an element is smaller than the pivot, swap it into the left section.
- Once finished, swap the pivot right between the smaller and larger numbers.
- The pivot index is now locked in its final position!
- Repeat on Both Sides:
- Recursively sort the left side:
QuickSort(A, low, pi - 1). - Recursively sort the right side:
QuickSort(A, pi + 1, high).
- Recursively sort the left side:
🧠 Memory Trick: Pick pivot Smaller to left, larger to right Pivot locked Repeat for both sides! Average time: , worst-case: (if already sorted).
Algorithm QuickSort(A, low, high)
// Sorts subarray A[low..high] using divide and conquer
// Input: Array A, starting index low, ending index high
// Output: Array A sorted in non-decreasing order
{
if (low < high) then
{
pi <- Partition(A, low, high); // Partitioning index
QuickSort(A, low, pi - 1); // Recursively sort elements before partition
QuickSort(A, pi + 1, high); // Recursively sort elements after partition
}
}
Algorithm Partition(A, low, high)
// Partitions subarray A[low..high] around pivot element A[high]
// Output: Final sorted position index of pivot
{
pivot <- A[high];
i <- low - 1; // Index of smaller element
for j <- low to high - 1 do
{
if (A[j] < pivot) then
{
i <- i + 1;
swap(A[i], A[j]);
}
}
swap(A[i + 1], A[high]);
return (i + 1);
}
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 ).