代码随想录第3 - 5天 | 回溯算法、DFS、BFS基本思想 | OD E01

在准备机试的过程中,我发现 OD 的机试真题风格与代码随想录中的风格存在较大差异。OD机试里出现原题的比例颇高。基于这样的情况我打算在接下来的两个月时间里,将代码随想录作为辅助学习资料来使用。

回溯算法

学习回溯算法能更好理解后面的DFS算法。

1. 组合

LeetCode例题:组合
给定两个整数 n 和 k,返回 1 … n 中所有可能的 k 个数的组合。

  • 循环嵌套: 直接用 k 层 for 循环嵌套,但由于这里的 k 是变量,所以无法直接写出嵌套代码。

  • 回溯算法:用 for 循环单层遍历,递归纵向遍历找到结果
    时间复杂度: O ( n ⋅ 2 n ) O(n·2^n) O(n2n)
    空间复杂度: 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

LeetCode例题:组合总和III

  • 回溯算法:遍历所有的 k 个元素组成的集合,如果这些元素和为 n 才保存。
    step1. 画树状图分析回溯过程
    step2. 回溯函数和参数
    step3. 递归结束条件
    step4. 单层遍历

错误分析

  1. 递归结束条件时,只要 path 中元素个数为 k 就结束, 而不是等 sum == n 才结束。
  2. 在保存 path 到 result 时,pathTop忘记++
  3. 在横向遍历搜索每一层时,for 循环里的 i 初始值不是 startIndex
  4. 在横向遍历搜索每一层时,for 循环里的 sum += path[pathTop-1]
    递归结束时的回撤过程 sum -= path[–pathTop]
  5. 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.电话号码字母组合

LeetCode例题:电话号码字母组合

  • 回溯算法:与前面不同的是,这里的组合是在多个不同的集合中选取元素来进行组合。

step1. 先画树状图发现,输入的数字控制搜索的深度,每个数字映射的字母个数控制搜索的宽度。
step2. 参数及函数,注意参数中的 index 表示的是当前在搜索的数字.(index是digits中的下标,不是数字本身)
step3. 结束递归条件:当路径中的长度等于数字的长度时,保存到结果集。
step4. 遍历单层。

错误分析

  1. letterMap是一维数组
  2. 最后要分输入空和输入非空来选择返回的结果。
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

LeetCode例题:组合综合I

  • 回溯算法:与前面不同的是,这里的搜索深度取决于当前路径中元素的和与 target 的大小差距。

step1. 先画树状图,选取一个节点后,下一层的搜索集合中包含这个节点本身及其后面的元素
step2. 参数及函数,注意参数中的 startIndex 表示的是当前在搜索的数字.(startindex是数组中的下标,不是数字本身)
step3. 结束递归条件
step4. 遍历单层。

错误分析

  1. 存储结果时,循环条件的边界是 pathTop 而不是数组长度
  2. 执行单层遍历时,把当前节点加入 path 的同时,不要忘记更新 sum
  3. 注意在调用回溯函数时,正确分配内存。
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

LeetCode例题:组合综合II

  • 回溯算法:搜索的集合中有重复的元素,但是要求最后搜索结果中的组合不能重复,所以要去重。

step1. 先画树状图,选取一个节点后,下一层的搜索集合中包含这个节点本身及其后面的元素
step2. 参数及函数,由于 startIndex 本身就可以去重,所以我们只要保证在本层遍历时不搜索已经搜过的值
step3. 结束递归条件
step4. 遍历单层。

错误分析

  1. 递归条件中不需要 “|| pathTop == candidatesSize”, 因为遍历到最深处后,会自动回溯。
  2. 使用 startIndex 去重,对数组排序后,如果本层遍历时,遇到一个已经访问过的节点,则跳过
  3. 快速排序 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 函数。

错误分析:注意笔误

  1. free() 写成 free[]
  2. 函数声明的时候 void dfs(grid, visited …) 忘记写参数的类型
  3. 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. 只要队列的头指针小于尾指针,就一直循环:取出队列头部,搜索头部附近四个方向的节点,符合要求的点放入队列尾部。

错误分析

  1. Queue head = queue[front++] front变量同名
  2. if() 中忘写 == : if(visited[nextX][nextY] && grid[nextX][nextY])

BFS 队列操作笔记

队列介绍

在 BFS(广度优先搜索)中,队列用于存储待处理的节点。队列是一个先进先出(FIFO)的数据结构。我们使用两个指针来管理队列的操作:

  • front:表示队列的前端,即将要被处理的节点。
  • rear:表示队列的后端,即下一个将被加入队列的节点。

BFS 执行流程

  1. 初始化队列,front = 0, rear = 0
  2. 将起始节点加入队列,rear++
  3. 进入 while 循环,直到队列为空:
    i. 从队列中取出节点(front++)。
    ii. 处理当前节点的相邻节点,如果相邻节点未被访问且合法,则加入队列(rear++),并标记为已访问。
  4. 循环结束时,所有可达节点都已被处理。

示例:

假设有一个 5x5 的网格,1 表示陆地,0 表示水域。我们从 (0, 0) 开始进行 BFS。

frontrear 变化过程

步骤队列状态frontrear操作说明
初始[]00队列为空
步骤 1[(0, 0)]01加入 (0, 0) 到队列
步骤 2[(0, 0), (1, 0)]12处理 (0, 0),加入 (1, 0)
步骤 3[(0, 0), (1, 0), (1, 1)]23处理 (1, 0),加入 (1, 1)
步骤 4[(0, 0), (1, 0), (1, 1)]33处理 (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;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值