Algorithms Design & Analysis
Unit 1: Introduction & Divide and Conquer
Introduction to Algorithms
What is an Algorithm?
The term “algorithm” is historically derived from the name of the famous 9th-century Persian mathematician and astronomer, Abu Ja’far Muhammad ibn Musa al-Khwarizmi (whose Latinized name became Algoritmi).
In computer science, an algorithm is defined as:
A finite set of unambiguous, step-by-step instructions that, when executed in a specific order with a given set of inputs, solves a computational problem and produces a desired output in a finite amount of time.
An algorithm acts as a blueprint or logical recipe for solving a problem, independent of any programming language or hardware platform.
graph LR
Input([Input<br>0 or more]) --> Alg[Algorithm<br>Logical Steps]
Alg --> Output([Output<br>1 or more])
Difference Between an Algorithm and a Program
While these terms are often used interchangeably in casual conversation, they have distinct technical differences:
| Attribute | Algorithm | Program |
|---|---|---|
| Definition | A theoretical, language-independent design or logic to solve a problem. | A concrete implementation of an algorithm in a specific programming language. |
| Termination | Must always terminate after a finite number of steps. | May or may not terminate (e.g., an Operating System or web server runs in an infinite loop). |
| Execution | Cannot be directly executed by a computer; must be converted to code. | Directly executable by the computer CPU after compilation/interpretation. |
| Medium | Written in natural language, pseudocode, or flowcharts. | Written in formal code (C, C++, Java, Python, etc.). |
A process that satisfies all characteristics of an algorithm except termination is called a computational procedure (e.g., operating systems, database management engines).
Core Characteristics / Criteria of an Algorithm
For a set of instructions to be classified as an algorithm, it must satisfy the following five fundamental criteria established by Donald Knuth:
- Input:
- It must have zero or more externally supplied values. Some algorithms do not require external inputs and generate values internally (e.g., generating first Fibonacci numbers).
- Output:
- It must produce at least one output value. The output is the solution to the given problem.
- Definiteness (Unambiguity):
- Every step of the algorithm must be clear, precise, and unambiguous. There should be no doubt about what action is to be taken.
- Non-example: Instructions like
"compute 5 / 0"or"add 6 or 7 to x"are invalid because their results or actions are undefined or ambiguous.
- Finiteness (Termination):
- If we trace the steps of the algorithm, it must terminate after a finite number of operations for all possible input values.
- Effectiveness (Feasibility):
- Every instruction must be basic enough that it can, in principle, be executed by a human using only pencil and paper in a finite amount of time.
- Non-example: Arithmetic using arbitrary real numbers is not always effective because some real numbers (like or ) have infinite decimal expansions, making exact arithmetic impossible in finite time.
Performance Analysis
Overview
Performance analysis is the process of evaluating the resources (computation time and memory space) that an algorithm requires to execute. It allows us to compare different algorithms designed to solve the same problem and choose the most efficient one.
Performance evaluation is split into two distinct phases:
- A Priori Analysis (Theoretical Evaluation):
- An analysis performed before implementing the algorithm in code.
- It determines the growth rate of the algorithm’s running time and space requirements as the input size increases.
- It is independent of machine specifications, compiler version, and programming language.
- Focuses on the frequency count of operations.
- A Posteriori Testing (Profiling / Empirical Evaluation):
- A practical evaluation performed after the algorithm is written, compiled, and executed.
- It involves running the program on sample datasets of varying sizes and measuring actual execution time (in milliseconds/seconds) using system clocks and tracking memory usage in bytes.
- Dependent on hardware, compiler, operating system, and system load.
Space Complexity
Definition
Space Complexity of an algorithm is the total amount of memory space (RAM/storage) required by the algorithm to run to completion as a function of the input size .
The total space requirements of any program , denoted as , can be expressed as:
Where:
- (Fixed Space Component):
- The memory required that is independent of the input size.
- This includes space for the machine code instructions, constant values, simple variables, and fixed-size structures.
- (Variable Space Component):
- The memory required that depends dynamically on the input size (denoted by ).
- This includes space for dynamic data structures (like arrays, linked lists, trees), recursion stack frames, and local variables inside active recursive calls.
Space Complexity Examples
Example 1: Iterative Sum of Elements
Consider an algorithm that sums the elements of an array:
Algorithm Sum(A, n)
// Input: An array A of size n
// Output: Sum of all elements in the array
{
total := 0.0;
for i := 1 to n do
total := total + A[i];
return total;
}
- Fixed space:
- Variables
n(size),total(accumulator), andi(loop counter) require constant space. Let this space be . - Code instructions require constant space .
- Variables
- Variable space:
- The array parameter
Ais passed by reference (usually a pointer of constant size). Thus, no extra local copies of the array are created. - The loop does not allocate dynamic memory.
- The array parameter
- Analysis:
- The space requirement is independent of the input size .
- Total space (Constant Space).
Example 2: Recursive Sum of Elements
Now, consider the recursive version of the same algorithm:
Algorithm RecSum(A, n)
// Input: An array A of size n
// Output: Sum of all elements in the array computed recursively
{
if (n <= 0) then
return 0.0;
else
return RecSum(A, n - 1) + A[n];
}
- Analysis:
- In a recursive algorithm, each call pushes an activation record (stack frame) onto the call stack.
- A stack frame contains parameters (
Apointer,n) and the return address. - For input size , the recursion goes to a depth of frames: .
- Each stack frame requires a fixed amount of space (say, bytes).
- Total space required by the stack = bytes.
- Therefore, the variable space component grows linearly with .
- Total Space Complexity (Linear Space).
Time Complexity
Definition
Time Complexity of an algorithm is the total time taken by the algorithm to execute and run to completion as a function of the input size .
Because the absolute execution time depends on hardware, compiler, and OS, we analyze time complexity theoretically by calculating the Step Count or Frequency Count of basic operations.
A program step is defined as a segment of code that is executed conceptually as a single unit, independent of the exact number of assembly instructions it translates into.
Time Complexity Calculations (Step Count Method)
Example 1: Iterative Sum of Elements
Let’s analyze the step count for the iterative array sum:
| Line No. | Statement | Step Cost | Execution Frequency (Count) | Total Steps |
|---|---|---|---|---|
| 1 | Algorithm Sum(A, n) | 0 | 0 | 0 |
| 2 | { | 0 | 0 | 0 |
| 3 | total := 0.0; | 1 | 1 | 1 |
| 4 | for i := 1 to n do | 1 | ||
| 5 | total := total + A[i]; | 1 | ||
| 6 | return total; | 1 | 1 | 1 |
| 7 | } | 0 | 0 | 0 |
| Total |
- Explanation of loop bounds: The loop header
for i := 1 to nexecutes times because it evaluates from up to (which executes the loop body times) plus a final check where , which fails and exits the loop. - Resulting Function: .
- As , the dominant term is , meaning the time complexity is linear, denoted as .
Example 2: Matrix Addition
Let’s analyze two matrices being added together:
Algorithm Add(A, B, C, m, n)
{
for i := 1 to m do // m + 1 times
for j := 1 to n do // m * (n + 1) times
C[i, j] := A[i, j] + B[i, j]; // m * n times
}
Let’s break down the execution count step-by-step:
- Outer loop
for i := 1 to m: runs times. - Inner loop
for j := 1 to n: runs times for each iteration of the outer loop. Since the outer loop executes times successfully, the inner loop header is evaluated times. - Assignment statement
C[i, j] := A[i, j] + B[i, j]: runs times for each successful iteration of the outer loop. Total executions = .
Total Step Count Calculation:
If (square matrices of size ):
- The dominant term is . Thus, the Time Complexity is quadratic, denoted as .
Asymptotic Notations
Concept
When analyzing the running time of an algorithm, we want to know how the execution time scales as the input size grows towards infinity ().
Asymptotic notations are mathematical tools used to:
- Ignore constant factors (like compiler overhead, clock speed).
- Ignore lower-order terms (which become insignificant as grows very large).
- Focus purely on the order of growth of the run time.
1. Big-Oh Notation () – Upper Bound
Formal Definition
We say (read as “f of n is Big-Oh of g of n”) if and only if there exist positive constants and such that:
---
config:
themeVariables:
xyChart:
backgroundColor: "transparent"
---
xychart-beta
title "Big-Oh: f(n) = O(g(n)) (Time / Cost vs Input Size n)"
x-axis ["1", "2", "3 (n0)", "4", "5", "6", "7"]
y-axis "Time / Cost" 0 --> 90
line "c * g(n)" [5, 10, 15, 25, 40, 60, 85]
line "f(n)" [15, 12, 15, 20, 30, 45, 65]
Graph Legend: Curve 1 (higher at ) is (Upper Bound) | Curve 2 is (Actual Complexity). At , holds.
Explanation
Big-Oh notation provides an asymptotic upper bound for a function. It guarantees that the algorithm will never take more than time. It represents the worst-case performance.
Mathematical Proofs
Problem 1: Prove that
- Goal: Find constants and such that for all .
- Let’s analyze the equation:
- If we choose , then for all , the term .
- So, .
- Therefore, we can choose and .
- Verification: For : (True). For : (True).
- Since for all , we have successfully proven that .
Problem 2: Prove that
- Goal: Find such that for all .
- For , we know that:
- Substitute these bounds into our original expression:
- This inequality holds for all .
- Thus, we choose and .
- Since for all , the statement is proven.
2. Omega Notation () – Lower Bound
Formal Definition
We say (read as “f of n is Omega of g of n”) if and only if there exist positive constants and such that:
---
config:
themeVariables:
xyChart:
backgroundColor: "transparent"
---
xychart-beta
title "Omega: f(n) = Ω(g(n)) (Time / Cost vs Input Size n)"
x-axis ["1", "2", "3 (n0)", "4", "5", "6", "7"]
y-axis "Time / Cost" 0 --> 120
line "f(n)" [5, 10, 20, 35, 55, 80, 110]
line "c * g(n)" [15, 12, 20, 28, 38, 50, 65]
Graph Legend: Curve 1 (higher at ) is (Actual Complexity) | Curve 2 is (Lower Bound). At , holds.
Explanation
Omega notation provides an asymptotic lower bound for a function. It guarantees that the algorithm will take at least time. It represents the best-case performance.
Mathematical Proofs
Problem: Prove that
- Goal: Find constants and such that for all .
- For :
- Therefore, we can choose and .
- Verification: For : (True). For : (True).
- Since for all , it is proven that .
3. Theta Notation () – Tight Bound
Formal Definition
We say (read as “f of n is Theta of g of n”) if and only if there exist positive constants and such that:
---
config:
themeVariables:
xyChart:
backgroundColor: "transparent"
---
xychart-beta
title "Theta: f(n) = Θ(g(n)) (Time / Cost vs Input Size n)"
x-axis ["1", "2", "3 (n0)", "4", "5", "6", "7"]
y-axis "Time / Cost" 0 --> 90
line "c2 * g(n)" [12, 24, 36, 48, 60, 72, 84]
line "f(n)" [20, 15, 30, 42, 50, 62, 75]
line "c1 * g(n)" [4, 8, 12, 16, 20, 24, 28]
Graph Legend: Curve 1 (highest) is (Upper Bound) | Curve 2 (middle) is (Actual Complexity) | Curve 3 (lowest) is (Lower Bound). At , holds.
Explanation
Theta notation provides a tight bound (exact rate of growth). It implies that the actual function grows exactly like up to constant factors. An algorithm has if and only if and .
Mathematical Proofs
Problem: Prove that
- Goal: Find positive constants such that for all .
- From our previous proofs:
- We found for all (Upper Bound: ).
- We found for all (Lower Bound: ).
- Combining these two inequalities:
- This satisfies the definition with , , and .
- Therefore, .
4. Little-oh Notation () – Strict Upper Bound
Formal Definition
We say (read as “f of n is little-oh of g of n”) if and only if for every positive constant , there exists a positive constant such that:
An equivalent and highly practical definition uses limits:
Explanation
While Big-Oh represents a loose upper bound (analogous to ), Little-oh represents a strict upper bound (analogous to ). It means becomes completely insignificant compared to as grows.
Mathematical Proofs
Problem: Prove that
- Let’s calculate the limit of the ratio of the two functions as :
- As , both terms approach zero:
- Since the limit is exactly , it is proven that .
- Note: because .
Comparison of Growth Rates
Asymptotic complexity classes can be ordered by their rate of growth. A lower growth rate means a more efficient algorithm for large values of .
---
config:
themeVariables:
xyChart:
backgroundColor: "transparent"
---
xychart-beta
title "Complexity Growth Rates (Execution Time T vs Input Size n)"
x-axis ["1", "2", "3", "4", "5", "6", "7", "8"]
y-axis "Execution Time T" 0 --> 800
line "O(2^n)" [6, 12, 24, 48, 96, 192, 384, 768]
line "O(n^2)" [6, 24, 54, 96, 150, 216, 294, 384]
line "O(n log n)" [0, 20, 48, 80, 116, 155, 196, 240]
line "O(n)" [18, 36, 54, 72, 90, 108, 126, 144]
line "O(log n)" [0, 15, 24, 30, 35, 39, 42, 45]
line "O(1)" [10, 10, 10, 10, 10, 10, 10, 10]
Key Algebraic Properties of Asymptotic Notations
Let and be positive functions.
- Transitivity:
- If and , then .
- If and , then .
- Reflexivity:
- Symmetry:
- if and only if .
- Transpose Symmetry:
- if and only if .
- if and only if (where is little-omega).
General Divide and Conquer Method
Core Concept
Divide and Conquer is a powerful algorithm design paradigm that operates by breaking down a large, complex problem into smaller, simpler subproblems of the same type, solving these subproblems, and then combining their individual solutions to form the solution to the original problem.
The strategy involves three main phases:
- Divide: Partition the problem of size into smaller, disjoint subproblems (). Typically, the division yields subproblems of approximately equal size ( or ).
- Conquer: Solve each subproblem. If the subproblem sizes are small enough (base cases), solve them directly (using a base case solver). Otherwise, solve them recursively by applying the Divide and Conquer strategy.
- Combine: Merge the solutions of the subproblems to obtain the solution for the original problem .
graph TD
P["Original Problem (Size n)"]
S1["Subproblem 1 (Size n/2)"]
S2["Subproblem 2 (Size n/2)"]
Sol1["Sub-Solution 1"]
Sol2["Sub-Solution 2"]
F["Final Solution"]
P -->|Divide| S1
P -->|Divide| S2
S1 -->|Conquer Recursively| Sol1
S2 -->|Conquer Recursively| Sol2
Sol1 -->|Combine| F
Sol2 -->|Combine| F
Control Abstraction
A control abstraction is a formula or pseudocode that outlines the flow of control in a design strategy. The control abstraction for Divide and Conquer is expressed recursively as follows:
Algorithm DAndC(P)
// P is the problem to be solved
{
if Small(P) then
return Solve(P); // Base case: solve directly
else
{
// Divide P into smaller subproblems P1, P2, ..., Pk
Divide P into P1, P2, ..., Pk;
// Conquer recursively
S1 := DAndC(P1);
S2 := DAndC(P2);
...
Sk := DAndC(Pk);
// Combine sub-solutions into final solution
return Combine(S1, S2, ..., Sk);
}
}
General Recurrence Relation
The running time of a Divide and Conquer algorithm is modeled using a recurrence relation:
Where:
- (): The number of subproblems generated in each split.
- (): The size of each subproblem (assuming all subproblems are of equal size).
- : The time required to divide the input and combine the subproblem solutions.
- : The time required to solve the base case directly (typically ).
The Master Theorem for Solving Recurrences
The Master Theorem provides a cookbook method for solving recurrence relations of the form , where and are constants, and is an asymptotically positive function.
We compare with (the “boundary” function):
Case 1: where
- If grows asymptotically slower than , then the work done at the leaves dominates the total running time.
- Solution: .
Case 2: where and
- If and grow at the same rate, then the work is distributed evenly across all levels of the recursion tree.
- Solution: .
- Special Case (): If , then .
Case 3: where
- If grows asymptotically faster than , then the work done at the root (dividing and combining) dominates the running time.
- Condition: We must also satisfy the regularity condition: for some constant and all sufficiently large .
- Solution: .
Binary Search
Concepts & Pre-conditions
Binary Search is an efficient search algorithm used to find the position of a target element within a sorted array (arranged in non-decreasing order).
- Core Idea: Instead of scanning elements sequentially (which takes time), Binary Search repeatedly divides the search interval in half.
- Pre-condition: The input list must be pre-sorted. If not sorted, Binary Search will fail to return correct results.
Pseudocode
1. Iterative Binary Search
Algorithm BinarySearch(A, n, x)
// A is a sorted array of size n (1-indexed)
// x is the target value to find
{
low := 1;
high := n;
while (low <= high) do
{
mid := floor((low + high) / 2);
if (x = A[mid]) then
return mid; // Target found
else if (x < A[mid]) then
high := mid - 1; // Discard right half
else
low := mid + 1; // Discard left half
}
return 0; // Target not found
}
2. Recursive Binary Search
Algorithm RecBinarySearch(A, low, high, x)
{
if (low > high) then
return 0; // Base case: not found
mid := floor((low + high) / 2);
if (x = A[mid]) then
return mid; // Target found
else if (x < A[mid]) then
return RecBinarySearch(A, low, mid - 1, x); // Search left
else
return RecBinarySearch(A, mid + 1, high, x); // Search right
}
Step-by-Step Trace Example
Let’s trace the search for target in the sorted array:
Trace Table for :
| Iteration | low | high | mid | A[mid] | Comparison / Action |
|---|---|---|---|---|---|
| 1 | 1 | 8 | Search right half. Set low := mid + 1 = 5 | ||
| 2 | 5 | 8 | Element found! Return index . |
Result: Element found at index .
Trace Table for a Missing Element :
| Iteration | low | high | mid | A[mid] | Comparison / Action |
|---|---|---|---|---|---|
| 1 | 1 | 8 | 4 | Set low := mid + 1 = 5 | |
| 2 | 5 | 8 | 6 | Set low := mid + 1 = 7 | |
| 3 | 7 | 8 | 7 | Set high := mid - 1 = 6 | |
| Exit | 7 | 6 | - | - | Loop terminates because low > high (). Return . |
Complexity Analysis
Time Complexity
At each step of the algorithm, the search space is cut in half. The recurrence relation representing the worst-case number of comparisons for an array of size is:
Using Case 2 of the Master Theorem ():
-
.
-
Since , we have .
-
Therefore, .
-
Best-Case: (when the target is found at the first
midcheck). -
Worst-Case: (when the target is at the ends or not in the array).
-
Average-Case: .
Space Complexity
- Iterative Version: auxiliary space since we only use a few variables (
low,high,mid). - Recursive Version: auxiliary space because of the recursion stack frames. The maximum stack depth is equal to the height of the decision tree, which is .
Merge Sort
Concepts
Merge Sort is an external, stable sorting algorithm based on the Divide and Conquer strategy.
- Core Idea: It divides the unsorted list of size into two halves of size , sorts each half recursively, and then merges the two sorted halves back into a single sorted list.
- Stability: It maintains the relative order of equal elements, making it a stable sort.
Pseudocode
1. MergeSort Algorithm
Algorithm MergeSort(A, low, high)
// Sorts array A from index low to high
{
if (low < high) then
{
mid := floor((low + high) / 2);
MergeSort(A, low, mid); // Sort left sub-array
MergeSort(A, mid + 1, high); // Sort right sub-array
Merge(A, low, mid, high); // Merge sorted halves
}
}
2. Merge Algorithm (Combining Step)
Algorithm Merge(A, low, mid, high)
// Merges two sorted sub-arrays: A[low..mid] and A[mid+1..high]
{
h := low; // Pointer for left sub-array
i := low; // Pointer for auxiliary array B
j := mid + 1; // Pointer for right sub-array
// Copy elements to auxiliary array B in sorted order
while (h <= mid and j <= high) do
{
if (A[h] <= A[j]) then
{
B[i] := A[h];
h := h + 1;
}
else
{
B[i] := A[j];
j := j + 1;
}
i := i + 1;
}
// Copy any remaining elements of the left sub-array
if (h > mid) then
for k := j to high do
{
B[i] := A[k];
i := i + 1;
}
else
for k := h to mid do
{
B[i] := A[k];
i := i + 1;
}
// Copy elements back from B to original array A
for k := low to high do
A[k] := B[k];
}
Step-by-Step Trace Example
Let’s trace Merge Sort on array:
Division Phase (Recursive Splits):
graph TD
A["[38, 27, 43, 3, 9, 82, 10]"]
B["[38, 27, 43]"]
C["[3, 9, 82, 10]"]
D["[38, 27]"]
E["[43]"]
F["[3, 9]"]
G["[82, 10]"]
H["[38]"]
I["[27]"]
J["[43]"]
K["[3]"]
L["[9]"]
M["[82]"]
N["[10]"]
A --> B
A --> C
B --> D
B --> E
C --> F
C --> G
D --> H
D --> I
F --> K
F --> L
G --> M
G --> N
Merge Phase (Combining Steps):
- Merge
[38]and[27][27, 38] - Merge
[27, 38]and[43][27, 38, 43] - Merge
[3]and[9][3, 9] - Merge
[82]and[10][10, 82] - Merge
[3, 9]and[10, 82][3, 9, 10, 82] - Merge
[27, 38, 43]and[3, 9, 10, 82][3, 9, 10, 27, 38, 42, 82](Final Sorted Array:[3, 9, 10, 27, 38, 43, 82]).
Complexity Analysis
Time Complexity
The recurrence relation for Merge Sort is:
Where represents the time taken by the Merge process to linearly scan and combine elements.
Using the Master Theorem ():
-
.
-
Since , we have .
-
Case 2 applies with :
-
Best, Worst, and Average Cases: All are because the algorithm divides the array and merges it regardless of the initial arrangement of elements.
Space Complexity
- Auxiliary Space: is required to hold the auxiliary array
Bduring the merging step. - Recursion Stack: to handle recursive frames.
- Total Space Complexity: (dominated by the auxiliary array).
Quick Sort (Partition Exchange Sort)
Concepts
Quick Sort is an in-place, unstable sorting algorithm based on Divide and Conquer.
- Core Idea: Unlike Merge Sort, which divides the array at the exact midpoint, Quick Sort divides the array dynamically using a pivot element.
- Partitioning: The array is rearranged so that all elements smaller than or equal to the pivot are placed to its left, and all elements larger than or equal to the pivot are placed to its right. The sub-arrays are then sorted independently. No merge step is needed at the end because the elements are already in their correct partitions.
Pseudocode
1. QuickSort Algorithm
Algorithm QuickSort(A, low, high)
{
if (low < high) then
{
// j is the correct index of the pivot element
j := Partition(A, low, high + 1); // high + 1 acts as bound
QuickSort(A, low, j - 1); // Sort left partition
QuickSort(A, j + 1, high); // Sort right partition
}
}
2. Partition Algorithm (Hoare-like standard)
To implement the partition, we append a sentinel value at the end of the array to prevent pointer from running off the bounds of the array.
Algorithm Partition(A, m, p)
// m is low, p is high + 1. Pivot is A[m].
{
pivot := A[m];
i := m;
j := p; // Started at high + 1
repeat
{
// Move i right as long as elements are smaller than pivot
repeat
i := i + 1;
until (A[i] >= pivot);
// Move j left as long as elements are larger than pivot
repeat
j := j - 1;
until (A[j] <= pivot);
if (i < j) then
Swap(A[i], A[j]);
} until (i >= j);
// Swap pivot to its final correct position
Swap(A[m], A[j]);
return j; // Return pivot position
}
Step-by-Step Trace Example
Let’s trace the partition of the array: We set a sentinel value at the end. Thus:
- Initial indices: , .
- Pivot: .
- Pointers: , .
Partitioning Execution:
- Loop 1:
- Move right: , Stops at .
- Move left: Stops at .
- Since (), Swap and .
- Array becomes:
- Loop 2:
- Move right: , , Stops at .
- Move left: , Stops at .
- Since (), exit the loop.
- Swap Pivot:
- Swap pivot with .
- Array becomes:
- Pivot is now at index (its final correct position).
Resulting Subproblems:
- Left partition:
- Right partition:
Complexity Analysis
Time Complexity
1. Worst-Case ()
- Occurs when: The input array is already sorted, reverse sorted, or contains all identical elements. In these cases, the partition element is always the minimum or maximum element, dividing the problem into subproblems of size and .
- Recurrence:
- Solving by substitution:
2. Best-Case ()
- Occurs when: The pivot always splits the array into two equal halves of size .
- Recurrence:
- Solving via Master Theorem Case 2: .
3. Average-Case ()
- Assuming random elements, the average split is reasonably balanced (e.g., ), which still yields a recursion depth of and total time of .
Space Complexity
- Worst-Case: stack space due to unbalanced recursive calls.
- Best-Case / Average-Case: stack space.
Strassen’s Matrix Multiplication
Concepts
Let and be two matrices. We want to calculate the product matrix .
Conventional Method ()
The standard algorithm uses three nested loops to multiply matrices:
for i := 1 to n do
for j := 1 to n do
{
C[i, j] := 0;
for k := 1 to n do
C[i, j] := C[i, j] + A[i, k] * B[k, j];
}
- This method does exactly multiplications and additions, leading to a complexity of .
Naive Divide & Conquer Approach
We can partition matrices into four submatrices of size :
Their product matrix is defined as:
Where:
-
-
-
-
-
Analysis: This formulation requires 8 multiplications of submatrices and 4 additions.
-
Recurrence: (where is the cost of matrix additions).
-
By Master Theorem Case 1 (): . This does not improve on the conventional method.
Strassen’s Formulas
Volker Strassen discovered a way to compute the submatrices of using only 7 multiplications (instead of 8) and 18 additions/subtractions:
We define 7 products to :
Using these products, the submatrices of are computed as:
Step-by-Step Numerical Trace Example
Let’s multiply the two matrices:
Submatrix Blocks (since , elements are matrices):
Step 1: Compute to
Step 2: Combine to form
Result Matrix: (Which is exactly correct as , , etc.)
Complexity Analysis
Time Complexity
The recurrence relation for Strassen’s matrix multiplication is:
Using the Master Theorem ():
- .
- Since , we have .
- Case 1 applies:
Thus, Strassen’s algorithm reduces matrix multiplication complexity from to .
Limitations of Strassen’s Algorithm
Despite its superior asymptotic complexity, Strassen’s algorithm has several practical issues:
- Space Overhead: It requires allocating many temporary submatrices during recursion, increasing the spatial constant factor.
- Crossover Point: For small matrices (typically or ), the constant factor of additions outweighs the savings in multiplications. Standard multiplication is faster for small matrices.
- Numerical Stability: The subtraction operations in Strassen’s algorithm introduce larger numerical precision errors than conventional methods.
- Hardware Optimization: Standard multiplication can be easily parallelized and fits better into cache hierarchies (blocked algorithms).