动态规划、回溯、BFS、二分、滑动窗口总结

动态规划

  • 动态规划的核心问题:重叠子问题,最优子结构,状态转移方程
  • 动态规划与记忆递归的区别:记忆递归为自顶而上的递归剪枝,动态规划为自底向上的循环迭代;
  • 正确的状态转移方程+dp[]数组:
    确定状态(原问题和子问题中变化的变量)
    ->确定dp数组的定义dp[i]
    -> 确定当前状态的'选择'并确定最优条件 (状态转移方程)
    -> 确定初始化状态
    -> 根据状态转移方程确定遍历顺序
  • 动态规划的核心方法:如何聪明的穷举
  • 代码框架:
    # 自顶向下递归的动态规划(记忆递归)
    def dp(状态1, 状态2, ...):
    	for 选择 in 所有可能的选择:
    		# 此时的状态已经因为做了选择而改变
    		result = 求最值(result, dp(状态1, 状态2, ...))
    	return result
    
    # 自底向上迭代的动态规划
    # 初始化 base case
    dp[0][0][...] = base case
    # 进行状态转移
    for 状态1 in 状态1的所有取值:
    	for 状态2 in 状态2的所有取值:
    		for ...
    			dp[状态1][状态2][...] = 求最值(选择1,选择2...)
    
    

回溯算法

  • 回溯问题的本质:决策树的遍历过程:路径 + 选择列表 + 结束条件;
  • 回溯的核心:使用深搜遍历决策树;
  • 前序遍历和后续遍历的区别:前序在进入节点执行,后续在离开节点时执行,因此,递归的本质就是在递归之前作出选择,同时在递归之后撤销选择;
  • 其核心就是多叉树的遍历问题,可以看做DFS算法
  • 代码框架:
    void backtracking(参数,路径、选择列表) {
    	if (终止条件) {
    		存放结果;
    		return;
    	}
    
    	for (选择:本层集合中元素(树中节点孩子的数量就是集合的大小)) {
    		处理节点;
    		backtracking(路径,选择列表); // 递归
    		回溯,撤销处理结果
    	}
    }
    ![image](https://img2024.cnblogs.com/blog/3027925/202404/3027925-20240416191328110-24279464.png)
    
    

BFS算法

  • DFS本质就是多叉树的遍历,只不过将条件变为图;
  • BFS的优势:找到最短路径,但是空间复杂度大(核心就是层序遍历)
  • BFS解决问题的本质:在一个图中,寻找从起点到终点的最短距离
  • 算法框架:
    int dir[4][2] = {0, 1, 1, 0, -1, 0, 0, -1}; // 表示四个方向
    // grid 是地图
    // visited标记访问过的节点,不要重复访问
    // x,y 表示开始搜索节点的下标
    void bfs(vector<vector<char>>& grid, vector<vector<bool>>& visited, int x, int y) {
    	queue<pair<int, int>> que; // 定义队列
    	que.push({x, y}); // 起始节点加入队列
    	visited[x][y] = true; // 只要加入队列,立刻标记为访问过的节点
    	int step = 0;步数
    	while(!que.empty()) { // 开始遍历队列里的元素
    		pair<int ,int> cur = que.front(); que.pop(); // 从队列取元素
    		int curx = cur.first;
    		int cury = cur.second; // 当前节点坐标
    		if (cur == target)return step//在这里判断是否到达终点
    		for (int i = 0; i < 4; i++) { // 开始想当前节点的四个方向左右上下去遍历(如果是图的话直接循环现有方向)
    			int nextx = curx + dir[i][0];
    			int nexty = cury + dir[i][1]; // 获取周边四个方向的坐标
    			if (nextx < 0 || nextx >= grid.size() || nexty < 0 || nexty >= grid[0].size()) continue;  // 坐标越界了,直接跳过
    			if (!visited[nextx][nexty]) { // 如果节点没被访问过
    				que.push({nextx, nexty});  // 队列添加该节点为下一轮要遍历的节点(将相邻节点加入队列)
    				visited[nextx][nexty] = true; // 只要加入队列立刻标记,避免重复访问
    			}
    		}
    		step++;
    	}
    
    }
    
  • 优化:双向BFS,已知target的情况下,不断交换,从目标和源同时搜索,同时保证搜索较小的集合

二分查找

  • 核心是二分的一致性,即开闭区间的一致性

  • 搜单一元素:双闭 + while等 + if相等返回 + mid直接加减1 + 出while -1

  • 搜左右边界:左闭右开 + while小于 + if相等不返回(判断情况) + mid闭区间加减1,开区间不加减 + 出while根据返回值半段是否-1

  • 算法框架:

    int binarySearch(int[] &nums, int target) {
    	int left = 0, right = ...;
    		while(...) {
    		int mid = left + (right - left)/2;
    		if(nums[mid] == target) {
    		} else if(nums[mid] < target) {
    			left = ...;
    		} else if(nums[mid] > target) {
    			right = ...;
    		}
    	}
    	return...;
    }
    
    详细模板
    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;
    }
    
  • while判断的核心:<= 为搜索区间为空,如[3,2], < 为搜索区间差一个/为空,如[2,2]、[2,2);

  • mid加减的核心:搜索区间,去除mid

滑动窗口

  • 核心:左右指针确定窗口(左闭右开)-> 增加right指针扩大窗口找到可行解 -> 缩小 left指针寻找最优解 ->重复知道right到达字符串头
  • 四个问题:
    1、当移动right扩大窗口,即加入字符时,应该更新哪些数据?

2、什么条件下,窗口应该暂停扩大,开始移动left缩小窗口?

3、当移动left缩小窗口,即移出字符时,应该更新哪些数据?

4、我们要的结果应该在扩大窗口时还是缩小窗口时进行更新?

  • 滑动窗口模板
/* 滑动窗口算法框架 */
void slidingWindow(string s) {
    // 用合适的数据结构记录窗口中的数据,根据具体场景变通
    // 比如说,我想记录窗口中元素出现的次数,就用 map
    // 我想记录窗口中的元素和,就用 int
    unordered_map<char, int> window;
    
    int left = 0, right = 0;
    while (right < s.size()) {
        // c 是将移入窗口的字符
        char c = s[right];
        window.add(c)
        // 增大窗口
        right++;
        // 进行窗口内数据的一系列更新
        ...

        /*** debug 输出的位置 ***/
        // 注意在最终的解法代码中不要 print
        // 因为 IO 操作很耗时,可能导致超时
        printf("window: [%d, %d)\n", left, right);
        /********************/
        
        // 判断左侧窗口是否要收缩
        while (left < right && window needs shrink) {
            // d 是将移出窗口的字符
            char d = s[left];
            window.remove(d)
            // 缩小窗口
            left++;
            // 进行窗口内数据的一系列更新
            ...
        }
    }
}

股票买卖

  • 股票买卖的核心就是状态转移,即在第i天持有股票的状态为j(k次交易,是否持有股票),手中资金为dp。用三维数组表达就是dp[i][k][0/1],可以通过状态压缩,将持有股票的状态压缩到k次交易中,即dp[i][j]
  • 算法框架
    dp[i][k][0 or 1]
    0 <= i <= n - 1, 1 <= k <= K
    n 为天数,大 K 为交易数的上限,0 和 1 代表是否持有股票。
    此问题共 n × K × 2 种状态,全部穷举就能搞定。
    
    for 0 <= i < n:
    	for 1 <= k <= K:
    		for s in {0, 1}:
    			dp[i][k][s] = max(buy, sell, rest)
    
    			dp[i][k][0] = max(dp[i-1][k][0], dp[i-1][k][1] + prices[i])
    			dp[i][k][1] = max(dp[i-1][k][1], dp[i-1][k-1][0] - prices[i])
    

打家劫舍

  • 其核心也是状态转移,选择是抢/不抢;而状态就是房子的索引;
  • 题目的变种是房子的构造,即分情况讨论:
    如果房子为循环数组,其核心是是否抢首/尾房间,即抢首房间/抢尾房间。求最大值。
    如果房子为树形,结合后续遍历,一样的,根据选择偷/不偷进行递归,其本质是抢->下家不抢 ,不抢->下家抢/不抢的最大值
  • 算法模板
     int rob(vector<int> &nums) {
    	int n = nums.size();
    	// dp[i] = x 表示:
    	// 从第 i 间房子开始抢劫,最多能抢到的钱为 x
    	// base case: dp[n] = 0,dp[1] = max(nums[0], nums[1])
    	vector<int> dp = dp(nums.size());
    	for (int i = 2; i < n; i--) {
    		dp[i] = max(dp[i - 1], nums[i] + dp[i - 2]);
    	}
    	return dp[nums.size() - 1];
    
    

n-sum问题

  • 核心是排序后,进行双指针搜索,小于总和时右移左指针,大于总和时候左移右指针,同时要注意跳过重复元素

  • 对于超过2个的num问题,采用固定一个数的策略,之后逐级调用2-sum函数(可以递归),注意,如果递归的话需要先排序,再递归。

  • 算法模板:

    vector<vector<int>> twoSumTarget(vector<int>& nums, int target) {
    	// nums 数组必须有序
    	sort(nums.begin(), nums.end());
    	int lo = 0, hi = nums.size() - 1;
    	vector<vector<int>> res;
    	while (lo < hi) {
    		int sum = nums[lo] + nums[hi];
    		int left = nums[lo], right = nums[hi];
    		if (sum < target) {
    			while (lo < hi && nums[lo] == left) lo++;
    		} else if (sum > target) {
    			while (lo < hi && nums[hi] == right) hi--;
    		} else {
    			res.push_back({left, right});
    			while (lo < hi && nums[lo] == left) lo++;
    			while (lo < hi && nums[hi] == right) hi--;
    		}
    	}
    	return res;
    }
    
    递归函数
    /* 注意:调用这个函数之前一定要先给 nums 排序 */
    vector<vector<int>> nSumTarget(
    	vector<int>& nums, int n, int start, int target) {
    
    	int sz = nums.size();
    	vector<vector<int>> res;
    	// 至少是 2Sum,且数组大小不应该小于 n
    	if (n < 2 || sz < n) return res;
    	// 2Sum 是 base case
    	if (n == 2) {
    		// 双指针那一套操作
    		int lo = start, hi = sz - 1;
    		while (lo < hi) {
    			int sum = nums[lo] + nums[hi];
    			int left = nums[lo], right = nums[hi];
    			if (sum < target) {
    				while (lo < hi && nums[lo] == left) lo++;
    			} else if (sum > target) {
    				while (lo < hi && nums[hi] == right) hi--;
    			} else {
    				res.push_back({left, right});
    				while (lo < hi && nums[lo] == left) lo++;
    				while (lo < hi && nums[hi] == right) hi--;
    			}
    		}
    	} else {
    		// n > 2 时,递归计算 (n-1)Sum 的结果
    		for (int i = start; i < sz; i++) {
    			vector<vector<int>> 
    				sub = nSumTarget(nums, n - 1, i + 1, target - nums[i]);
    			for (vector<int>& arr : sub) {
    				// (n-1)Sum 加上 nums[i] 就是 nSum
    				arr.push_back(nums[i]);
    				res.push_back(arr);
    			}
    			while (i < sz - 1 && nums[i] == nums[i + 1]) i++;
    		}
    	}
    	return res;
    }
    
posted @ 2024-04-17 14:26  David_Dong  阅读(28)  评论(0编辑  收藏  举报