Unit 6 - Notes
Unit 6: Algorithms
1. Understanding Algorithms and Analysis
1.1 Definition
An algorithm is a finite sequence of well-defined, computer-implementable instructions, typically to solve a class of problems or to perform a computation. In the context of Combinatorial Studies, algorithms are often used to generate structures (permutations, graphs) or optimize configurations.
1.2 Five Essential Properties (Knuth)
For a procedure to be considered an algorithm, it must satisfy:
- Finiteness: The algorithm must always terminate after a finite number of steps.
- Definiteness: Each step must be precisely defined; the actions to be carried out must be rigorously and unambiguously specified.
- Input: An algorithm has zero or more inputs.
- Output: An algorithm has one or more outputs that have a specific relation to the inputs.
- Effectiveness: All operations must be sufficiently basic that they can in principle be done exactly and in a finite length of time by a human using pencil and paper.
1.3 Algorithm Analysis
Analysis refers to the process of predicting the resources that the algorithm requires.
- Correctness: Proving that the algorithm produces the correct output for every valid input.
- Efficiency: Measuring the computational complexity.
- Time Complexity: How long the algorithm takes to run as a function of input size.
- Space Complexity: How much memory the algorithm uses during execution.
2. Running Time Analysis and Rate of Growth
2.1 The RAM Model
To analyze running time independent of specific hardware, we use the Random Access Machine (RAM) model:
- Instructions are executed one after another (no concurrent operations).
- Standard operations (addition, comparison, assignment) take constant time ($1$ unit).
- Memory access takes constant time.
2.2 Input Size ()
The running time is expressed as a function of the input size .
- For sorting/searching: is the number of items in the list.
- For graph algorithms: usually refers to vertices () and edges ().
- For arithmetic: is the number of bits needed to represent the number.
2.3 Rate of Growth
We are interested in the rate of growth (or order of growth) of the running time. We look at how behaves as . Lower order terms and leading constants are ignored because for very large inputs, the highest-degree term dominates.
Comparison of Growth Rates (Slowest to Fastest):
- $1$ (Constant)
- (Logarithmic)
- (Linear)
- (Linearithmic/Quasilinear)
- (Quadratic)
- (Cubic)
- (Exponential)
- (Factorial)
2.4 Cases of Analysis
- Worst-Case Analysis (Standard): The maximum running time for any input of size . This provides an upper bound guarantee.
- Average-Case Analysis: The expected running time over all possible inputs of size . Requires probability distribution assumptions.
- Best-Case Analysis: The minimum running time. Often not useful for guaranteeing performance.
3. Asymptotic Notation: Big-O Notation
Asymptotic notation provides a mathematical framework to describe the limiting behavior of a function.
3.1 Big-O Notation () – Upper Bound
represents the set of functions that grow no faster than .
- Definition: if there exist positive constants and such that:
- Meaning: Worst-case scenario. The algorithm will not take longer than this order.
3.2 Big-Omega Notation () – Lower Bound
represents the set of functions that grow at least as fast as .
- Definition: if there exist positive constants and such that:
3.3 Big-Theta Notation () – Tight Bound
implies the function grows at the same rate as .
- Definition: if AND .
3.4 Rules for Manipulation
- Constant Factors Rule: .
- Sum Rule: .
- Product Rule: .
- Polynomial Rule: If is a polynomial of degree , then .
4. Recursion and Backtracking
4.1 Recursion
Recursion is a method where the solution to a problem depends on solutions to smaller instances of the same problem.
Components:
- Base Case: The condition under which the recursion stops (prevents infinite loops).
- Recursive Step: The function calls itself with a modified parameter (moving toward the base case).
Recurrence Relations:
The running time of recursive algorithms is described by recurrence relations.
- Example (Merge Sort): .
- Master Theorem: A formulaic way to solve recurrences of the form .
4.2 Backtracking
Backtracking is an algorithmic technique for solving problems recursively by trying to build a solution incrementally, one piece at a time, removing those solutions that fail to satisfy the constraints of the problem at any point of time.
Key Concepts:
- State Space Tree: A tree representing all possible states (configurations) of the problem.
- Depth-First Search (DFS): Backtracking explores the tree depth-first.
- Pruning: If the current node cannot possibly lead to a valid solution, the algorithm abandons this branch (backtracks) to save time. This distinguishes backtracking from brute-force enumeration.
Combinatorial Application:
Generating permutations, combinations, or solving constraint satisfaction problems.
Example: The N-Queens Problem
- Goal: Place queens on an chessboard so no two queens attack each other.
- Approach: Place a queen in column 1. Move to column 2. If a safe square exists, place queen. If not, backtrack to column 1 and move that queen.
- Complexity: in worst case, but average performance is much better due to pruning.
# Pseudocode for Backtracking Template
def backtrack(candidate):
if find_solution(candidate):
output(candidate)
return
for next_candidate in list_of_candidates:
if is_valid(next_candidate):
place(next_candidate)
backtrack(next_candidate)
remove(next_candidate) # Backtrack step
5. Dynamic Programming (DP) Strategies
Dynamic Programming is an optimization technique used to solve problems by breaking them down into simpler subproblems and storing the results of these subproblems to avoid redundant computations.
5.1 Two Necessary Properties
For DP to be applicable, the problem must exhibit:
- Optimal Substructure: An optimal solution to the problem contains within it optimal solutions to subproblems.
- Overlapping Subproblems: The recursive algorithm visits the same subproblems repeatedly.
5.2 Approaches
-
Top-Down with Memoization:
- Write the recursive solution.
- Create a cache (array or hash map).
- Before computing a subproblem, check if the result is in the cache.
- If yes, return it. If no, compute and save it.
-
Bottom-Up with Tabulation:
- Identify the order in which subproblems must be solved (usually smallest to largest).
- Fill a table iteratively.
- The final answer is usually in the last cell of the table.
5.3 Combinatorial Example: Binomial Coefficients
Calculating "n choose k" recursively involves massive redundancy.
- Recursive Relation: .
- DP Approach (Pascal's Triangle):
Construct a table where row and column stores .
- Complexity: Reduces from Exponential (Recursive) to (DP).
6. Greedy Algorithms
6.1 Definition
A greedy algorithm builds up a solution piece by piece, always choosing the next piece that offers the most immediate and obvious benefit. It makes a locally optimal choice in the hope that this choice will lead to a globally optimal solution.
6.2 Key Properties
- Greedy Choice Property: A global optimum can be arrived at by selecting a local optimum.
- Optimal Substructure: An optimal solution to the problem contains an optimal solution to subproblems.
6.3 Contrast with Dynamic Programming
- DP: Makes a decision at each step based on the solutions to all subproblems (looks back). It is exhaustive and guarantees the optimum.
- Greedy: Makes a decision based on current information without looking at the bigger picture (looks forward). It is faster but does not always guarantee the optimal solution.
6.4 Examples
- Interval Scheduling (Activity Selection): Given a set of activities with start and finish times, select the maximum number of non-overlapping activities.
- Greedy Strategy: Always pick the remaining activity that finishes earliest. (This is proven to work).
- Huffman Coding: Used for data compression. Builds a tree by repeatedly combining the two symbols with the lowest frequency.
- Knapsack Problem:
- Fractional Knapsack: (Items can be cut) -> Greedy works (pick highest value/weight ratio).
- 0/1 Knapsack: (Items are whole) -> Greedy fails. Requires Dynamic Programming.