Algorithm design is the process of creating a step-by-step procedure or formula to solve a given problem. To create efficient and optimal solutions, computer scientists use various algorithm design techniques. These techniques help break down a problem into manageable subproblems and efficiently combine the results.
Here are some of the primary algorithm design techniques:
Brute Force (also known as Exhaustive Search) is the simplest approach to solving a problem. In this technique, we solve the problem by checking all possible solutions and choosing the best one. It is a direct and straightforward method but often inefficient for large inputs.
In an array of numbers, you can use brute force to find the maximum element by comparing each element to the current maximum.
def find_max(arr):
max_value = arr[0]
for num in arr[1:]:
if num > max_value:
max_value = num
return max_value
Time Complexity: O(n), where n is the number of elements in the array.
Divide and Conquer is a powerful algorithm design technique where the problem is recursively divided into smaller subproblems. Each subproblem is solved independently, and the solutions are combined to form the solution to the original problem. This technique often leads to efficient algorithms.
Merge Sort is a classic example of divide and conquer. It divides the array into two halves, recursively sorts each half, and then merges the sorted halves.
def merge_sort(arr):
if len(arr) > 1:
mid = len(arr) // 2
left = arr[:mid]
right = arr[mid:]
merge_sort(left)
merge_sort(right)
i = j = k = 0
while i < len(left) and j < len(right):
if left[i] < right[j]:
arr[k] = left[i]
i += 1
else:
arr[k] = right[j]
j += 1
k += 1
while i < len(left):
arr[k] = left[i]
i += 1
k += 1
while j < len(right):
arr[k] = right[j]
j += 1
k += 1
Time Complexity: O(n log n)
Dynamic Programming (DP) is a technique used to solve problems by breaking them down into simpler subproblems. DP is especially useful when the problem has overlapping subproblems and optimal substructure. It avoids redundant work by storing the solutions to subproblems and reusing them when needed.
Using dynamic programming, we can compute the Fibonacci sequence in O(n) time by storing previously computed values.
Memoization (Top-Down):
def fibonacci(n, memo={}):
if n in memo:
return memo[n]
if n <= 1:
return n
memo[n] = fibonacci(n-1, memo) + fibonacci(n-2, memo)
return memo[n]
Time Complexity: O(n), due to memoization.
A Greedy Algorithm is a strategy for solving optimization problems by making locally optimal choices at each step, hoping that these local choices will lead to a globally optimal solution. Greedy algorithms are typically easier to implement but may not always produce an optimal solution for all problems.
In the activity selection problem, given a set of activities with start and finish times, the goal is to select the maximum number of activities that do not overlap.
def activity_selection(activities):
activities.sort(key=lambda x: x[1]) # Sort by finish time
selected = [activities[0]]
for i in range(1, len(activities)):
if activities[i][0] >= selected[-1][1]:
selected.append(activities[i])
return selected
Time Complexity: O(n log n) due to sorting.
Backtracking is a technique used to find all (or some) solutions to a problem by incrementally building the solution and abandoning ("backtracking") the solution as soon as it is determined that it cannot be extended to a valid solution. This technique is used for constraint satisfaction problems.
In the N-Queens problem, we need to place N queens on an N x N chessboard such that no two queens can attack each other.
def is_safe(board, row, col, n):
for i in range(col):
if board[row][i] == 1:
return False
for i, j in zip(range(row, -1, -1), range(col, -1, -1)):
if board[i][j] == 1:
return False
for i, j in zip(range(row, n, 1), range(col, -1, -1)):
if board[i][j] == 1:
return False
return True
def solve_n_queens(board, col, n):
if col >= n:
return True
for i in range(n):
if is_safe(board, i, col, n):
board[i][col] = 1
if solve_n_queens(board, col + 1, n):
return True
board[i][col] = 0
return False
Time Complexity: In the worst case, it can be , where is the size of the board.
Branch and Bound is a general algorithm design paradigm for solving optimization problems, specifically combinatorial problems. It systematically searches the solution space by branching and pruning unpromising parts of the space.
The 0/1 Knapsack problem asks for the optimal selection of items to maximize value without exceeding weight capacity. Branch and Bound can help prune solutions by evaluating partial solutions.
These techniques are foundational for designing efficient algorithms to solve a wide variety of problems in computer science and operations research. Each technique has its strengths and weaknesses, and the choice of technique often depends on the nature of the problem.
Open this section to load past papers