BFS算法框架
BFS是广度优先搜索,其核心思想是把一些问题抽象成图,从一个点开始,向四周扩散。BFS通常是用来解决最短路的问题,问题的本质就是让你在一副“图”中找到从起点 start 到终点 target 的最近距离。
比如:
- 走迷宫,有的格子是围墙不能走(限制条件),从起点到终点的最短距离是多少?
- 两个单词,要求通过替换某些字母,把其中一个变成另一个,每次只能替换一个字母,最少要替换几次?
- 求二叉树最小深度和高度 111. 二叉树的最小深度
- 转盘锁,每次波动一个位置,把一个密码变成目标密码,并且有些密码是死亡密码不能经过它,问最小的旋转次数752. 打开转盘锁
- 连连看游戏,消除两个方块的条件不仅是图案相同,还要保证两个方块之间的最短连线不能多于两个拐点。
BFS 算法框架
// 计算从起点 start 到终点 target 的最近距离int BFS(Node start, Node target) {queue<Node> q; // 核心数据结构unordered_set<Node> visited; // 避免走回头路q.push(start); // 将起点加入队列visited.insert(start);//起点已经访问过了int step = 0; // 记录扩散的步数while ( !q.empty() ) {int sz = q.size();/* 将当前队列中的所有节点向四周扩散 */for (int i = 0; i < sz; i++) {Node cur = q.front();q.pop();/* 划重点:这里判断是否到达终点 */if (cur == target)return step;if (满足限制条件) continue;//根据题目的限制条件,有些分支需要跳过/* 将 cur 的相邻节点加入队列 */for (Node x : cur.adj())if (x not in visited) {q.push(x);visited.insert(x);}}/* 划重点:更新步数在这里 */step++;}return step;}
队列q 是BFS 的核心数据结构;cur.adj()泛指cur相邻的节点,比如说二维数组中,cur上下左右四面的位置就是相邻节点;visited的主要作用是防止走回头路,大部分时候都是必须的,但是像一般的二叉树结构,没有子节点到父节点的指针,不会走回头路就不需要visited。
二分搜索框架
寻找一个数(基本的二分搜索)
int binary_search(int[] nums, int target) {int left = 0, right = nums.length - 1;while(left <= right) {int mid = left + (right - left) / 2;if (nums[mid] < target) {left = mid + 1;} else if (nums[mid] > target) {right = mid - 1;} else if(nums[mid] == target) {// 直接返回return mid;}}// 直接返回return -1;}
寻找左侧边界的二分搜索
int left_bound(int[] nums, int target) {int left = 0, right = nums.length - 1;while (left <= right) {int mid = left + (right - left) / 2;if (nums[mid] < target) {left = mid + 1;} else if (nums[mid] > target) {right = mid - 1;} else if (nums[mid] == target) {// 别返回,锁定左侧边界right = mid - 1;}}// 最后要检查 left 越界的情况if (left >= nums.length || nums[left] != target)return -1;return left;}
寻找右侧边界的二分搜索
int right_bound(int[] nums, int target) {int left = 0, right = nums.length - 1;while (left <= right) {int mid = left + (right - left) / 2;if (nums[mid] < target) {left = mid + 1;} else if (nums[mid] > target) {right = mid - 1;} else if (nums[mid] == target) {// 别返回,锁定右侧边界left = mid + 1;}}// 最后要检查 right 越界的情况if (right < 0 || nums[right] != target)return -1;return right;}
动态规划解题套路框架
回溯算法解题套路框架
解决一个回溯问题,实际上就是一个决策树的遍历过程。你只需要思考 3 个问题:
1、**路径:也就是已经做出的选择。
2、选择列表:也就是你当前可以做的选择。
3、结束条件:也就是到达决策树底层,无法再做选择的条件。
回溯算法的框架:**
result = []def backtrack(路径, 选择列表):if 满足结束条件:result.add(路径)returnfor 选择 in 选择列表:做选择backtrack(路径, 选择列表)撤销选择
其核心就是 for 循环里面的递归,在递归调用之前「做选择」,在递归调用之后「撤销选择」。
写backtrack函数时,需要维护走过的「路径」和当前可以做的「选择列表」,当触发「结束条件」时,将「路径」记入结果集。
其实想想看,回溯算法和动态规划是不是有点像呢?动态规划的三个需要明确的点就是「状态」「选择」和「base case」,是不是就对应着走过的「路径」,当前的「选择列表」和「结束条件」?
某种程度上说,动态规划的暴力求解阶段就是回溯算法。只是有的问题具有重叠子问题性质,可以用 dp table 或者备忘录优化,将递归树大幅剪枝,这就变成了动态规划。而今天的两个问题,都没有重叠子问题,也就是回溯算法问题了,复杂度非常高是不可避免的。
例子:
- 46. 全排列 47. 全排列 II
51. N皇后**
滑动窗口算法
/* 滑动窗口算法框架 */void slidingWindow(string s, string t) {//在s中寻找满足条件的tunordered_map<char, int> need, window;for (char c : t) need[c]++;int left = 0, right = 0;int valid = 0;while (right < s.size()) {// c 是将移入窗口的字符char c = s[right];// 右移窗口right++;// 进行窗口内数据的一系列更新,【根据情况更改】,如if (need.count(c)) {window[c]++;if (window[c] == need[c]) valid++;}......// 判断左侧窗口是否要收缩while (window needs shrink) {// d 是将移出窗口的字符char d = s[left];// 左移窗口left++;// 进行窗口内数据的一系列更新,【根据情况更改】,如if (need.count(d)) {if(window[d] == need[d]) valid--;window[d]--;}......}}}
例子:
- 567. 字符串的排列
- 438. 找到字符串中所有字母异位词
- 3. 无重复字符的最长子串
参考
公众号:【labuladong】 ,文章链接 — 列表形式目录
