在准备机试的过程中,我发现 OD 的机试真题风格与代码随想录中的风格存在较大差异。OD机试里出现原题的比例颇高。基于这样的情况我打算在接下来的两个月时间里,将代码随想录作为辅助学习资料来使用。
回溯算法
学习回溯算法能更好理解后面的DFS算法。
1. 组合
LeetCode例题:组合
给定两个整数 n 和 k,返回 1 … n 中所有可能的 k 个数的组合。
-
循环嵌套: 直接用 k 层 for 循环嵌套,但由于这里的 k 是变量,所以无法直接写出嵌套代码。
-
回溯算法:用 for 循环单层遍历,递归纵向遍历找到结果
时间复杂度: O ( n ⋅ 2 n ) O(n·2^n) O(n⋅2n)
空间复杂度: O ( n ) O(n) O(n)
算法步骤:
step1. 递归函数以及参数
step2. 递归结束条件:path 长度为 k,此时加入到结果集
step3. 单层遍历搜索
step4. 返回结果,包括result的长度以及result中每个路径的长度
int *path;
int **result;
int pathTop; // 存放 path 长度
int resultTop; // 存放结果集长度
// 定义回溯算法
void backtracking(int n, int k, int startIndex) {
// 如果 path 长度为 k 则将其保存到结果集中,并结束递归
if (pathTop == k) {
int *temp = (int *)malloc(sizeof(int) * k);
for (int i = 0; i < k; i++) {
temp[i] = path[i];
}
result[resultTop++] = temp;
return;
}
for(int i = startIndex; i <= n; i++) {
path[pathTop++] = i; // 把当前节点加入到 path 中
backtracking(n, k, i + 1); // 递归搜索下一个节点
pathTop--; // 递归结果后回溯
}
}
// 定义组合函数
int** combine(int n, int k, int* returnSize, int** returnColumnSizes) {
path = (int *)malloc(sizeof(int) * k);
result = (int **)malloc(sizeof(int) * 10000);
pathTop = resultTop = 0;
backtracking(n, k, 1);
*returnSize = resultTop;
*returnColumnSizes = (int *)malloc(sizeof(int) * resultTop);
for(int i = 0; i < resultTop; i++) {
(*returnColumnSizes)[i] = k;
}
return result;
}
2.组合总和III
- 回溯算法:遍历所有的 k 个元素组成的集合,如果这些元素和为 n 才保存。
step1. 画树状图分析回溯过程
step2. 回溯函数和参数
step3. 递归结束条件
step4. 单层遍历
错误分析
- 递归结束条件时,只要 path 中元素个数为 k 就结束, 而不是等 sum == n 才结束。
- 在保存 path 到 result 时,pathTop忘记++
- 在横向遍历搜索每一层时,for 循环里的 i 初始值不是 startIndex
- 在横向遍历搜索每一层时,for 循环里的 sum += path[pathTop-1]
递归结束时的回撤过程 sum -= path[–pathTop] - Leetcode提交时的处理:
returnColumnSizes要先分配内存空间,注意是sizeof(int *)
result分配内存空间时,注意是sizeof(int *)
int *path;
int **result;
int pathTop;
int resultTop;
int sum = 0;
void backtracking(int n, int k, int startIndex) {
if (pathTop == k) {
if (sum == n) {
int *temp = (int *)malloc(sizeof(int) * k);
for(int i = 0; i < k; i++) {
temp[i] = path[i];
}
result[resultTop++] = temp;
}
return; // 1# 注意 return 是在 pathTop == k 就执行
}
for(int i = startIndex; i <= 9; i++) { // 2# 注意 i = startIndex
path[pathTop++] = i; // 3# 注意 pathTop要++
sum += path[pathTop-1]; // 4# 注意 pathTop - 1 才是path现在的末尾元素
backtracking(n, k, i + 1);
sum -= path[--pathTop]; // 5# 注意 --pathTop 才是 path 现在的末尾元素
}
}
int** combinationSum3(int k, int n, int* returnSize, int** returnColumnSizes) {
path = (int *)malloc(sizeof(int) * k);
result = (int **)malloc(sizeof(int *) * 10000); // 6# 注意这里是 sizeof(int *)
pathTop = resultTop = 0;
backtracking(n, k, 1);
*returnSize = resultTop;
*returnColumnSizes = (int *)malloc(sizeof(int) * resultTop); // 7# 注意这里是 sizeof(int)
for(int i = 0; i < resultTop; i++) {
(*returnColumnSizes)[i] = k;
}
return result;
}
3.电话号码字母组合
- 回溯算法:与前面不同的是,这里的组合是在多个不同的集合中选取元素来进行组合。
step1. 先画树状图发现,输入的数字控制搜索的深度,每个数字映射的字母个数控制搜索的宽度。
step2. 参数及函数,注意参数中的 index 表示的是当前在搜索的数字.(index是digits中的下标,不是数字本身)
step3. 结束递归条件:当路径中的长度等于数字的长度时,保存到结果集。
step4. 遍历单层。
错误分析
- letterMap是一维数组
- 最后要分输入空和输入非空来选择返回的结果。
char *path;
char **result;
int pathTop;
int resultTop;
char *letterMap[10] = { // 1# letterMap是一维数组
"", // 0
"", // 1
"abc",
"def",
"ghi",
"jkl",
"mno",
"pqrs",
"tuv",
"wxyz" // 9
};
void backtracking(char *digits, int index) {
if (pathTop == strlen(digits)) {
char *temp = (char *)malloc(sizeof(char) * (strlen(digits) + 1));
for(int i = 0; i < strlen(digits); i++) {
temp[i] = path[i];
}
temp[strlen(digits)] = '\0';
result[resultTop++] = temp;
return;
}
int digit = digits[index] - '0';
for(int i = 0; i < strlen(letterMap[digit]); i++) {
path[pathTop++] = letterMap[digit][i];
backtracking(digits, index + 1);
pathTop--;
}
}
char ** letterCombinations(char * digits, int* returnSize) {
path = (char *)malloc(sizeof(char) * strlen(digits));
result = (char **)malloc(sizeof(char *) * 1000);
pathTop = resultTop = 0;
// 2# 若digits数组中元素个数为0,返回空集
if (strlen(digits) == 0) {
*returnSize = resultTop;
return result;
}
else {
backtracking(digits, 0);
*returnSize = resultTop;
return result;
}
}
4.组合综合I
- 回溯算法:与前面不同的是,这里的搜索深度取决于当前路径中元素的和与 target 的大小差距。
step1. 先画树状图,选取一个节点后,下一层的搜索集合中包含这个节点本身及其后面的元素
step2. 参数及函数,注意参数中的 startIndex 表示的是当前在搜索的数字.(startindex是数组中的下标,不是数字本身)
step3. 结束递归条件
step4. 遍历单层。
错误分析
- 存储结果时,循环条件的边界是 pathTop 而不是数组长度
- 执行单层遍历时,把当前节点加入 path 的同时,不要忘记更新 sum
- 注意在调用回溯函数时,正确分配内存。
int *path;
int **result;
int pathTop;
int resultTop;
int sum;
int *resultColumnSizes; // 存放每个结果的长度
void backtracking(int *candidates, int target, int startIndex, int candidatesSize) {
if (sum > target) return;
if (sum == target) {
int *temp = (int *)malloc(sizeof(int) * pathTop);
for(int i = 0; i < pathTop; i++) { // 1# 循环条件写成 i < candidatesSize
temp[i] = path[i];
}
resultColumnSizes[resultTop] = pathTop;
result[resultTop++] = temp;
return;
}
for(int i = startIndex; i < candidatesSize; i++) {
path[pathTop++] = candidates[i];
sum += path[pathTop - 1]; // 2# 忘记写求和
backtracking(candidates, target, i, candidatesSize);
sum -= path[--pathTop];
}
}
int** combinationSum(int* candidates, int candidatesSize, int target, int* returnSize, int** returnColumnSizes) {
path = (int *)malloc(sizeof(int) * 1000);
result = (int **)malloc(sizeof(int *) * 1000);
pathTop = resultTop = sum = 0;
resultColumnSizes = (int *)malloc(sizeof(int) * 1000); // 3# 忘记分配内存
backtracking(candidates, target, 0, candidatesSize);
*returnSize = resultTop;
*returnColumnSizes = (int *)malloc(sizeof(int) * resultTop); // 4# 忘记分配内存
for(int i = 0; i < resultTop; i++) {
(*returnColumnSizes)[i] = resultColumnSizes[i];
}
return result;
}
5.组合综合II
- 回溯算法:搜索的集合中有重复的元素,但是要求最后搜索结果中的组合不能重复,所以要去重。
step1. 先画树状图,选取一个节点后,下一层的搜索集合中包含这个节点本身及其后面的元素
step2. 参数及函数,由于 startIndex 本身就可以去重,所以我们只要保证在本层遍历时不搜索已经搜过的值
step3. 结束递归条件
step4. 遍历单层。
错误分析
- 递归条件中不需要 “|| pathTop == candidatesSize”, 因为遍历到最深处后,会自动回溯。
- 使用 startIndex 去重,对数组排序后,如果本层遍历时,遇到一个已经访问过的节点,则跳过
- 快速排序 qsort() ,固定写法
int *path;
int **result;
int pathTop;
int resultTop;
int sum;
int *resultColumnSizes;
int cmp(const void *a, const void *b) {
return *((int *)a) - *((int *)b);
}
void backtracking(int *candidates, int candidatesSize, int target, int startIndex) {
if (sum > target) return; // 1# 不需要 "|| pathTop == candidatesSize", 因为遍历到最深处后,会自动回溯。
if (sum == target) {
int *temp = (int *)malloc(sizeof(int) * pathTop);
for(int i = 0; i < pathTop; i++) {
temp[i] = path[i];
}
resultColumnSizes[resultTop] = pathTop;
result[resultTop++] = temp;
return;
}
for(int i = startIndex; i < candidatesSize; i++) {
if (i > startIndex && candidates[i] == candidates[i - 1]) continue; // 2# 使用startIndex去重,对数组排序后,如果本层遍历时,遇到一个已经访问过的节点,则跳过
path[pathTop++] = candidates[i];
sum += path[pathTop - 1];
backtracking(candidates, candidatesSize, target, i + 1);
sum -= path[--pathTop];
}
}
int** combinationSum2(int* candidates, int candidatesSize, int target, int* returnSize, int** returnColumnSizes) {
path = (int *)malloc(sizeof(int) * candidatesSize);
result = (int **)malloc(sizeof(int *) * 10000);
resultColumnSizes = (int *)malloc(sizeof(int) * 10000);
pathTop = resultTop = sum = 0;
qsort(candidates, candidatesSize, sizeof(int), cmp); // 3# 快速排序,固定写法
backtracking(candidates, candidatesSize, target, 0);
*returnSize = resultTop;
*returnColumnSizes = (int *)malloc(sizeof(int) * resultTop);
for(int i = 0; i < resultTop; i++) {
(* returnColumnSizes)[i] = resultColumnSizes[i];
}
return result;
}
BFS 和 DFS
1. 岛屿数量(深搜版)
- DFS算法:已知一个点是陆地的时候,想要搜索出整个陆地的范围,可以考虑DFS和BFS。
整体步骤
step0. 动态分配内存、输入图、初始化 visited
step1. 两个 for 循环遍历整个图
step2. 如果遍历到陆地且是一个没有访问过的陆地,则 count++ ,并且用 DFS 把整个陆地搜索出来
step3. 整个陆地搜索出来后,继续在两个 for 循环中遍历图。
step4. 释放内存
DFS步骤
step1. 定义 4 个方向的二维数组
step2. 确定 DFS 函数的参数:图、访问记录、当前坐标、图的尺寸
step3. 遍历 4 个方向, 每个方向中计算 nextX 和 nextY 来记录陆地。
step4. 在每个方向中递归调用 DFS 函数。
错误分析:注意笔误
- free() 写成 free[]
- 函数声明的时候 void dfs(grid, visited …) 忘记写参数的类型
- grid[nextX][nextY] == 1 写成 grid[nextX][nextY == 1]
完整代码
#include <stdio.h>
#include <stdlib.h>
int dir[4][2] = {{0, 1}, {1, 0}, {0, -1}, {-1, 0}};
void dfs(int **grid, int **visited, int x, int y, int row, int col) {
// 深度优先算法搜索策略是:搜索一个方向,直到该方向搜索完毕,再搜索另一个方向。
for(int i = 0; i < 4; i++) {
int nextX = x + dir[i][0];
int nextY = y + dir[i][1];
if (nextX < 0 || nextX >= row || nextY < 0 || nextY >= col) continue;
if (visited[nextX][nextY] == 0 && grid[nextX][nextY] == 1) {
visited[nextX][nextY] = 1;
dfs(grid, visited, nextX, nextY, row, col);
}
}
};
int main(){
int row, col;
scanf("%d %d", &row, &col);
int **grid = (int **)malloc(sizeof(int *) * row);
for(int i = 0; i < row; i++) {
grid[i] = (int *)malloc(sizeof(int) * col);
}
int **visited = (int **)malloc(sizeof(int *) * row);
for(int i = 0; i < row; i++) {
visited[i] = (int *)malloc(sizeof(int) * col);
}
for(int i = 0; i < row; i++) {
for(int j = 0; j < col; j++) {
visited[i][j] = 0;
scanf("%d", &grid[i][j]);
}
}
int count = 0;
for(int i = 0; i < row; i++) {
for(int j = 0; j < col; j++) {
if (grid[i][j] == 1 && visited[i][j] == 0) {
count++;
visited[i][j] = 0;
dfs(grid, visited, i, j, row, col);
}
}
}
printf("%d", count);
for(int i = 0; i < row; i++) {
free(grid[i]);
free(visited[i]);
}
free(grid);
free(visited);
return 0;
}
2. 岛屿数量(广搜版)
- BFS算法:已知一个点是陆地的时候,想要搜索出整个陆地的范围,可以考虑DFS和BFS。
整体步骤
step0. 动态分配内存、输入图、初始化 visited
step1. 两个 for 循环遍历整个图
step2. 如果遍历到陆地且是一个没有访问过的陆地,则 count++ ,并且用 BFS 把整个陆地搜索出来
step3. 整个陆地搜索出来后,继续在两个 for 循环中遍历图。
step4. 释放内存
BFS步骤
step1. 定义 4 个方向的二维数组
step2. 确定 BFS 函数的参数:图、访问记录、当前坐标、图的尺寸
step3. 创建队列结构体并分配空间
step4. 把当前节点加入队列并标记已访问
step4. 只要队列的头指针小于尾指针,就一直循环:取出队列头部,搜索头部附近四个方向的节点,符合要求的点放入队列尾部。
错误分析
- Queue head = queue[front++] front变量同名
- if() 中忘写 == : if(visited[nextX][nextY] && grid[nextX][nextY])
BFS 队列操作笔记
队列介绍
在 BFS(广度优先搜索)中,队列用于存储待处理的节点。队列是一个先进先出(FIFO)的数据结构。我们使用两个指针来管理队列的操作:
front
:表示队列的前端,即将要被处理的节点。rear
:表示队列的后端,即下一个将被加入队列的节点。
BFS 执行流程
- 初始化队列,
front = 0, rear = 0
。 - 将起始节点加入队列,
rear++
。 - 进入 while 循环,直到队列为空:
i. 从队列中取出节点(front++
)。
ii. 处理当前节点的相邻节点,如果相邻节点未被访问且合法,则加入队列(rear++
),并标记为已访问。 - 循环结束时,所有可达节点都已被处理。
示例:
假设有一个 5x5 的网格,1
表示陆地,0
表示水域。我们从 (0, 0)
开始进行 BFS。
front
和 rear
变化过程
步骤 | 队列状态 | front | rear | 操作说明 |
---|---|---|---|---|
初始 | [] | 0 | 0 | 队列为空 |
步骤 1 | [(0, 0)] | 0 | 1 | 加入 (0, 0) 到队列 |
步骤 2 | [(0, 0), (1, 0)] | 1 | 2 | 处理 (0, 0) ,加入 (1, 0) |
步骤 3 | [(0, 0), (1, 0), (1, 1)] | 2 | 3 | 处理 (1, 0) ,加入 (1, 1) |
步骤 4 | [(0, 0), (1, 0), (1, 1)] | 3 | 3 | 处理 (1, 1) ,没有新元素加入 |
完整代码
#include <stdio.h>
#include <stdlib.h>
int dir[4][2] = {{0, 1}, {1, 0}, {-1, 0}, {0, -1}};
typedef struct {
int x;
int y;
} Queue;
void bfs(int **grid, int **visited, int x, int y, int row, int col) {
Queue *queue = (Queue *)malloc(sizeof(Queue) * row * col);
int front = 0;
int rear = 0;
queue[rear].x = x;
queue[rear].y = y;
rear++;
visited[x][y] = 1;
while(front < rear) {
// 把队列头节点取出来,搜索头节点的四个方向,符合要求的加入队列尾部
Queue head = queue[front++]; // 1# Queue front = queue[front++] front重名
for(int i = 0; i < 4; i++) {
int nextX = head.x + dir[i][0];
int nextY = head.y + dir[i][1];
if (nextX < 0 || nextX >= row || nextY <0 || nextY >= col) continue;
// 2# if() 中忘写 == : if(visited[nextX][nextY] && grid[nextX][nextY])
if (visited[nextX][nextY] == 0 && grid[nextX][nextY] == 1) {
queue[rear].x = nextX;
queue[rear].y = nextY;
rear++;
visited[nextX][nextY] = 1;
}
}
}
};
int main() {
int row, col;
scanf("%d %d", &row, &col);
int **grid = (int **)malloc(sizeof(int *) * row * col);
int **visited = (int **)malloc(sizeof(int *) * row * col);
for(int i = 0; i < row; i++) {
grid[i] = (int *)malloc(sizeof(int) * col);
visited[i] = (int *)malloc(sizeof(int) * col);
}
for(int i = 0; i < row; i++) {
for(int j = 0; j < col; j++) {
visited[i][j] = 0;
scanf("%d", &grid[i][j]);
}
}
int count = 0;
for(int i = 0; i < row; i++) {
for(int j = 0; j < col; j++) {
if (visited[i][j] == 0 && grid[i][j] == 1) {
count ++;
bfs(grid, visited, i, j, row, col);
}
}
}
printf("%d", count);
return 0;
}
E01:流浪地球
1. 流浪地球
整体思路:利用广度优先搜索(BFS)在一个环状结构上传播发动机启动时刻。
step1. 宏定义最大发动机数量和默认启动时间最大
step2. 用一个全局变量记录发动机最终启动时间。用一个队列结构体记录发动机的位置和 BFS 搜索过程中的启动时间。
step3. 读取发动机总数和手动启动的总数
step4. 初始化所有发动机启动时间为最大
step5. 读取手动启动的时刻和位置
step6. 把手动启动的发动机加入队列中,并进行 BFS 搜索来更新所有发动机的实际启动时间。
step7. 在所有发动机的实际启动时间中找到最大值。
BFS思路:
step0. 为队列分配内存空间,初始化头尾指针,把手动启动的发动机放到队列中。
step1. 取出头节点
step2. 搜索头节点附近的发动机,如果附近的发动机通过传播启动,则更新启动时间并加入到队列尾部。
完整代码
#include <stdio.h>
#include <stdlib.h>
#define MAXLEN 1000
#define INF 1001
int launches[MAXLEN];
typedef struct {
int pos; // 队列中发动机的位置
int time; // 队列中发动机的启动时间
} Queue;
void bfs(int n) {
Queue *queue = (Queue *)malloc(sizeof(Queue) * MAXLEN);
int front = 0;
int rear = 0;
for(int i = 0; i < n; i++) {
if (launches[i] != INF) {
queue[rear].pos = i;
queue[rear].time = launches[i];
rear ++;
}
}
while(front < rear) {
Queue cur = queue[front++]; // 1# front 没有 ++
int dir[2] = {(cur.pos + 1 + n) % n, (cur.pos - 1 + n) % n};
for (int i = 0; i < 2; i++) {
int next = dir[i];
if (launches[next] > cur.time + 1) {
launches[next] = cur.time + 1;
queue[rear].pos = next;
queue[rear].time = cur.time + 1;
rear++;
}
}
}
};
int main() {
int n, e; // n发动机总数 e手动启动总数
scanf("%d %d", &n, &e);
// 2# 没有初始化发动机启动时间为 INF
for(int i = 0; i < n; i++) {
launches[i] = INF;
}
for(int i = 0; i < e; i++) {
int t, p;
scanf("%d %d", &t, &p);
launches[p] = t;
}
bfs(n);
int maxTime = 0;
int maxCount = 0;
for(int i = 0; i < n; i++) {
if (launches[i] > maxTime) {
maxTime = launches[i];
maxCount = 0;
}
else if (launches[i] == maxTime) {
maxCount ++;
}
}
printf("%d\n", maxCount);
int count = 0;
for(int i = 0; i < n; i++) {
if (launches[i] == maxTime) {
count ++;
if (count != maxCount) printf("%d ", i);
else printf("%d", i);
}
}
return 0;
}