Definition: An iterative algorithm approaches the solution to a problem by repeatedly executing a segment of code.
Explanation: The key to an iterative algorithm is the repeated execution of a specific step until a condition is met. You can think of it like brushing your teeth every morning—you keep brushing until all your teeth are clean.
Applications: Calculating factorials, summing numbers, traversing arrays, etc.
Example: Calculating Factorial
Approach:
Use a loop to gradually calculate the product for the factorial.
Code Example:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
#include<stdio.h>
// Calculate the factorial of n unsignedlonglongfactorial(int n) { unsignedlonglong result = 1; for (int i = 1; i <= n; ++i) { result *= i; // Multiply by each number gradually } return result; // Return the result }
intmain() { int n = 5; // The number for which to calculate the factorial printf("Factorial of %d is %llu\n", n, factorial(n)); // Print the result return0; }
4.2 Brute Force Method
Definition: The brute force method tries all possible solutions until it finds one that satisfies the conditions.
Explanation: The brute force method is straightforward and simple, where you try every possible answer and then find the correct one. It’s like finding the right key to open a door from a bunch of keys—you try each key one by one until you find the right one.
Applications: Finding the maximum value in an array, password cracking, combination problems, etc.
Example: Finding the Maximum Value in an Array
Approach:
Compare each element in the array one by one and keep track of the maximum value.
Code Example:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
#include<stdio.h>
// Find the maximum value in an array intfindMax(int arr[], int size) { int max = arr[0]; // Assume the first element is the maximum for (int i = 1; i < size; ++i) { if (arr[i] > max) { max = arr[i]; // Update the maximum value } } return max; // Return the maximum value }
intmain() { int arr[] = {1, 3, 5, 7, 9, 2, 4, 6, 8, 0}; // Example array int size = sizeof(arr) / sizeof(arr[0]); // Array size printf("Maximum value is %d\n", findMax(arr, size)); // Print the maximum value return0; }
4.3 Divide and Conquer Algorithm
Definition: The divide and conquer algorithm divides a problem into multiple subproblems, solves each subproblem independently, and then combines the solutions to the subproblems to obtain the solution to the original problem.
Explanation: The core idea of the divide and conquer algorithm is to break down a big problem into smaller problems and solve them individually, like when you clean a room by dividing it into several areas and cleaning each one by one. Eventually, the entire room is clean.
Applications: Merge sort, quick sort, binary search, etc.
// Merge two sorted subarrays voidmerge(int arr[], int l, int m, int r) { int i, j, k; int n1 = m - l + 1; int n2 = r - m;
// Create temporary arrays int L[n1], R[n2];
// Copy data to temporary arrays for (i = 0; i < n1; i++) L[i] = arr[l + i]; for (j = 0; j < n2; j++) R[j] = arr[m + 1 + j];
// Merge the temporary arrays back into the original array i = 0; // Initial index of left subarray j = 0; // Initial index of right subarray k = l; // Initial index of merged subarray while (i < n1 && j < n2) { if (L[i] <= R[j]) { arr[k] = L[i]; i++; } else { arr[k] = R[j]; j++; } k++; }
// Copy the remaining elements while (i < n1) { arr[k] = L[i]; i++; k++; }
while (j < n2) { arr[k] = R[j]; j++; k++; } }
// Merge sort voidmergeSort(int arr[], int l, int r) { if (l < r) { int m = l + (r - l) / 2; // Calculate the middle point
mergeSort(arr, l, m); // Recursively sort the left half mergeSort(arr, m + 1, r); // Recursively sort the right half
merge(arr, l, m, r); // Merge the two halves } }
intmain() { int arr[] = {12, 11, 13, 5, 6, 7}; // Example array int arr_size = sizeof(arr) / sizeof(arr[0]); // Array size
printf("Given array is \n"); for (int i = 0; i < arr_size; i++) printf("%d ", arr[i]); printf("\n");
printf("\nSorted array is \n"); for (int i = 0; i < arr_size; i++) printf("%d ", arr[i]); printf("\n"); return0; }
4.4 Greedy Algorithm
Definition: A greedy algorithm makes the locally optimal choice at each step with the hope of finding the global optimum.
Explanation: The strategy of a greedy algorithm is to make the best choice at each step, like when you are walking through a maze and always choose the path that looks closest to the exit. This doesn’t always guarantee the shortest path, but it may lead you to the exit faster.
Applications: Activity selection problem, knapsack problem, minimum spanning tree (Prim’s and Kruskal’s algorithms), etc.
Example: Activity Selection Problem
Approach:
Sort all activities by their end time.
Select the first activity, then select the next activity that starts after the current one ends.
// Select activities voidactivitySelection(Activity activities[], int n) { int i, j;
printf("Selected activities are:\n");
i = 0; // The first activity is always selected printf("(%d, %d) ", activities[i].start, activities[i].end);
// Select other activities for (j = 1; j < n; j++) { if (activities[j].start >= activities[i].end) { printf("(%d, %d) ", activities[j].start, activities[j].end); i = j; // Update the last selected activity } } printf("\n"); }
intmain() { Activity activities[] = {{1, 4}, {3, 5}, {0, 6}, {5, 7}, {8, 9}, {5, 9}}; // Array of activities int n = sizeof(activities) / sizeof(activities[0]); // Number of activities
activitySelection(activities, n); // Call the activity selection algorithm return0; }
4.5 Dynamic Programming
Definition: Dynamic programming decomposes a problem into smaller subproblems and uses the solutions to the subproblems to construct the solution to the original problem. It typically avoids recomputation by storing the results of already solved subproblems.
Explanation: The idea behind dynamic programming is to break down a big problem into many smaller problems and save the solution to each subproblem, so each small problem is solved only once. It’s like building a house where each room’s construction plan is recorded, so when you build a similar room later, you don’t have to design it from scratch.
Applications: Fibonacci sequence, longest common subsequence, knapsack problem, etc.
Example: Fibonacci Sequence
Approach:
Break the problem into computing the sum of the two preceding Fibonacci numbers.
Use an array to store already computed Fibonacci numbers to avoid recomputation.
// Compute the Fibonacci sequence for (int i = 2; i <= n; i++) { fib[i] = fib[i - 1] + fib[i - 2]; }
return
fib[n]; // Return the result }
intmain() { int n = 10; // Position in the Fibonacci sequence to compute printf("Fibonacci number at position %d is %llu\n", n, fibonacci(n)); // Print the result return0; }
4.6 Comparison of Algorithm Strategies
Comparison:
Iterative Algorithm: Simple and straightforward, suitable for simple repetitive problems but may not be efficient.
Brute Force Method: Simple to understand but inefficient, suitable for small-scale problems.
Divide and Conquer Algorithm: Solves problems by breaking them down into subproblems, suitable for problems with recursive nature and efficient.
Greedy Algorithm: Makes locally optimal choices, suitable for specific problems but does not guarantee global optimality.
Dynamic Programming: Suitable for problems with overlapping subproblems and optimal substructure, efficient but requires more storage space.
Iterative Algorithm vs Brute Force Method: The iterative algorithm focuses more on repeated calculations, while the brute force method tries all possible solutions.
Divide and Conquer Algorithm vs Dynamic Programming: The divide and conquer algorithm solves subproblems independently, while dynamic programming records solutions to avoid recomputation. Divide and conquer is suitable for problems with non-overlapping subproblems, while dynamic programming is suitable for problems with overlapping subproblems.
Greedy Algorithm vs Dynamic Programming: The greedy algorithm makes locally optimal choices at each step, while dynamic programming considers the overall problem to guarantee global optimality.
5.1 Overview of Graph Search
Graph search algorithms are used to traverse or search nodes in a graph. They are mainly divided into Depth-First Search (DFS) and Breadth-First Search (BFS).
5.2 Breadth-First Search (BFS)
Definition: Starts from the initial node and visits nodes layer by layer until the target node is found or all nodes are traversed.
Explanation: BFS is like searching tree nodes level by level, starting from the root. It ensures that all nodes in each layer are visited before moving on to the next layer.
Applications: Shortest path finding, social network friend recommendations, level-order traversal, etc.
// Check if the queue is empty intisEmpty(Queue* q) { return q->rear == -1; }
// Enqueue voidenqueue(Queue* q, int value) { if (q->rear == MAX - 1) return; if (q->front == -1) q->front = 0; q->items[++q->rear] = value; }
// Dequeue intdequeue(Queue* q) { int item; if (isEmpty(q)) { printf("Queue is empty\n"); return-1; } else { item = q->items[q->front]; if (q->front >= q->rear) { q->front = -1; q->rear = -1; } else { q->front++; } return item; } }
// Breadth-First Search voidbfs(int graph[][MAX], int startVertex, int numVertices) { Queue* q = createQueue(); int visited[MAX] = {0}; // Initialize the visited array
printf("BFS traversal starting from vertex %d: ", startVertex); visited[startVertex] = 1; // Mark the starting node as visited enqueue(q, startVertex);
while (!isEmpty(q)) { int currentVertex = dequeue(q); printf("%d ", currentVertex);
// Visit all adjacent nodes for (int i = 0; i < numVertices; i++) { if (graph[currentVertex][i] == 1 && !visited[i]) { visited[i] = 1; enqueue(q, i); } } } printf("\n"); }
Definition: Starts from the initial node and visits nodes as deeply as possible along each branch until the target node is found or all nodes are traversed.
Explanation: DFS is like starting from the root of a tree and going down as far as possible, then backtracking to the previous node to continue. It’s similar to exploring one direction deeply and backtracking when a dead end is encountered.
Applications: Topological sorting, connectivity checking in graphs, maze solving, etc.
// Depth-First Search voiddfs(int graph[][MAX], int vertex, int visited[], int numVertices) { visited[vertex] = 1; // Mark the current node as visited printf("%d ", vertex);
// Visit all adjacent nodes for (int i = 0; i < numVertices; i++) { if (graph[vertex][i] == 1 && !visited[i]) { dfs(graph, i, visited, numVertices); } } }
int visited[MAX] = {0}; // Initialize the visited array printf("DFS traversal starting from vertex 0: "); dfs(graph, 0, visited, numVertices); // Call depth-first search printf("\n"); return0; }
5.4 Backtracking
Definition: Backtracking is a methodical way of searching through a problem’s solution space by recursively building a solution and abandoning it if it doesn’t meet the problem’s requirements.
Explanation: Backtracking is like navigating a maze—when you hit a dead end, you backtrack to the previous junction and try a different path until you find the exit or determine there is no viable path.
Applications: N-Queens problem, Sudoku solving, generating combinations, etc.
Example: N-Queens Problem
Approach:
Place a queen on the board, trying each column and ensuring no conflicts.
If placing a queen is not possible at a certain step, backtrack to the previous step and try a different position.
if (solveNQUtil(board, 0) == 0) { printf("Solution does not exist"); return; }
printSolution(board); // Print the result }
intmain() { solveNQ(); // Call the N-Queens solution return0; }
5.5 Branch and Bound
Definition: Branch and bound is an optimization search algorithm that improves search efficiency by limiting and pruning the search space of impossible or suboptimal solutions.
Explanation: Branch and bound is like searching through options with some conditions to rule out impossible paths early, avoiding inefficient searches. It’s like filtering out products based on budget and requirements when shopping.
Applications: Traveling salesman problem, 0/1 knapsack problem, maximum clique problem in graphs, etc.
Example: 0/1 Knapsack Problem
Approach:
For each item, consider two cases: including the item in the knapsack and excluding it.
Use a bound function (like remaining capacity, current value) to prune impossible solutions.
// Define a structure to represent items typedefstruct { int weight; // Item weight int value; // Item value } Item;
// Return the larger of two numbers intmax(int a, int b) { return (a > b) ? a : b; }
// Dynamic programming solution to the 0/1 knapsack problem intknapsack(Item items[], int n, int W) { // Create a one-dimensional array to store the maximum value for different capacities int dp[W + 1]; // Initialize the array with all zeros for (int i = 0; i <= W; i++) dp[i] = 0;
// Iterate through each item for (int i = 0; i < n; i++) { // Iterate through the knapsack capacity from W down to the item's weight for (int w = W; w >= items[i].weight; w--) { // Update the dp array by taking the maximum value between including the current item and not including it dp[w] = max(dp[w], dp[w - items[i].weight] + items[i].value); } } // Return the maximum value for the knapsack with capacity W return dp[W]; }
intmain() { // Define an array of items with their weights and values Item items[] = {{2, 3}, {3, 4}, {4, 5}, {5, 6}}; // Knapsack capacity int W = 5; // Number of items int n = sizeof(items) / sizeof(items[0]); // Calculate and print the maximum value for the knapsack with capacity W printf("Maximum value in Knapsack = %d\n", knapsack(items, n, W)); return0; }
// 找到数组中的最大值 intfindMax(int arr[], int size) { int max = arr[0]; // 假设第一个元素为最大值 for (int i = 1; i < size; ++i) { if (arr[i] > max) { max = arr[i]; // 更新最大值 } } return max; // 返回最大值 }
intmain() { int arr[] = {1, 3, 5, 7, 9, 2, 4, 6, 8, 0}; // 示例数组 int size = sizeof(arr) / sizeof(arr[0]); // 数组大小 printf("Maximum value is %d\n", findMax(arr, size)); // 打印最大值 return0; }
// 定义一个结构体来表示物品 typedefstruct { int weight; // 物品重量 int value; // 物品价值 } Item;
// 返回两个数中较大的一个 intmax(int a, int b) { return (a > b) ? a : b; }
// 动态规划解决0/1背包问题 intknapsack(Item items[], int n, int W) { // 创建一个一维数组来存储背包在不同容量下的最大价值 int dp[W + 1]; // 初始化数组,所有值都为0 for (int i = 0; i <= W; i++) dp[i] = 0;
// 遍历每个物品 for (int i = 0; i < n; i++) { // 从背包容量W开始,逐渐减小 for (int w = W; w >= items[i].weight; w--) { // 更新dp数组,取放入当前物品后的价值和不放入当前物品的价值的最大值 dp[w] = max(dp[w], dp[w - items[i].weight] + items[i].value); } } // 返回容量为W的背包能装下的最大价值 return dp[W]; }
intmain() { // 定义物品数组,每个物品有重量和价值 Item items[] = {{2, 3}, {3, 4}, {4, 5}, {5, 6}}; // 背包的容量 int W = 5; // 物品的数量 int n = sizeof(items) / sizeof(items[0]); // 计算并打印在背包容量为W的情况下能装下的最大价值 printf("Maximum value in Knapsack = %d\n", knapsack(items, n, W)); return0; }