内容来源于书本与网络,仅供个人学习交流使用 笔者@StuBoo
未完待续
1.回溯法 (Backtracking)
应用:组合、排列、子集等组合型问题,0/1背包问题、图的着色问题等。
时空复杂度:时空复杂度较高,指数级别。时间复杂度:O(2^n) 或更高,其中 n 是问题规模。空间复杂度:O(n) 或更高,取决于递归深度。
特性:
- 通过深度优先搜索遍历解空间。
- 需要撤销选择,回溯到上一步,尝试其他选择。
通常用于解决组合型问题。
1.1 总述
回溯是一种经典的搜索算法,通常用于解决组合、排列、子集等问题。
回溯法既不属于动态规划也不属于贪心算法。回溯法是一种搜索算法,它通过不断尝试各种可能的选择,然后回溯(撤销选择)来找到问题的解。回溯算法通常用于求解组合、排列、子集等问题。回溯法是一种通用的搜索算法,适用于一些组合、排列、子集等问题,而不限于最优化问题。
虽然回溯法与动态规划和贪心算法都属于求解最优化问题的算法范畴,但它们有很大的区别:
动态规划:动态规划通常通过保存子问题的解来避免重复计算,具有最优子结构。典型的动态规划问题有斐波那契数列、背包问题等。
贪心算法:贪心算法则通过每一步的局部最优选择来期望达到全局最优解,不进行回溯。典型的贪心算法问题有霍夫曼编码、最小生成树算法等。
回溯法更注重搜索整个解空间,通过深度优先搜索的方式,逐步尝试各种选择,遇到无效选择时回溯撤销选择,直到找到问题的解或遍历完整个解空间。
回溯算法基本思想:
- 递归: 使用递归实现对解空间的深度优先搜索。
- 选择: 在每一步根据问题的要求做出选择,尝试不同的可能性。
- 撤销选择: 在递归完成后,撤销当前选择,进行回溯,继续尝试其他可能性。
关键点和优化:
- 剪枝: 在递归的过程中,通过一些条件判断提前终止不符合条件的搜索路径,减少搜索空间,提高效率。
- 状态重置: 在递归完成后,需要将当前选择撤销,进行回溯,保持状态的一致性。
- 选择列表: 在每一步的递归中,需要考虑当前可以做的选择,通常使用循环遍历选择列表。
- 记录路径: 如果需要记录路径或结果,需要使用合适的数据结构进行记录。
典型问题类型:
组合问题: 如组合总和、子集、电话号码的字母组合等。
排列问题: 如全排列、字符串的全排列等。
N 皇后问题: 在 n×n 棋盘上放置 n 个皇后,使其不能相互攻击。
总体思路:
- 确定问题的解空间和选择列表。
- 编写回溯函数,实现对解空间的深度优先搜索。
- 在递归中做出选择、递归到下一层、撤销选择,实现回溯。
1.2 LeetCode实战
全排列
给定一个不含重复数字的数组 nums ,返回其 所有可能的全排列 。你可以 按任意顺序 返回答案。
#include <vector>
class Solution {
public:
std::vector<std::vector<int>> permute(std::vector<int>& nums) {
std::vector<std::vector<int>> result;
if (nums.empty()) {
return result;
}
std::vector<int> current; // 用于存储当前排列
std::vector<bool> used(nums.size(), false); // 记录数字是否被使用过
backtrack(nums, current, used, result);
return result;
}
private:
void backtrack(const std::vector<int>& nums, std::vector<int>& current,
std::vector<bool>& used, std::vector<std::vector<int>>& result) {
// 终止条件:当前排列长度达到数组长度
if (current.size() == nums.size()) {
result.push_back(current); // 将当前排列加入结果集
return;
}
for (int i = 0; i < nums.size(); ++i) {
if (!used[i]) {
// 选择当前数字,递归到下一层
current.push_back(nums[i]);
used[i] = true;
backtrack(nums, current, used, result);
// 撤销选择,进行回溯
current.pop_back();
used[i] = false;
}
}
}
};
在这个实现中,used 数组用于记录数字是否被使用过,防止重复选择。回溯函数 backtrack 中,在每一层递归中选择当前数字,递归到下一层,然后撤销选择进行回溯。
电话号码的字母组合
给定一个仅包含数字 2-9 的字符串,返回所有它能表示的字母组合。答案可以按 任意顺序 返回。
给出数字到字母的映射如下(与电话按键相同)。注意 1 不对应任何字母。
#include <vector>
#include <string>
class Solution {
public:
std::vector<std::string> letterCombinations(std::string digits) {
std::vector<std::string> result;
if (digits.empty()) {
return result;
}
std::vector<std::string> mapping = {"", "", "abc", "def", "ghi", "jkl", "mno", "pqrs", "tuv", "wxyz"};
std::string current;
backtrack(digits, 0, current, mapping, result);
return result;
}
private:
void backtrack(const std::string& digits, int index, std::string& current,
const std::vector<std::string>& mapping, std::vector<std::string>& result) {
if (index == digits.size()) {
// 当递归到字符串末尾时,将当前组合加入结果集
result.push_back(current);
return;
}
int digit = digits[index] - '0';
const std::string& letters = mapping[digit];
for (char letter : letters) {
// 选择当前字母,递归到下一层
current.push_back(letter);
backtrack(digits, index + 1, current, mapping, result);
// 撤销选择,进行回溯
current.pop_back();
}
}
};
实现中,mapping 数组存储了数字和字母的映射关系。回溯函数 backtrack 通过递归地选择当前字母,构建可能的组合,并在递归完成后撤销选择进行回溯。
组合总和
给你一个 无重复元素 的整数数组 candidates 和一个目标整数 target ,找出 candidates 中可以使数字和为目标数 target 的 所有 不同组合 ,并以列表形式返回。你可以按 任意顺序 返回这些组合。
candidates 中的 同一个 数字可以 无限制重复被选取 。如果至少一个数字的被选数量不同,则两种组合是不同的。
对于给定的输入,保证和为 target 的不同组合数少于 150 个。
class Solution {
public:
std::vector<std::vector<int>> combinationSum(std::vector<int>& candidates, int target) {
std::vector<std::vector<int>> result;
std::vector<int> current;
backtrack(candidates, target, 0, current, result);
return result;
}
private:
void backtrack(const std::vector<int>& candidates, int target, int start,
std::vector<int>& current, std::vector<std::vector<int>>& result) {
if (target == 0) {
// 当目标值为0时,将当前组合加入结果集
result.push_back(current);
return;
}
for (int i = start; i < candidates.size(); ++i) {
if (target - candidates[i] >= 0) {
// 选择当前数字,并递归下一层
current.push_back(candidates[i]);
backtrack(candidates, target - candidates[i], i, current, result);
// 撤销选择,进行回溯
current.pop_back();
}
}
}
};
N 皇后
按照国际象棋的规则,皇后可以攻击与之处在同一行或同一列或同一斜线上的棋子。
n 皇后问题 研究的是如何将 n 个皇后放置在 n×n 的棋盘上,并且使皇后彼此之间不能相互攻击。
给你一个整数 n ,返回所有不同的 n 皇后问题 的解决方案。
每一种解法包含一个不同的 n 皇后问题 的棋子放置方案,该方案中 'Q' 和 '.' 分别代表了皇后和空位。
#include <vector>
#include <string>
class Solution {
public:
std::vector<std::vector<std::string>> solveNQueens(int n) {
std::vector<std::vector<std::string>> result;
if (n <= 0) {
return result;
}
std::vector<std::string> board(n, std::string(n, '.')); // 初始化棋盘
backtrack(n, 0, board, result);
return result;
}
private:
void backtrack(int n, int row, std::vector<std::string>& board,
std::vector<std::vector<std::string>>& result) {
// 终止条件:已经放置完所有皇后
if (row == n) {
result.push_back(board); // 将当前棋盘加入结果集
return;
}
for (int col = 0; col < n; ++col) {
if (isValid(board, row, col, n)) {
// 在当前位置放置皇后,递归到下一层
board[row][col] = 'Q';
backtrack(n, row + 1, board, result);
// 撤销选择,进行回溯
board[row][col] = '.';
}
}
}
bool isValid(const std::vector<std::string>& board, int row, int col, int n) {
// 检查同一列是否有皇后
for (int i = 0; i < row; ++i) {
if (board[i][col] == 'Q') {
return false;
}
}
// 检查左上到右下斜线是否有皇后
for (int i = row - 1, j = col - 1; i >= 0 && j >= 0; --i, --j) {
if (board[i][j] == 'Q') {
return false;
}
}
// 检查左下到右上斜线是否有皇后
for (int i = row - 1, j = col + 1; i >= 0 && j < n; --i, ++j) {
if (board[i][j] == 'Q') {
return false;
}
}
return true;
}
};
实现中,使用回溯算法递归地尝试在每一行放置皇后,检查是否满足国际象棋的规则。通过不断递归和回溯,生成所有不同的 N 皇后问题的解决方案。
1.3 模板
基本模板:
class Solution {
public:
// 主函数,入口点
void solve(/*其他参数*/) {
// 初始化结果集等必要的数据结构
// ...
// 调用回溯函数
backtrack(/*参数列表*/);
// 打印结果或其他操作
printResult();
}
private:
// 回溯函数
void backtrack(/*参数列表*/) {
// 终止条件
if (/*满足条件*/) {
// 处理当前解
processSolution();
return;
}
// 递归处理每一步的选择
for (/*每个选择*/) {
// 做出选择
makeChoice();
// 递归到下一层
backtrack(/*参数列表*/);
// 撤销选择,进行回溯
undoChoice();
}
}
}
2.动态规划 (Dynamic Programming)
应用:最优子结构性质的问题,问题可以被分解为重叠的子问题,具有递归关系,子问题的解可以被重复利用,例如最长公共子序列、背包问题等。
时空复杂度:时空复杂度较低,多项式级别。时间复杂度:O(n^2) 或更低,其中 n 是问题规模。空间复杂度:O(n) 或更低,取决于状态表格的大小。
特性:
- 自底向上或自顶向下的求解过程。
- 使用状态表格保存子问题的解,避免重复计算。
- 通常用于求解最优化问题。
2.1 概述
动态规划(Dynamic Programming,简称DP)是一种将问题分解成子问题并仅仅解决一次,将解保存起来的优化技术。DP主要适用于有重叠子问题和最优子结构性质的问题。
通常许多子问题非常相似,为此动态规划法试图仅仅解决每个子问题一次,从而减少计算量:一旦某个给定子问题的解已经算出,则将其记忆化存储,以便下次需要同一个子问题解之时直接查表。这种做法在重复子问题的数目关于输入的规模呈指数增长时特别有用。
适用情况
- 最优子结构性质。如果问题的最优解所包含的子问题的解也是最优的,我们就称该问题具有最优子结构性质(即满足最优化原理)。最优子结构性质为动态规划算法解决问题提供了重要线索。
- 无后效性。即子问题的解一旦确定,就不再改变,不受在这之后、包含它的更大的问题的求解决策影响。
- 子问题重叠性质。子问题重叠性质是指在用递归算法自顶向下对问题进行求解时,每次产生的子问题并不总是新问题,有些子问题会被重复计算多次。动态规划算法正是利用了这种子问题的重叠性质,对每一个子问题只计算一次,然后将其计算结果保存在一个表格中,当再次需要计算已经计算过的子问题时,只是在表格中简单地查看一下结果,从而获得较高的效率,降低了时间复杂度。
动态规划的一般步骤:
- 定义状态:明确定义问题的状态,找出问题中变化的量,这些变化的量即为状态。
- 找到状态转移方程:确定状态之间的关系,即状态转移方程。这是动态规划的核心,描述了问题的最优子结构。
- 初始化:确定初始状态,即问题中最小规模的子问题的解。
- 计算顺序:按照一定的计算顺序,一步步计算状态的值,直到计算出问题的解。
- 解的表示:根据问题的要求,确定最终的解是哪个状态,即哪些状态是我们最终关心的。
举例:
斐波那契数列。斐波那契数列的状态定义为 f(n) 表示第 n 个斐波那契数,状态转移方程为 f(n) = f(n-1) + f(n-2),初始状态为 f(0) = 0 和 f(1) = 1。
- 定义状态:f(n) 表示第 n 个斐波那契数。
- 状态转移方程:f(n) = f(n-1) + f(n-2)。
- 初始化:f(0) = 0,f(1) = 1。
- 计算顺序:从小到大计算 f(2), f(3), ..., f(n)。
- 解的表示:最终解是 f(n)。
2.2 力扣实战
杨辉三角
给定一个非负整数 numRows,生成「杨辉三角」的前 numRows 行。
动态规划实现:
class Solution {
public:
std::vector<std::vector<int>> generate(int numRows) {
std::vector<std::vector<int>> triangle;
for (int i = 0; i < numRows; ++i) {
std::vector<int> row(i + 1, 1); // 初始化当前行并将元素都置为1
for (int j = 1; j < i; ++j) {
row[j] = triangle[i - 1][j - 1] + triangle[i - 1][j]; // 计算当前元素的值
}
triangle.push_back(row); // 将当前行加入杨辉三角
}
return triangle;
}
};
打家劫舍
你是一个专业的小偷,计划偷窃沿街的房屋。每间房内都藏有一定的现金,影响你偷窃的唯一制约因素就是相邻的房屋装有相互连通的防盗系统,如果两间相邻的房屋在同一晚上被小偷闯入,系统会自动报警。
给定一个代表每个房屋存放金额的非负整数数组,计算你 不触动警报装置的情况下 ,一夜之内能够偷窃到的最高金额。
动态规划实现:
动态规划的关键是找到状态转移方程。在这个问题中,我们可以定义一个数组 dp,其中 dp[i] 表示在第 i 个房屋结束时,小偷能够偷窃到的最大金额。状态转移方程为:
dp[i] = max(dp[i-1], dp[i-2] + nums[i])
nums[i] 表示第 i 个房屋内存放的金额。在第 i 个房屋结束时,小偷可以选择偷窃这个房屋或者不偷窃。如果偷窃,那么最大金额为前两个房屋的最大金额加上当前房屋的金额;如果不偷窃,那么最大金额为前一个房屋的最大金额。
class Solution {
public:
int rob(vector<int>& nums) {
int n = nums.size();
if (n == 0) {
return 0;
} else if (n == 1) {
return nums[0];
}
vector<int> dp(n, 0);
dp[0] = nums[0];
dp[1] = max(nums[0], nums[1]);
for (int i = 2; i < n; ++i) {
dp[i] = max(dp[i - 1], dp[i - 2] + nums[i]);
}
return dp[n - 1];
}
};
dp[i] 记录了在第 i 个房屋结束时能够偷窃到的最大金额。通过遍历数组,计算出最后一个房屋的最大金额,即为整个问题的解。
完全平方数
给你一个整数 n ,返回 和为 n 的完全平方数的最少数量 。
完全平方数 是一个整数,其值等于另一个整数的平方;换句话说,其值等于一个整数自乘的积。例如,1、4、9 和 16 都是完全平方数,而 3 和 11 不是。
使用动态规划数组 dp,其中 dp[i] 表示和为 i 的完全平方数的最少数量。
状态转移方程可以定义为:
dp[i]=min(dp[i],dp[i−j^2]+1)
其中 j 的取值范围是 1 到 根号i。
动态规划实现:
class Solution {
public:
int numSquares(int n) {
vector<int> dp(n + 1, INT_MAX);
dp[0] = 0;
for (int i = 1; i <= n; ++i) {
for (int j = 1; j * j <= i; ++j) {
dp[i] = min(dp[i], dp[i - j * j] + 1);
}
}
return dp[n];
}
};
dp[i] 记录了和为 i 的完全平方数的最少数量。通过双重循环,遍历所有可能的平方数,更新 dp[i]。
零钱兑换
给你一个整数数组 coins ,表示不同面额的硬币;以及一个整数 amount ,表示总金额。
计算并返回可以凑成总金额所需的 最少的硬币个数 。如果没有任何一种硬币组合能组成总金额,返回 -1 。
你可以认为每种硬币的数量是无限的。
动态规划实现:
令 dp[i] 表示凑成金额 i 所需的最少硬币个数。对于每个金额 i,我们可以考虑使用所有硬币中的任何一种硬币,假设选择硬币的面值为 coin,那么状态转移方程可以定义为:
dp[i]=min(dp[i],dp[i−coin]+1)
这表示,凑成金额 i 的最少硬币个数可以通过凑成金额 i - coin 的最少硬币个数加上 1 来得到。我们在所有可能的硬币中选择最小的数量。
class Solution {
public:
int coinChange(vector<int>& coins, int amount) {
vector<int> dp(amount + 1, INT_MAX);
dp[0] = 0; // 初始状态,凑成金额为 0 的硬币个数为 0
for (int i = 1; i <= amount; ++i) {
for (int coin : coins) {
if (i - coin >= 0 && dp[i - coin] != INT_MAX) {
dp[i] = min(dp[i], dp[i - coin] + 1);
}
}
}
return (dp[amount] == INT_MAX) ? -1 : dp[amount];
}
};
dp[i] 记录了凑成金额 i 所需的最少硬币个数。通过两层循环,遍历金额和硬币,更新 dp[i] 的值。
单词拆分
给你一个字符串 s 和一个字符串列表 wordDict 作为字典。请你判断是否可以利用字典中出现的单词拼接出 s 。
注意:不要求字典中出现的单词全部都使用,并且字典中的单词可以重复使用。
动态规划实现:
令 dp[i] 表示字符串 s 的前 i 个字符是否可以被字典中的单词拼接而成。对于每个位置 i,我们考虑前面的字符,假设 j 是一个位置,0 <= j < i,如果 dp[j] 为 true(表示前 j 个字符可以被拼接),且从 j 到 i 的子串也在字典中,那么 dp[i] 也为 true。即:
dp[i]=dp[j] and s[j:i] in wordDict
其中,s[j:i] 表示字符串 s 从位置 j 到 i 的子串。
class Solution {
public:
bool wordBreak(string s, vector<string>& wordDict) {
unordered_set<string> wordSet(wordDict.begin(), wordDict.end());
int n = s.length();
vector<bool> dp(n + 1, false);
dp[0] = true; // 空字符串可以被拼接
for (int i = 1; i <= n; ++i) {
for (int j = 0; j < i; ++j) {
if (dp[j] && wordSet.count(s.substr(j, i - j))) {
dp[i] = true;
break;
}
}
}
return dp[n];
}
};
dp[i] 记录了字符串 s 的前 i 个字符是否可以被拼接。通过两层循环,遍历所有的位置 i 和 j,更新 dp[i]。
最长递增子序列
给你一个整数数组 nums ,找到其中最长严格递增子序列的长度。
子序列 是由数组派生而来的序列,删除(或不删除)数组中的元素而不改变其余元素的顺序。例如,[3,6,2,7] 是数组 [0,3,1,6,2,2,7] 的子序列。
动态规划实现:
令 dp[i] 表示以 nums[i] 结尾的最长递增子序列的长度。对于每个位置 i,我们考虑前面的位置 j,0 <= j < i,如果 nums[i] > nums[j],那么 dp[i] 就可以通过 dp[j] 的值加上 1 来更新。
dp[i]=max(dp[i],dp[j]+1)
class Solution {
public:
int lengthOfLIS(vector<int>& nums) {
int n = nums.size();
if (n == 0) {
return 0;
}
vector<int> dp(n, 1);
for (int i = 1; i < n; ++i) {
for (int j = 0; j < i; ++j) {
if (nums[i] > nums[j]) {
dp[i] = max(dp[i], dp[j] + 1);
}
}
}
return *max_element(dp.begin(), dp.end());
}
};
dp[i] 记录了以 nums[i] 结尾的最长递增子序列的长度。通过两层循环,遍历所有的位置 i 和 j,更新 dp[i]。
乘积最大子数组
给你一个整数数组 nums ,请你找出数组中乘积最大的非空连续子数组(该子数组中至少包含一个数字),并返回该子数组所对应的乘积。
测试用例的答案是一个 32-位 整数。
动态规划实现:
令 maxProd[i] 表示以 nums[i] 结尾的最大乘积子数组的乘积,minProd[i] 表示以 nums[i] 结尾的最小乘积子数组的乘积。对于 maxProd[i],可以选择继续乘以 nums[i] 或者重新开始以 nums[i] 为起点。
maxProd[i] = max(nums[i], maxProd[i-1] * nums[i])
minProd[i] = min(nums[i], minProd[i-1] * nums[i])
class Solution {
public:
int maxProduct(vector<int>& nums) {
int n = nums.size();
if (n == 0) {
return 0;
}
int maxProd = nums[0];
int minProd = nums[0];
int result = nums[0];
for (int i = 1; i < n; ++i) {
int tempMax = maxProd;
maxProd = max({nums[i], maxProd * nums[i], minProd * nums[i]});
minProd = min({nums[i], tempMax * nums[i], minProd * nums[i]});
result = max(result, maxProd);
}
return result;
}
};
maxProd 和 minProd 分别记录了以当前位置 nums[i] 结尾的子数组的最大乘积和最小乘积。通过一重循环,遍历所有的位置 i,更新这两个值,最终得到最大乘积。
背包问题
给定一组物品,每个物品有两个属性:重量 w[i] 和价值 v[i],以及一个背包的容量 C。现在要求从这组物品中选择若干个放入背包中,使得这些物品的总重量不超过背包容量,同时总价值最大。
动态规划实现:
- 状态定义:
定义一个二维数组 dpi,表示在前 i 个物品中,背包容量为 j 时的最大总价值。
- 状态转移方程:
dpi=max(dpi−1,dpi−1]+v[i])
其中,dpi-1 表示不选择第 i 个物品时的最大总价值,dpi-1] + v[i] 表示选择第 i 个物品时的最大总价值。
C++中的伪代码实现:
int knapsack(int N, int C, vector<int>& w, vector<int>& v) {
vector<vector<int>> dp(N + 1, vector<int>(C + 1, 0));
for (int i = 1; i <= N; ++i) {
for (int j = 1; j <= C; ++j) {
if (j >= w[i]) {
dp[i][j] = max(dp[i-1][j], dp[i-1][j-w[i]] + v[i]);
} else {
dp[i][j] = dp[i-1][j];
}
}
}
return dp[N][C];
}
分割等和子集
给你一个 只包含正整数 的 非空 数组 nums 。请你判断是否可以将这个数组分割成两个子集,使得两个子集的元素和相等。
动态规划实现:
令 dpi 表示前 i 个元素是否可以构成和为 j 的子集。对于每个元素 nums[i],我们有两个选择:
不选择 nums[i],即
dpi = dpi-1;
选择 nums[i],即
dpi = dpi-1]。
class Solution {
public:
bool canPartition(vector<int>& nums) {
int n = nums.size();
if (n == 0) {
return false;
}
int sum = 0;
for (int num : nums) {
sum += num;
}
// 如果总和为奇数,不可能划分成两个和相等的子集
if (sum % 2 != 0) {
return false;
}
int target = sum / 2;
vector<vector<bool>> dp(n + 1, vector<bool>(target + 1, false));
// 初始化
dp[0][0] = true;
// 遍历计算
for (int i = 1; i <= n; ++i) {
for (int j = 0; j <= target; ++j) {
dp[i][j] = dp[i-1][j];
if (j >= nums[i-1]) {
dp[i][j] = dp[i][j] || dp[i-1][j-nums[i-1]];
}
}
}
return dp[n][target];
}
};
最长有效括号
给你一个只包含 '(' 和 ')' 的字符串,找出最长有效(格式正确且连续)括号子串的长度。
动态规划实现:
令 dp[i] 表示以字符串的第 i 个字符结尾的最长有效括号子串的长度。如果字符串第 i 个字符是 (,那么 dp[i] 必定为0,因为以 ( 结尾的子串无法形成有效括号子串。如果字符串第 i 个字符是 ),那么我们需要考虑它前面的字符。
如果 s[i-1] 是 (,那么
dp[i] = dp[i-2] + 2
如果 s[i-1] 是 ) 并且 i - dp[i-1] - 1 大于等于0,并且 s[i - dp[i-1] - 1] 是 (,那么
dp[i] = dp[i-1] + dp[i - dp[i-1] - 2] + 2
tip:
如果 s[i-1] 是 ) 并且 i - dp[i-1] - 1 大于等于0,并且 s[i - dp[i-1] - 1] 是 (,那么我们可以形成一对匹配的括号,即 ...(...)... 的形式。
- dp[i-1] 表示以 s[i-1] 结尾的最长有效括号子串的长度。
- dp[i - dp[i-1] - 2] 表示在 s[i-1] 之前,与 s[i-1] 形成一对有效括号的前一个字符之前的最长有效括号子串的长度。
- +2 表示当前形成的一对有效括号。
实际上是在原有的最长有效括号子串的基础上,加上了新形成的一对括号。这样就确保了正确地计算了以 s[i] 结尾的最长有效括号子串的长度。
class Solution {
public:
int longestValidParentheses(string s) {
int n = s.size();
if (n <= 1) {
return 0;
}
vector<int> dp(n, 0);
int maxLen = 0;
for (int i = 1; i < n; ++i) {
if (s[i] == ')') {
if (s[i - 1] == '(') {
dp[i] = (i >= 2 ? dp[i - 2] : 0) + 2;
} else if (i - dp[i - 1] > 0 && s[i - dp[i - 1] - 1] == '(') {
dp[i] = dp[i - 1] + ((i - dp[i - 1] >= 2) ? dp[i - dp[i - 1] - 2] : 0) + 2;
}
maxLen = max(maxLen, dp[i]);
}
}
return maxLen;
}
};
2.3 总结
在动态规划中,选择是基于问题的性质而定的。通常情况下:
- 选择min: 当问题要求最小值、最小花费、最少次数等时,我们选择min。例如,在找零钱的问题中,我们要求凑成总金额所需的最少硬币个数,因此使用 dp[i] = min(dp[i], dp[i - coin] + 1)。
- 选择max: 当问题要求最大值、最大收益、最长长度等时,我们选择max。例如,在打家劫舍的问题中,我们要求一夜之内能够偷窃到的最高金额,因此使用 dp[i] = max(dp[i - 1], dp[i - 2] + nums[i])。
模板
int dynamicProgramming(int n, vector<int>& nums) {
// 1. 定义状态
vector<int> dp(n + 1, 0);
// 2. 初始化
dp[0] = ...;
// 3. 状态转移方程
for (int i = 1; i <= n; ++i) {
dp[i] = ...; // 根据状态转移方程更新 dp[i]
}
// 4. 得到最终结果
int result = dp[n];
return result;
}
优化方法
- 状态压缩: 对于一些问题,可以通过状态压缩将二维动态规划数组优化为一维,减少空间复杂度。
- 贪心策略: 在一些情况下,可以使用贪心策略对状态进行更新,而不是完全遍历所有可能的状态。
- 提前终止: 在某些情况下,可以提前终止遍历,减少计算量。例如,在背包问题中,如果已经达到目标值,则可以提前结束循环。
- 逆向思维: 对于一些问题,可以考虑从终点开始逆向思考,计算得到初始状态。这有时可以简化问题的解决过程。
- 滚动数组: 在一些问题中,只需要保存当前状态和前一个状态,不需要完整的动态规划数组,可以使用滚动数组进行优化。
- 剪枝: 对于搜索空间较大的问题,可以使用剪枝策略,提前剪掉一些不必要的计算分支。
- 前缀和等预处理: 对于一些问题,可以通过预处理得到一些额外的信息,如前缀和、前缀最大值等,以加速动态规划过程。
零钱兑换:给定不同面额的硬币和一个总金额,找到最少的硬币个数凑成该金额。
未优化:
class Solution {
public:
int coinChange(std::vector<int>& coins, int amount) {
int n = coins.size();
std::vector<int> dp(amount + 1, INT_MAX);
dp[0] = 0;
for (int i = 1; i <= amount; ++i) {
for (int coin : coins) {
if (i - coin >= 0 && dp[i - coin] != INT_MAX) {
dp[i] = std::min(dp[i], dp[i - coin] + 1);
}
}
}
return (dp[amount] == INT_MAX) ? -1 : dp[amount];
}
};
优化版本:
class Solution {
public:
int coinChange(std::vector<int>& coins, int amount) {
int n = coins.size();
std::vector<int> dp(amount + 1, INT_MAX);
dp[0] = 0;
for (int coin : coins) {
for (int i = coin; i <= amount; ++i) {
if (dp[i - coin] != INT_MAX) {
dp[i] = std::min(dp[i], dp[i - coin] + 1);
}
}
}
return (dp[amount] == INT_MAX) ? -1 : dp[amount];
}
};
未优化版本使用二维数组dp,考虑所有金额和硬币的组合。优化版本中,我们只使用一维数组dp,在计算每个金额时,只考虑当前硬币的影响,减少了空间复杂度。
由于我们从前往后遍历,每个dp[i]只与前面的dp[i-coin]有关,所以在内层循环中,我们可以直接使用一维数组dp的前面元素,不需要使用二维数组。
这种优化方法称为「滚动数组」,通过降低空间复杂度提高了算法的效率。
3.贪心算法 (Greedy Algorithm)
应用:每一步的选择不会影响后续步骤,局部最优选择期望导致全局最优解,问题具有贪心选择性质,不需要回溯。
时空复杂度:时空复杂度较低,线性或对数级别。时间复杂度:O(n log n) 或更低,其中 n 是问题规模。空间复杂度:O(1) 或常数级别。
特性:
- 每一步都做出局部最优选择。
- 不进行回溯,通常无需保存状态。
概述
贪心算法(Greedy Algorithm),又称贪婪算法,是一种在每一步选择中都采取在当前状态下最好或最优(即最有利)的选择,从而希望导致结果是最好或最优的算法。
贪心算法在有最优子结构的问题中尤为有效。最优子结构的意思是局部最优解能决定全局最优解。简单地说,问题能够分解成子问题来解决,子问题的最优解能递推到最终问题的最优解。
贪心算法与动态规划的不同在于它对每个子问题的解决方案都做出选择,不能回退。动态规划则会保存以前的运算结果,并根据以前的结果对当前进行选择,有回退功能。
贪心法可以解决一些最优化问题,如:求图中的最小生成树、求哈夫曼编码……对于其他问题,贪心法一般不能得到我们所要求的答案。一旦一个问题可以通过贪心法来解决,那么贪心法一般是解决这个问题的最好办法。由于贪心法的高效性以及其所求得的答案比较接近最优结果,贪心法也可以用作辅助算法或者直接解决一些要求结果不特别精确的问题。在不同情况,选择最优的解,可能会导致辛普森悖论(Simpson's Paradox),不一定出现最优的解。
对于大部分的问题,贪心法通常都不能找出最佳解(不过也有例外),因为他们一般没有测试所有可能的解。贪心法容易过早做决定,因而没法达到最佳解。例如,所有对图着色问题。
思路概括:
- 问题建模:将问题转化为可以用贪心策略求解的形式。
- 制定贪心策略:找到一个合适的贪心策略,即在每一步选择中都选择当前状态下的最优解。
- 证明正确性:证明贪心选择是局部最优的,并且通过局部最优选择能够达到全局最优解。
常见应用:
- 活动选择问题(Activity Selection Problem): 在一组互不相容的活动中,选择最大数量的活动,使得它们彼此不重叠。
- 霍夫曼编码(Huffman Coding): 用不同长度的二进制编码表示不同字符,使得出现频率高的字符的编码长度较短。
- 最小生成树(Minimum Spanning Tree): 在一个带权重的无向图中,找到一个树,包含所有的顶点且边的权重之和最小。
- 单源最短路径问题(Dijkstra's Algorithm): 在带权重的有向图中,找到一个顶点到其他所有顶点的最短路径。
- 零钱找零问题: 找到一组硬币的最小数量,使其总面值等于给定的金额。
力扣实战
买卖股票的最佳时机
给定一个数组 prices ,它的第 i 个元素 prices[i] 表示一支给定股票第 i 天的价格。
你只能选择 某一天 买入这只股票,并选择在 未来的某一个不同的日子 卖出该股票。设计一个算法来计算你所能获取的最大利润。
返回你可以从这笔交易中获取的最大利润。如果你不能获取任何利润,返回 0 。
贪心实现:
class Solution {
public:
int maxProfit(std::vector<int>& prices) {
int n = prices.size();
if (n <= 1) {
return 0; // 如果数组长度小于等于1,则无法交易,利润为0
}
int maxProfit = 0;
int minPrice = prices[0]; // 初始化最低买入价格为第一天的股票价格
for (int i = 1; i < n; ++i) {
// 更新最低买入价格
minPrice = std::min(minPrice, prices[i]);
// 计算当前卖出时的利润,并更新最大利润
maxProfit = std::max(maxProfit, prices[i] - minPrice);
}
return maxProfit;
}
};
跳跃游戏
给你一个非负整数数组 nums ,你最初位于数组的 第一个下标 。数组中的每个元素代表你在该位置可以跳跃的最大长度。
判断你是否能够到达最后一个下标,如果可以,返回 true ;否则,返回 false 。
贪心实现:
class Solution {
public:
bool canJump(std::vector<int>& nums) {
int n = nums.size();
int maxReach = 0; // 记录当前能够到达的最远位置
for (int i = 0; i < n; ++i) {
if (i > maxReach) {
return false; // 如果当前位置超过了能够到达的最远位置,则无法到达最后一个下标
}
maxReach = std::max(maxReach, i + nums[i]); // 更新能够到达的最远位置
}
return true;
}
};
实现中,维护一个变量 maxReach 来记录当前能够到达的最远位置。在遍历数组的过程中,不断更新 maxReach,表示在当前位置可以跳跃的最大长度。如果某一步发现当前位置超过了能够到达的最远位置,则说明无法到达最后一个下标,返回 false。否则,最终返回 true。
跳跃游戏 II
给定一个长度为 n 的 0 索引整数数组 nums。初始位置为 nums[0]。
每个元素 nums[i] 表示从索引 i 向前跳转的最大长度。换句话说,如果你在 nums[i] 处,你可以跳转到任意 nums[i + j] 处:
- 0 <= j <= nums[i]
- i + j < n
返回到达 nums[n - 1] 的最小跳跃次数。生成的测试用例可以到达 nums[n - 1]。
贪心实现:
class Solution {
public:
int jump(std::vector<int>& nums) {
int n = nums.size();
int jumps = 0; // 记录跳跃次数
int curEnd = 0; // 当前能够到达的最远位置
int curFarthest = 0; // 在当前跳跃范围内能够到达的最远位置
for (int i = 0; i < n - 1; ++i) {
curFarthest = std::max(curFarthest, i + nums[i]);
if (i == curEnd) {
// 当前跳跃范围结束,更新下一次跳跃范围的边界
curEnd = curFarthest;
jumps++;
}
}
return jumps;
}
};
实现中维护了两个变量 curEnd 和 curFarthest,分别表示当前跳跃范围的边界和在当前范围内能够到达的最远位置。在遍历数组的过程中,不断更新这两个变量,当遍历到当前跳跃范围的边界时,表示需要进行下一次跳跃,同时增加跳跃次数。
划分字母区间
给你一个字符串 s 。我们要把这个字符串划分为尽可能多的片段,同一字母最多出现在一个片段中。
注意,划分结果需要满足:将所有划分结果按顺序连接,得到的字符串仍然是 s 。
返回一个表示每个字符串片段的长度的列表。
贪心实现:
class Solution {
public:
vector<int> partitionLabels(string s) {
int n = s.size();
vector<int> ans;
int start=0,end=0;
unordered_map<char,int> map;
for(int i = 0;i<n;++i){
map[s[i]] = i;
}
for(int i = 0;i<n;++i){
end = max(end,map[s[i]]);
if(i==end){
ans.push_back(end-start+1);
start = end+1;
}
}
return ans;
}
};
4.分治法 (Divide and Conquer)
应用:问题可以分解为互不相交的子问题,具有递归结构,子问题独立求解,子问题的解合并得到原问题的解。
时空复杂度:时空复杂度较低,多项式级别。时间复杂度:O(n log n) 或更低,其中 n 是问题规模。空间复杂度:O(log n) 或更低,取决于递归深度。
特性:
- 通过递归地将问题分解为子问题。
- 子问题独立求解,不涉及状态的回溯。
- 典型的应用如归并排序、快速排序等。
异同点总结:
- 回溯法:主要用于解决组合、排列、子集等组合型问题,时空复杂度高,需要回溯选择。
- 动态规划:用于求解最优子结构性质的问题,通过保存子问题的解避免重复计算,时空复杂度较低。
- 贪心算法:适用于每一步的选择不会影响后续步骤、且期望通过局部最优选择得到全局最优解的问题。
- 分治法:将问题分解为互不相交的子问题,子问题独立求解,通过合并子问题的解得到原问题的解。适用于具有递归结构的问题。
5.图与树
基础(树):树的前序、中序和后序遍历是树的三种基本遍历方式,它们可以通过递归和迭代两种方法实现,层序遍历是一种按层次逐级访问树节点的方式。
前序遍历(Preorder Traversal):
// 递归实现:
#include <iostream>
using namespace std;
struct TreeNode {
int val;
TreeNode* left;
TreeNode* right;
TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
};
void preorderTraversalRecursive(TreeNode* root) {
if (root) {
cout << root->val << " ";
preorderTraversalRecursive(root->left);
preorderTraversalRecursive(root->right);
}
}
// 迭代实现(使用栈)
#include <stack>
void preorderTraversalIterative(TreeNode* root) {
stack<TreeNode*> st;
while (root || !st.empty()) {
while (root) {
cout << root->val << " ";
st.push(root);
root = root->left;
}
if (!st.empty()) {
root = st.top();
st.pop();
root = root->right;
}
}
}
中序遍历(Inorder Traversal):
// 递归实现:
void inorderTraversalRecursive(TreeNode* root) {
if (root) {
inorderTraversalRecursive(root->left);
cout << root->val << " ";
inorderTraversalRecursive(root->right);
}
}
// 迭代实现(使用栈)
void inorderTraversalIterative(TreeNode* root) {
stack<TreeNode*> st;
while (root || !st.empty()) {
while (root) {
st.push(root);
root = root->left;
}
if (!st.empty()) {
root = st.top();
st.pop();
cout << root->val << " ";
root = root->right;
}
}
}
后序遍历(Postorder Traversal):
// 递归实现:
void postorderTraversalRecursive(TreeNode* root) {
if (root) {
postorderTraversalRecursive(root->left);
postorderTraversalRecursive(root->right);
cout << root->val << " ";
}
}
// 迭代实现(使用两个栈)
void postorderTraversalIterative(TreeNode* root) {
stack<TreeNode*> st1, st2;
st1.push(root);
while (!st1.empty()) {
root = st1.top();
st1.pop();
st2.push(root);
if (root->left) {
st1.push(root->left);
}
if (root->right) {
st1.push(root->right);
}
}
while (!st2.empty()) {
cout << st2.top()->val << " ";
st2.pop();
}
}
层序遍历(Level Order Traversal):
#include <queue>
void levelOrderTraversal(TreeNode* root) {
if (!root) return;
queue<TreeNode*> q;
q.push(root);
while (!q.empty()) {
TreeNode* node = q.front();
q.pop();
cout << node->val << " ";
if (node->left) q.push(node->left);
if (node->right) q.push(node->right);
}
}
力扣实战
二叉树的直径
给你一棵二叉树的根节点,返回该树的 直径 。
二叉树的 直径 是指树中任意两个节点之间最长路径的 长度 。这条路径可能经过也可能不经过根节点 root 。
两节点之间路径的 长度 由它们之间边数表示。
实现:
/**
* Definition for a binary tree node.
* struct TreeNode {
* int val;
* TreeNode *left;
* TreeNode *right;
* TreeNode() : val(0), left(nullptr), right(nullptr) {}
* TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
* TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
* };
*/
class Solution {
int ans = 0;
int depth(TreeNode* p){
if(p == nullptr) return 0;
int l = depth(p->left);
int r = depth(p->right);
ans = max(ans, l+r+1);
return max(l,r)+1;
}
public:
int diameterOfBinaryTree(TreeNode* root) {
// depth(root);
// return ans-1;
unordered_map<TreeNode*, int> height; // 用于保存节点的高度
stack<TreeNode*> st;
TreeNode* current = root;
TreeNode* prev = nullptr;
int result = 0;
while (current || !st.empty()) {
while (current) {
st.push(current);
current = current->left;
}
current = st.top();
// 如果右子树为空或者已经访问过右子树,处理当前节点
if (!current->right || current->right == prev) {
int leftHeight = height[current->left];
int rightHeight = height[current->right];
// 计算经过当前节点的直径
result = max(result, leftHeight + rightHeight);
// 计算当前节点的高度
height[current] = 1 + max(leftHeight, rightHeight);
st.pop();
prev = current;
current = nullptr;
} else {
// 访问右子树
current = current->right;
}
}
return result;
}
};
二叉树的层序遍历
给你二叉树的根节点 root ,返回其节点值的 层序遍历 。 (即逐层地,从左到右访问所有节点)。
实现:
/**
* Definition for a binary tree node.
* struct TreeNode {
* int val;
* TreeNode *left;
* TreeNode *right;
* TreeNode() : val(0), left(nullptr), right(nullptr) {}
* TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
* TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
* };
*/
class Solution {
public:
vector<vector<int>> levelOrder(TreeNode* root) {
vector<vector<int>> result;
if(!root) return result;
queue <TreeNode*> q;
q.push(root);
while(!q.empty()){
int n = q.size();
result.push_back(vector <int> ());
for(int i=0;i<n;++i){
auto ptr = q.front();q.pop();
result.back().push_back(ptr->val);
if(ptr->left) q.push(ptr->left);
if(ptr->right) q.push(ptr->right);
}
}
return result;
}
};
将有序数组转换为二叉搜索树
给你一个整数数组 nums ,其中元素已经按 升序 排列,请你将其转换为一棵 高度平衡 二叉搜索树。
高度平衡 二叉树是一棵满足「每个节点的左右两个子树的高度差的绝对值不超过 1 」的二叉树。
实现:
/**
* Definition for a binary tree node.
* struct TreeNode {
* int val;
* TreeNode *left;
* TreeNode *right;
* TreeNode() : val(0), left(nullptr), right(nullptr) {}
* TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
* TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
* };
*/
class Solution {
public:
TreeNode* sortedArrayToBST(vector<int>& nums) {
if (nums.empty()) return nullptr;
return BST(nums, 0, nums.size() - 1);
}
private:
TreeNode* BST(std::vector<int>& nums, int left, int right) {
if (left > right) return nullptr;
int mid = left + (right - left) / 2;
TreeNode* root = new TreeNode(nums[mid]);
root->left = BST(nums, left, mid - 1);
root->right = BST(nums, mid + 1, right);
return root;
}
};
验证二叉搜索树
给你一个二叉树的根节点 root ,判断其是否是一个有效的二叉搜索树。
有效 二叉搜索树定义如下:
- 节点的左子树只包含 小于 当前节点的数。
- 节点的右子树只包含 大于 当前节点的数。
- 所有左子树和右子树自身必须也是二叉搜索树。
实现:
/**
* Definition for a binary tree node.
* struct TreeNode {
* int val;
* TreeNode *left;
* TreeNode *right;
* TreeNode() : val(0), left(nullptr), right(nullptr) {}
* TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
* TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
* };
*/
class Solution {
public:
bool isValidBST(TreeNode* root) {
// vector<int> nums;
// dfs(root,nums);
// if (nums.size() <= 1) return true;
// for (size_t i = 0; i < nums.size() - 1; ++i) {
// if (nums[i] >= nums[i + 1]) return false;
// }
// return true;
// }
// private:
// void dfs(TreeNode* node,vector<int>& arr){
// if(node->left) dfs(node->left,arr);
// arr.push_back(node->val);
// if(node->right) dfs(node->right,arr);
// }
return isValid(root, LONG_MIN, LONG_MAX);
}
private:
bool isValid(TreeNode* root, long long lower, long long upper) {
if (!root) return true;
if (root->val <= lower || root->val >= upper) return false;
return isValid(root->left, lower, root->val) &&
isValid(root->right, root->val, upper);
}
};
二叉搜索树中第K小的元素
给定一个二叉搜索树的根节点 root ,和一个整数 k ,请你设计一个算法查找其中第 k 个最小元素(从 1 开始计数)。
实现:
/**
* Definition for a binary tree node.
* struct TreeNode {
* int val;
* TreeNode *left;
* TreeNode *right;
* TreeNode() : val(0), left(nullptr), right(nullptr) {}
* TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
* TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
* };
*/
class Solution {
public:
int kthSmallest(TreeNode* root, int k) {
// stack<TreeNode*> st;
// while(root || st.size()>0){
// while(root){
// st.push(root);
// root = root->left;
// }
// root = st.top();
// st.pop();
// --k;
// if(!k) break;
// root = root->right;
// }
// return root->val;
// }
if(!root) return 0;
vector<int> nums;
dfs(root,nums);
return nums[k-1];
}
private:
void dfs(TreeNode* node, vector<int>& nums){
if(node->left) dfs(node->left,nums);
nums.push_back(node->val);
if(node->right) dfs(node->right,nums);
}
};
二叉树的右视图
给定一个二叉树的 根节点 root,想象自己站在它的右侧,按照从顶部到底部的顺序,返回从右侧所能看到的节点值。
实现:
/**
* Definition for a binary tree node.
* struct TreeNode {
* int val;
* TreeNode *left;
* TreeNode *right;
* TreeNode() : val(0), left(nullptr), right(nullptr) {}
* TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
* TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
* };
*/
class Solution {
public:
vector<int> rightSideView(TreeNode* root) {
vector<int> ans;
if(!root) return ans;
queue<TreeNode*> queue;
TreeNode* p = root;
queue.push(p);
while(!queue.empty()){
int n = queue.size();
for(int i=0;i<n;++i){
p = queue.front();queue.pop();
if(p->left != nullptr) queue.push(p->left);
if(p->right != nullptr) queue.push(p->right);
if(i == n-1) ans.push_back(p->val);
}
}
return ans;
}
};
二叉树展开为链表
给你二叉树的根结点 root ,请你将它展开为一个单链表:
- 展开后的单链表应该同样使用 TreeNode ,其中 right 子指针指向链表中下一个结点,而左子指针始终为 null 。
- 展开后的单链表应该与二叉树 先序遍历 顺序相同。
实现:
/**
* Definition for a binary tree node.
* struct TreeNode {
* int val;
* TreeNode *left;
* TreeNode *right;
* TreeNode() : val(0), left(nullptr), right(nullptr) {}
* TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
* TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
* };
*/
class Solution {
public:
void flatten(TreeNode* root) {
vector<TreeNode*> l;
dfs(root,l);
int n = l.size();
for(int i=1;i<n;++i){
TreeNode* prev = l.at(i-1),*curr = l.at(i);
prev->left=nullptr;
prev->right=curr;
}
return;
}
private:
void dfs(TreeNode* root,vector<TreeNode*> &l){
if(root != nullptr) {
l.push_back(root);
dfs(root->left,l);
dfs(root->right,l);
}
return;
}
};
从前序与中序遍历序列构造二叉树
给定两个整数数组 preorder 和 inorder ,其中 preorder 是二叉树的先序遍历, inorder 是同一棵树的中序遍历,请构造二叉树并返回其根节点。
实现:
/**
* Definition for a binary tree node.
* struct TreeNode {
* int val;
* TreeNode *left;
* TreeNode *right;
* TreeNode() : val(0), left(nullptr), right(nullptr) {}
* TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
* TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
* };
*/
class Solution {
public:
TreeNode* buildTree(vector<int>& preorder, vector<int>& inorder) {
std::unordered_map<int, int> inorder_map;
for (int i = 0; i < inorder.size(); ++i) inorder_map[inorder[i]] = i;
return build(preorder, 0, preorder.size() - 1, inorder, 0, inorder.size() - 1, inorder_map);
}
private:
TreeNode* build(vector<int>& preorder, int preStart, int preEnd,
vector<int>& inorder, int inStart, int inEnd,
unordered_map<int, int>& inorder_map) {
if (preStart > preEnd || inStart > inEnd) return nullptr;
int rootValue = preorder[preStart];
TreeNode* root = new TreeNode(rootValue);
int rootIndexInorder = inorder_map[rootValue];
int leftSubtreeSize = rootIndexInorder - inStart;
root->left = build(preorder, preStart + 1, preStart + leftSubtreeSize,
inorder, inStart, rootIndexInorder - 1, inorder_map);
root->right = build(preorder, preStart + leftSubtreeSize + 1, preEnd,
inorder, rootIndexInorder + 1, inEnd, inorder_map);
return root;
}
};
路径总和 III
给定一个二叉树的根节点 root ,和一个整数 targetSum ,求该二叉树里节点值之和等于 targetSum 的 路径 的数目。
路径 不需要从根节点开始,也不需要在叶子节点结束,但是路径方向必须是向下的(只能从父节点到子节点)。
**
DFS实现**:
rootSum 函数:
int rootSum(TreeNode* root, long targetSum) {
if (!root) return 0;
int ret = 0;
if (root->val == targetSum) ret++;
ret += rootSum(root->left, targetSum - root->val);
ret += rootSum(root->right, targetSum - root->val);
return ret;
}
参数:
root:当前子树的根节点。
targetSum:目标和。
返回值:返回以当前节点为根的子树中节点值之和等于目标和的路径数目。
递归过程:
- 如果当前节点为空 (!root),直接返回0。
- 初始化变量 ret 为0,用于记录满足条件的路径数目。
- 如果当前节点的值等于 targetSum,说明找到了一条路径,将 ret 加1。
- 递归调用 rootSum 函数计算左子树中满足条件的路径数目,并累加到 ret 中。
- 递归调用 rootSum 函数计算右子树中满足条件的路径数目,并累加到 ret 中。
- 返回 ret。
pathSum 函数:
public:
int pathSum(TreeNode* root, int targetSum) {
if (!root) return 0;
int ret = rootSum(root, targetSum);
ret += pathSum(root->left, targetSum);
ret += pathSum(root->right, targetSum);
return ret;
}
参数:
root:二叉树的根节点。
targetSum:目标和。
返回值:返回整个二叉树中节点值之和等于目标和的路径数目。
主体逻辑:
- 如果根节点为空 (!root),直接返回0。
- 初始化变量 ret 为0,用于记录满足条件的路径数目。
- 调用 rootSum 函数计算以根节点为起点的路径数目,并累加到 ret 中。
- 递归调用 pathSum 函数计算左子树中满足条件的路径数目,并累加到 ret 中。
- 递归调用 pathSum 函数计算右子树中满足条件的路径数目,并累加到 ret 中。
- 返回 ret。
利用递归,深度优先地遍历二叉树,不断计算以每个节点为起点的路径数目,并在整个树中进行累加。
前缀和实现:
成员变量:
unordered_map<long long, int> prefix:
存储前缀和的哈希表,prefix[curr] 表示从根节点到当前节点的路径上的节点值之和为 curr 的个数。
dfs 函数:
int dfs(TreeNode *root, long long curr, int targetSum) {
if (!root) {
return 0;
}
int ret = 0;
curr += root->val;
if (prefix.count(curr - targetSum)) {
ret = prefix[curr - targetSum];
}
prefix[curr]++;
ret += dfs(root->left, curr, targetSum);
ret += dfs(root->right, curr, targetSum);
prefix[curr]--;
return ret;
}
参数:
root:当前子树的根节点。
curr:当前节点的前缀和。
targetSum:目标和。
返回值:返回以当前节点为终点的路径中满足条件的路径数目。
递归过程:
- 如果当前节点为空 (!root),直接返回0。
- 初始化变量 ret 为0,用于记录满足条件的路径数目。
- 计算当前节点的前缀和 curr。
- 如果 prefix 哈希表中存在 curr - targetSum,说明存在满足条件的前缀和,将其加到 ret 中。
- 将当前前缀和 curr 添加到 prefix 哈希表中。
- 递归调用 dfs 函数计算左子树中满足条件的路径数目,并累加到 ret 中。
- 递归调用 dfs 函数计算右子树中满足条件的路径数目,并累加到 ret 中。
- 回溯,从 prefix 哈希表中移除当前前缀和 curr。
- 返回 ret。
pathSum 函数:
int pathSum(TreeNode* root, int targetSum) {
prefix[0] = 1;
return dfs(root, 0, targetSum);
}
参数:
root:二叉树的根节点。
targetSum:目标和。
返回值:返回整个二叉树中节点值之和等于目标和的路径数目。
主体逻辑:
- 初始化 prefix 哈希表,将0添加为前缀和,初始路径数目为1。
调用 dfs 函数计算以根节点为起点的路径数目,并返回结果。
二叉树的最近公共祖先
给定一个二叉树, 找到该树中两个指定节点的最近公共祖先。
百度百科中最近公共祖先的定义为:“对于有根树 T 的两个节点 p、q,最近公共祖先表示为一个节点 x,满足 x 是 p、q 的祖先且 x 的深度尽可能大(一个节点也可以是它自己的祖先)。”
递归实现:
/**
* Definition for a binary tree node.
* struct TreeNode {
* int val;
* TreeNode *left;
* TreeNode *right;
* TreeNode(int x) : val(x), left(NULL), right(NULL) {}
* };
*/
class Solution {
public:
TreeNode* lowestCommonAncestor(TreeNode* root, TreeNode* p, TreeNode* q) {
if (!root || root == p || root == q) return root;
TreeNode* left = lowestCommonAncestor(root->left, p, q);
TreeNode* right = lowestCommonAncestor(root->right, p, q);
if (left && right) return root;
return left ? left : right;
}
};
- 如果当前节点为空 (!root),或者当前节点等于目标节点之一 (root == p 或 root == q),则直接返回当前节点。这是递归的终止条件。
- 在左子树中递归调用 lowestCommonAncestor 函数,寻找左子树中的最近公共祖先。
- 在右子树中递归调用 lowestCommonAncestor 函数,寻找右子树中的最近公共祖先。
- 根据左右子树的结果进行判断:
- 如果左右子树都找到了目标节点,说明当前节点就是最近公共祖先,直接返回当前节点。
- 如果只在左子树中找到了目标节点,则返回左子树的结果。
- 如果只在右子树中找到了目标节点,则返回右子树的结果。
- 递归过程会一直向上返回最近公共祖先,直到找到最终的结果。
巧用哈希表:
class Solution {
public:
unordered_map<int, TreeNode*> fa; // 保存节点的父节点信息
unordered_map<int, bool> vis; // 记录节点是否被访问过
// 递归函数,用于构建父节点信息
void dfs(TreeNode* root) {
if (root->left != nullptr) {
fa[root->left->val] = root;
dfs(root->left);
}
if (root->right != nullptr) {
fa[root->right->val] = root;
dfs(root->right);
}
}
// 寻找两个节点的最近公共祖先
TreeNode* lowestCommonAncestor(TreeNode* root, TreeNode* p, TreeNode* q) {
fa[root->val] = nullptr; // 根节点的父节点为空
dfs(root); // 构建父节点信息
while (p != nullptr) {
vis[p->val] = true; // 将节点p及其祖先标记为已访问
p = fa[p->val]; // 移向p的父节点
}
while (q != nullptr) {
if (vis[q->val]) {
return q; // 找到第一个被标记为已访问的节点,即为最近公共祖先
}
q = fa[q->val]; // 移向q的父节点
}
return nullptr; // 如果没有找到最近公共祖先,返回nullptr
}
};
这个代码使用了两个哈希表 fa 和 vis:
fa 哈希表用于保存每个节点的父节点信息,在 dfs 函数中递归构建。
vis 哈希表用于记录节点是否被访问过,在 lowestCommonAncestor 函数中使用。
具体流程如下:
- 初始化根节点的父节点为 nullptr,并调用 dfs 函数构建父节点信息。
- 从节点 p 开始,沿着父节点一直向上标记为已访问,直到根节点。
- 然后从节点 q 开始,沿着父节点一直向上查找,找到第一个已访问的节点,即为最近公共祖先。
- 如果没有找到最近公共祖先,返回 nullptr。
这种方法的时间复杂度为 O(N),其中 N 是二叉树的节点数。
二叉树中的最大路径和
二叉树中的 路径 被定义为一条节点序列,序列中每对相邻节点之间都存在一条边。同一个节点在一条路径序列中 至多出现一次 。该路径 至少包含一个 节点,且不一定经过根节点。
路径和 是路径中各节点值的总和。
给你一个二叉树的根节点 root ,返回其 最大路径和 。
递归实现:
/**
* Definition for a binary tree node.
* struct TreeNode {
* int val;
* TreeNode *left;
* TreeNode *right;
* TreeNode() : val(0), left(nullptr), right(nullptr) {}
* TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
* TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
* };
*/
class Solution {
public:
int maxPathSum(TreeNode* root) {
int maxSum = INT_MIN; // 用于保存最大路径和的结果
maxPathSumHelper(root, maxSum);
return maxSum;
}
private:
int maxPathSumHelper(TreeNode* root, int& maxSum) {
if (!root) return 0;
// 计算左子树和右子树的最大贡献值
int leftMax = max(maxPathSumHelper(root->left, maxSum), 0);
int rightMax = max(maxPathSumHelper(root->right, maxSum), 0);
// 计算以当前节点为根的路径和,包括左右子树的贡献值
int rootPathSum = root->val + leftMax + rightMax;
// 更新最大路径和的结果
maxSum = max(maxSum, rootPathSum);
// 返回以当前节点为路径的最大贡献值
return root->val + max(leftMax, rightMax);
}
};
基础(图):图的遍历算法有两种主要方法:深度优先搜索(Depth-First Search, DFS)和广度优先搜索(Breadth-First Search, BFS)。
1.深度优先搜索 (DFS)
DFS是一种递归或栈的方式进行遍历,其基本思想是从起始节点出发,沿着一条路径一直深入,直到不能再继续深入,然后回溯到上一个节点,继续深入其他路径。
DFS算法步骤:
- 从起始节点开始。
- 访问当前节点,并将其标记为已访问。
- 对当前节点的邻居节点进行递归访问,如果邻居节点未被访问过。
DFS代码示例(使用递归):
#include <iostream>
#include <vector>
#include <unordered_set>
class Graph {
public:
Graph(int vertices) : V(vertices), adjList(vertices) {}
void addEdge(int u, int v) {
adjList[u].push_back(v);
adjList[v].push_back(u);
}
void DFS(int start) {
std::unordered_set<int> visited;
DFSUtil(start, visited);
}
private:
void DFSUtil(int v, std::unordered_set<int>& visited) {
visited.insert(v);
std::cout << v << " ";
for (int neighbor : adjList[v]) {
if (visited.find(neighbor) == visited.end()) {
DFSUtil(neighbor, visited);
}
}
}
int V; // Number of vertices
std::vector<std::vector<int>> adjList; // Adjacency list
};
int main() {
Graph g(6);
g.addEdge(0, 1);
g.addEdge(0, 2);
g.addEdge(1, 3);
g.addEdge(2, 4);
g.addEdge(2, 5);
std::cout << "DFS starting from vertex 0:" << std::endl;
g.DFS(0);
return 0;
}
2.广度优先搜索 (BFS)
BFS是一种层级遍历的方式,从起始节点开始,逐层遍历节点。每一层的节点都要在同一时间访问,并且要先访问距离起始节点近的节点,再访问距离远的节点。
BFS算法步骤:
- 从起始节点开始,将其加入队列。
- 出队列并访问该节点。
- 将该节点的未访问邻居节点加入队列。
- 重复步骤2和步骤3,直到队列为空。
BFS代码示例:
#include <iostream>
#include <queue>
#include <unordered_set>
class Graph {
public:
Graph(int vertices) : V(vertices), adjList(vertices) {}
void addEdge(int u, int v) {
adjList[u].push_back(v);
adjList[v].push_back(u);
}
void BFS(int start) {
std::unordered_set<int> visited;
std::queue<int> q;
visited.insert(start);
q.push(start);
while (!q.empty()) {
int current = q.front();
std::cout << current << " ";
q.pop();
for (int neighbor : adjList[current]) {
if (visited.find(neighbor) == visited.end()) {
visited.insert(neighbor);
q.push(neighbor);
}
}
}
}
private:
int V; // Number of vertices
std::vector<std::vector<int>> adjList; // Adjacency list
};
int main() {
Graph g(6);
g.addEdge(0, 1);
g.addEdge(0, 2);
g.addEdge(1, 3);
g.addEdge(2, 4);
g.addEdge(2, 5);
std::cout << "BFS starting from vertex 0:" << std::endl;
g.BFS(0);
return 0;
}
图的最短路径算法有多种,其中两种较为常见的是Dijkstra算法和Bellman-Ford算法。
1.Dijkstra算法
Dijkstra算法用于求解单源最短路径问题,即从一个起始节点到其他所有节点的最短路径。该算法要求图中的权重值非负。
Dijkstra算法步骤:
- 初始化距离数组,将起始节点的距离设置为0,其他节点的距离设置为无穷大。
- 从未被访问的节点中选择距离最短的节点。
- 更新与选定节点相邻节点的距离,如果新的路径更短。
- 标记选定节点为已访问。
- 重复步骤2至步骤4,直到所有节点都被访问。
C++代码示例:
#include <iostream>
#include <vector>
#include <queue>
#include <limits>
class Graph {
public:
Graph(int vertices) : V(vertices), adjList(vertices) {}
void addEdge(int u, int v, int weight) {
adjList[u].push_back({v, weight});
adjList[v].push_back({u, weight});
}
void dijkstra(int start) {
std::vector<int> dist(V, std::numeric_limits<int>::max());
dist[start] = 0;
std::priority_queue<std::pair<int, int>, std::vector<std::pair<int, int>>, std::greater<std::pair<int, int>>> pq;
pq.push({0, start});
while (!pq.empty()) {
int u = pq.top().second;
pq.pop();
for (const auto& neighbor : adjList[u]) {
int v = neighbor.first;
int weight = neighbor.second;
if (dist[u] + weight < dist[v]) {
dist[v] = dist[u] + weight;
pq.push({dist[v], v});
}
}
}
// Print the shortest distances
std::cout << "Shortest distances from vertex " << start << ":\n";
for (int i = 0; i < V; ++i) {
std::cout << "To vertex " << i << ": " << dist[i] << "\n";
}
}
private:
int V; // Number of vertices
std::vector<std::vector<std::pair<int, int>>> adjList; // Adjacency list
};
int main() {
Graph g(6);
g.addEdge(0, 1, 2);
g.addEdge(0, 2, 4);
g.addEdge(1, 2, 1);
g.addEdge(1, 3, 7);
g.addEdge(2, 4, 3);
g.addEdge(3, 4, 1);
g.addEdge(3, 5, 5);
g.addEdge(4, 5, 2);
g.dijkstra(0);
return 0;
}
2.Bellman-Ford算法
Bellman-Ford算法用于解决单源最短路径问题,对图中的权重可以为负值,但不能包含负权重环。
Bellman-Ford算法步骤:
初始化距离数组,将起始节点的距离设置为0,其他节点的距离设置为无穷大。
重复以下步骤 V-1 次(其中 V 是节点数):
对每一条边 (u, v) 进行松弛操作,即尝试通过节点 u 缩短到节点 v 的距离。
检测是否存在负权重环,如果存在则算法失败。
C++代码示例:
#include <iostream>
#include <vector>
#include <limits>
class Graph {
public:
Graph(int vertices) : V(vertices), edges(vertices) {}
void addEdge(int u, int v, int weight) {
edges.push_back({u, v, weight});
}
void bellmanFord(int start) {
std::vector<int> dist(V, std::numeric_limits<int>::max());
dist[start] = 0;
for (int i = 0; i < V - 1; ++i) {
for (const auto& edge : edges) {
int u = edge.u;
int v = edge.v;
int weight = edge.weight;
if (dist[u] != std::numeric_limits<int>::max() && dist[u] + weight < dist[v]) {
dist[v] = dist[u] + weight;
}
}
}
// Check for negative-weight cycles
for (const auto& edge : edges) {
int u = edge.u;
int v = edge.v;
int weight = edge.weight;
if (dist[u] != std::numeric_limits<int>::max() && dist[u] + weight < dist[v]) {
std::cout << "Graph contains a negative-weight cycle\n";
return;
}
}
// Print the shortest distances
std::cout << "Shortest distances from vertex " << start << ":\n";
for (int i = 0; i < V; ++i) {
std::cout << "To vertex " << i << ": " << dist[i] << "\n";
}
}
private:
struct Edge {
int u, v, weight;
};
int V; // Number of vertices
std::vector<Edge> edges; // Edges of the graph
};
int main() {
Graph g(6);
g.addEdge(0, 1, 2);
g.addEdge(0, 2, 4);
g.addEdge(1, 2, 1);
g.addEdge(1, 3, 7);
g.addEdge(2, 4, 3);
g.addEdge(3, 4, 1);
g.addEdge(3, 5, 5);
g.addEdge(4, 5, 2);
g.bellmanFord(0);
return 0;
}
Dijkstra算法对于权重非负的图效果更好,而Bellman-Ford算法可以处理包含负权重边的图。
二维数组表示:
#include <iostream>
#include <vector>
#include <queue>
#include <algorithm>
class WeightedGraphAlgorithms {
public:
// Prim's Algorithm for Minimum Spanning Tree
int primMST(const std::vector<std::vector<int>>& graph);
// Kruskal's Algorithm for Minimum Spanning Tree
int kruskalMST(const std::vector<std::vector<int>>& graph);
// Dijkstra's Algorithm for Shortest Path
std::vector<int> dijkstra(const std::vector<std::vector<int>>& graph, int start);
// Bellman-Ford Algorithm for Shortest Path
std::vector<int> bellmanFord(const std::vector<std::vector<int>>& graph, int start);
private:
// Utility function for finding the parent of a set (used in Kruskal's algorithm)
int find(int parent[], int i);
// Utility function for union of two sets (used in Kruskal's algorithm)
void unionSets(int parent[], int rank[], int x, int y);
};
int WeightedGraphAlgorithms::primMST(const std::vector<std::vector<int>>& graph) {
int V = graph.size();
std::vector<int> parent(V, -1);
std::vector<int> key(V, INT_MAX);
std::vector<bool> inMST(V, false);
key[0] = 0;
for (int count = 0; count < V - 1; ++count) {
int u = -1;
for (int v = 0; v < V; ++v) {
if (!inMST[v] && (u == -1 || key[v] < key[u])) {
u = v;
}
}
inMST[u] = true;
for (int v = 0; v < V; ++v) {
if (graph[u][v] && !inMST[v] && graph[u][v] < key[v]) {
parent[v] = u;
key[v] = graph[u][v];
}
}
}
int minCost = 0;
for (int i = 1; i < V; ++i) {
minCost += graph[parent[i]][i];
}
return minCost;
}
int WeightedGraphAlgorithms::find(int parent[], int i) {
if (parent[i] == -1) {
return i;
}
return find(parent, parent[i]);
}
void WeightedGraphAlgorithms::unionSets(int parent[], int rank[], int x, int y) {
int rootX = find(parent, x);
int rootY = find(parent, y);
if (rank[rootX] < rank[rootY]) {
parent[rootX] = rootY;
} else if (rank[rootX] > rank[rootY]) {
parent[rootY] = rootX;
} else {
parent[rootY] = rootX;
rank[rootX]++;
}
}
int WeightedGraphAlgorithms::kruskalMST(const std::vector<std::vector<int>>& graph) {
int V = graph.size();
std::vector<std::pair<int, std::pair<int, int>>> edges;
for (int i = 0; i < V; ++i) {
for (int j = 0; j < V; ++j) {
if (graph[i][j] != 0) {
edges.push_back({graph[i][j], {i, j}});
}
}
}
std::sort(edges.begin(), edges.end());
int minCost = 0;
std::vector<int> parent(V, -1);
std::vector<int> rank(V, 0);
for (const auto& edge : edges) {
int u = edge.second.first;
int v = edge.second.second;
int weight = edge.first;
int rootU = find(parent.data(), u);
int rootV = find(parent.data(), v);
if (rootU != rootV) {
minCost += weight;
unionSets(parent.data(), rank.data(), rootU, rootV);
}
}
return minCost;
}
std::vector<int> WeightedGraphAlgorithms::dijkstra(const std::vector<std::vector<int>>& graph, int start) {
int V = graph.size();
std::vector<int> dist(V, INT_MAX);
dist[start] = 0;
std::priority_queue<std::pair<int, int>, std::vector<std::pair<int, int>>, std::greater<std::pair<int, int>>> pq;
pq.push({0, start});
while (!pq.empty()) {
int u = pq.top().second;
pq.pop();
for (int v = 0; v < V; ++v) {
if (graph[u][v] && dist[u] != INT_MAX && dist[u] + graph[u][v] < dist[v]) {
dist[v] = dist[u] + graph[u][v];
pq.push({dist[v], v});
}
}
}
return dist;
}
std::vector<int> WeightedGraphAlgorithms::bellmanFord(const std::vector<std::vector<int>>& graph, int start) {
int V = graph.size();
std::vector<int> dist(V, INT_MAX);
dist[start] = 0;
for (int i = 0; i < V - 1; ++i) {
for (int u = 0; u < V; ++u) {
for (int v = 0; v < V; ++v) {
if (graph[u][v] && dist[u] != INT_MAX && dist[u] + graph[u][v] < dist[v]) {
dist[v] = dist[u] + graph[u][v];
}
}
}
}
// Check for negative-weight cycles
for (int u = 0; u < V; ++u) {
for (int v = 0; v < V; ++v) {
if (graph[u][v] && dist[u] != INT_MAX && dist[u] + graph[u][v] < dist[v]) {
// Graph contains a negative-weight cycle
return std::vector<int>();
}
}
}
return dist;
}
力扣实战
岛屿数量
给你一个由 '1'(陆地)和 '0'(水)组成的的二维网格,请你计算网格中岛屿的数量。
岛屿总是被水包围,并且每座岛屿只能由水平方向和/或竖直方向上相邻的陆地连接形成。
此外,你可以假设该网格的四条边均被水包围。
实现:
class Solution {
private:
void dfs(vector<vector<char>>& grid ,int r,int c){
int nr=grid.size();
int nc = grid[0].size();
grid[r][c] = '0';
if(r-1>=0&& grid[r-1][c] == '1') dfs(grid,r-1,c);
if(r+1<nr&& grid[r+1][c] == '1') dfs(grid,r+1,c);
if(c-1>=0&& grid[r][c-1] == '1') dfs(grid,r,c-1);
if(c+1<nc&& grid[r][c+1] == '1') dfs(grid,r,c+1);
}
public:
int numIslands(vector<vector<char>>& grid) {
//深度优先解法
// int nr = grid.size();
// if(!nr) return 0;
// int nc = grid[0].size();
// int num_islands = 0;
// for(int r=0;r<nr;++r){
// for(int c = 0;c<nc;++c){
// if(grid[r][c] == '1'){
// ++num_islands;
// dfs(grid,r,c);
// }
// }
// }
// return num_islands;
//广度优先解法
int nr = grid.size();
if(!nr) return 0;
int nc = grid[0].size();
int num_islands = 0;
for(int r = 0; r < nr ; ++r){
for(int c= 0; c < nc ; ++c ){
if(grid[r][c] == '1'){
++num_islands;
grid[r][c] = '0';
queue<pair<int,int>> neighbors;
neighbors.push({r,c});
while(!neighbors.empty()){
auto rc = neighbors.front();
neighbors.pop();
int row = rc.first,col = rc.second;
if(row-1>=0&& grid[row-1][col] == '1') {
neighbors.push({row-1,col});
grid[row-1][col] = '0';
}
if(row+1<nr&& grid[row+1][col] == '1') {
neighbors.push({row+1,col});
grid[row+1][col] = '0';
}
if(col-1>=0&& grid[row][col-1] == '1') {
neighbors.push({row,col-1});
grid[row][col-1] = '0';
}
if(col+1<nc&& grid[row][col+1] == '1') {
neighbors.push({row,col+1});
grid[row][col+1] = '0';
}
}
}
}
}
return num_islands;
}
};
课程表
你这个学期必须选修 numCourses 门课程,记为 0 到 numCourses - 1 。
在选修某些课程之前需要一些先修课程。 先修课程按数组 prerequisites 给出,其中 prerequisites[i] = [ai, bi] ,表示如果要学习课程 ai 则 必须 先学习课程 bi 。
例如,先修课程对 [0, 1] 表示:想要学习课程 0 ,你需要先完成课程 1 。
请你判断是否可能完成所有课程的学习?如果可以,返回 true ;否则,返回 false 。
实现:
class Solution {
public:
bool canFinish(int numCourses, vector<vector<int>>& prerequisites) {
vector<vector<int>> graph(numCourses, vector<int>());
vector<int> inDegree(numCourses, 0);
// 构建有向图和入度数组
for (const auto& prerequisite : prerequisites) {
int course = prerequisite[0];
int prerequisiteCourse = prerequisite[1];
graph[prerequisiteCourse].push_back(course);
inDegree[course]++;
}
queue<int> zeroInDegreeCourses;
// 将入度为0的课程加入队列
for (int i = 0; i < numCourses; ++i) {
if (inDegree[i] == 0) {
zeroInDegreeCourses.push(i);
}
}
// 进行拓扑排序
while (!zeroInDegreeCourses.empty()) {
int course = zeroInDegreeCourses.front();
zeroInDegreeCourses.pop();
for (int nextCourse : graph[course]) {
inDegree[nextCourse]--;
if (inDegree[nextCourse] == 0) {
zeroInDegreeCourses.push(nextCourse);
}
}
}
// 检查是否所有课程的入度都为0
for (int i = 0; i < numCourses; ++i) {
if (inDegree[i] != 0) {
return false;
}
}
return true;
}
};
算法思想的使用及举例
是否具有最优子结构性质?
- 如果问题可以被分解为子问题,且子问题的最优解可以组合成原问题的最优解,可能适合使用动态规划。
是否可以通过贪心选择策略得到全局最优解?
- 如果每一步的选择都是局部最优的,并且这些选择期望能够得到全局最优解,可能适合使用贪心算法。
是否可以通过深度优先搜索来穷尽所有可能的解空间?
- 如果问题的解空间是一个树状结构,可以通过深度优先搜索来穷尽所有可能的解空间,可能适合使用回溯法。
是否可以通过分治法将问题分解为互不相交的子问题?
- 如果问题可以被分解为互不相交的子问题,且通过合并子问题的解得到原问题的解,可能适合使用分治法。
是否需要遍历图的结构?
- 如果问题可以被建模成图的结构,可能需要使用图论算法,例如深度优先搜索、广度优先搜索等。
是否可以通过状态压缩来优化解空间搜索?
- 如果问题的状态空间非常大,可以考虑使用位运算等方法进行状态压缩,例如在动态规划或搜索问题中。
举例
- 组合、排列、子集问题通常涉及在给定集合中选择若干元素,满足某些条件。回溯法通过深度优先搜索的方式遍历解空间,逐步选择元素,撤销选择,寻找问题的解。
- 最短路径问题通常涉及在图中找到从一个节点到另一个节点的最短路径。图论算法如Dijkstra、Bellman-Ford等天然适用于解决这类问题。
- 背包问题涉及在给定容量的背包中选择一些物品,使得价值最大或者总重量最小。动态规划适合解决这类问题,因为它可以通过保存子问题的解来避免重复计算。
- 排序问题通常涉及将一组元素按照一定的规则进行排序。贪心算法适合解决这类问题,因为每一步都可以通过选择当前最优的元素来期望得到全局最优解。
- 分治法适合解决问题可以分解为互不相交的子问题,子问题独立求解,最后合并得到原问题解的情况。归并排序和快速排序是分治法的经典应用。
- 动态规划适合解决具有最优子结构性质的问题,其中问题的最优解可以由其子问题的最优解推导得到。例如,最长公共子序列问题。
- 图的遍历问题,如寻找连通分量、拓扑排序等,通常可以通过深度优先搜索和广度优先搜索解决。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。