算法篇——动态规划【力扣Hot100】
文章目录
- 一、力扣Hot100
- 1.1完全平方数
- 1.2最长递增子序列
- 1.3乘积最大子数组
- 1.4分割等和子集
- 1.5最长有效括号
- 二、二维dp
- 2.1最长回文子串
- 2.2最长公共子序列
- 2.3编辑距离
- 三、贪心算法
- 3.1跳跃游戏
- 3.2跳跃游戏2
- 3.3划分字母区间
- 四、回溯
- 4.1全排列
- 4.2子集
- 4.3字母组合
- 4.4组合总和
- 4.5括号生成
- 4.6单词搜索
- 4.7分割回文串
- 4.8N皇后
一、力扣Hot100
1.1完全平方数
给你一个整数 n ,返回 和为 n 的完全平方数的最少数量 。
完全平方数 是一个整数,其值等于另一个整数的平方;换句话说,其值等于一个整数自乘的积。例如,1、4、9 和 16 都是完全平方数,而 3 和 11 不是。
int numSquares(int n) {// 预处理所有小于等于n的完全平方数vector<int> squares;for (int i = 1; i * i <= n; ++i) {squares.push_back(i * i);}vector<int> dp(n + 1, INT_MAX);dp[0] = 0; // 填充dp数组for (int i = 1; i <= n; ++i) {for (int square : squares) {if (i >= square) {dp[i] = min(dp[i], dp[i - square] + 1);}}}return dp[n];
}
1.2最长递增子序列
给你一个整数数组 nums ,找到其中最长严格递增子序列的长度。
子序列 是由数组派生而来的序列,删除(或不删除)数组中的元素而不改变其余元素的顺序。例如,[3,6,2,7] 是数组 [0,3,1,6,2,2,7] 的子序列。
int lengthOfLIS(vector<int>& nums) {if (nums.empty()) return 0;int n = nums.size();// dp[i]表示以nums[i]结尾的最长严格递增子序列的长度vector<int> dp(n, 1);int max_len = 1;for (int i = 1; i < n; ++i) {// 检查所有在i之前的元素for (int j = 0; j < i; ++j) {// 如果当前元素大于之前的元素,说明可以构成更长的子序列if (nums[i] > nums[j]) {dp[i] = max(dp[i], dp[j] + 1);}}// 更新最长子序列长度max_len = max(max_len, dp[i]);}return max_len;
}
1.3乘积最大子数组
给你一个整数数组 nums ,请你找出数组中乘积最大的非空连续 子数组(该子数组中至少包含一个数字),并返回该子数组所对应的乘积。测试用例的答案是一个 32-位 整数。
int maxProduct(vector<int>& nums) {if (nums.empty()) return 0;int max_product = nums[0];int current_max = nums[0]; // 当前最大值int current_min = nums[0]; // 当前最小值(处理负数情况)for (int i = 1; i < nums.size(); ++i) {// 保存当前最大值,因为它会在计算current_min时被修改int temp = current_max;// 更新当前最大值和最小值current_max = max({nums[i], current_max * nums[i], current_min * nums[i]});current_min = min({nums[i], temp * nums[i], current_min * nums[i]});// 更新全局最大乘积max_product = max(max_product, current_max);}return max_product;
}
1.4分割等和子集
给你一个 只包含正整数 的 非空 数组 nums 。请你判断是否可以将这个数组分割成两个子集,使得两个子集的元素和相等。
问题可以转化为:是否存在一个子集,其元素和等于数组总和的一半。这是一个典型的 0-1 背包问题变体。
bool canPartition(vector<int>& nums) {// 计算数组总和int total_sum = accumulate(nums.begin(), nums.end(), 0);// 如果总和是奇数,不可能分成两个和相等的子集if (total_sum % 2 != 0) {return false;}// 目标子集和为总和的一半int target = total_sum / 2;// dp[i]表示能否组成和为i的子集vector<bool> dp(target + 1, false);dp[0] = true; // 基础情况:和为0的子集总是存在(空子集)for (int num : nums) {// 从后往前更新,避免重复使用同一个元素for (int j = target; j >= num; --j) {dp[j] = dp[j] || dp[j - num];}}// 返回能否组成目标和的子集return dp[target];
}
1.5最长有效括号
给你一个只包含 ‘(’ 和 ‘)’ 的字符串,找出最长有效(格式正确且连续)括号 子串 的长度。
左右括号匹配,即每个左括号都有对应的右括号将其闭合的字符串是格式正确的,比如 “(()())”。
int longestValidParentheses(string s) {int n = s.size();if (n == 0) return 0;// dp[i]表示以s[i]结尾的最长有效括号子串的长度vector<int> dp(n, 0);int max_len = 0;for (int i = 1; i < n; ++i) {// 只有当当前字符是右括号时才可能形成有效括号if (s[i] == ')') {// 情况1:与前一个字符形成"()"if (s[i-1] == '(') {dp[i] = (i >= 2 ? dp[i-2] : 0) + 2;}// 情况2:与前面的有效子串形成"(...)"else if (i - dp[i-1] > 0 && s[i - dp[i-1] - 1] == '(') {dp[i] = dp[i-1] + 2 + (i - dp[i-1] >= 2 ? dp[i - dp[i-1] - 2] : 0);}max_len = max(max_len, dp[i]);}}return max_len;
}
二、二维dp
2.1最长回文子串
给你一个字符串 s,找到 s 中最长的 回文 子串。
string longestPalindrome(string s) {int n = s.size();if (n == 0) return "";// dp[i][j]表示s[i..j]是否为回文子串vector<vector<bool>> dp(n, vector<bool>(n, false));int start = 0, max_len = 1;// 单个字符都是回文for (int i = 0; i < n; ++i) {dp[i][i] = true;}// 检查长度为2的子串for (int i = 0; i < n - 1; ++i) {if (s[i] == s[i + 1]) {dp[i][i + 1] = true;start = i;max_len = 2;}}// 检查长度大于2的子串for (int len = 3; len <= n; ++len) {for (int i = 0; i <= n - len; ++i) {int j = i + len - 1; // 子串的结束索引// 如果首尾字符相等且中间子串是回文,则当前子串是回文if (s[i] == s[j] && dp[i + 1][j - 1]) {dp[i][j] = true;if (len > max_len) {start = i;max_len = len;}}}}return s.substr(start, max_len);
}
2.2最长公共子序列
给定两个字符串 text1 和 text2,返回这两个字符串的最长 公共子序列 的长度。如果不存在 公共子序列 ,返回 0 。
一个字符串的 子序列 是指这样一个新的字符串:它是由原字符串在不改变字符的相对顺序的情况下删除某些字符(也可以不删除任何字符)后组成的新字符串。
例如,“ace” 是 “abcde” 的子序列,但 “aec” 不是 “abcde” 的子序列。
两个字符串的 公共子序列 是这两个字符串所共同拥有的子序列。
int longestCommonSubsequence(string text1, string text2) {int m = text1.size();int n = text2.size();// 创建dp数组,dp[i][j]表示text1[0..i-1]和text2[0..j-1]的最长公共子序列长度vector<vector<int>> dp(m + 1, vector<int>(n + 1, 0));for (int i = 1; i <= m; ++i) {for (int j = 1; j <= n; ++j) {// 如果当前字符相等,则最长公共子序列长度为前一个状态+1if (text1[i - 1] == text2[j - 1]) {dp[i][j] = dp[i - 1][j - 1] + 1;} // 如果当前字符不相等,则取去掉其中一个字符后的最长公共子序列长度的较大值else {dp[i][j] = max(dp[i - 1][j], dp[i][j - 1]);}}}// dp[m][n]存储的是两个完整字符串的最长公共子序列长度return dp[m][n];
}
2.3编辑距离
给你两个单词 word1 和 word2, 请返回将 word1 转换成 word2 所使用的最少操作数 。
你可以对一个单词进行如下三种操作:
插入一个字符
删除一个字符
替换一个字符
int minDistance(string word1, string word2) {int m = word1.size();int n = word2.size();// dp[i][j]表示将word1[0..i-1]转换为word2[0..j-1]的最少操作数vector<vector<int>> dp(m + 1, vector<int>(n + 1, 0));// 初始化边界条件// 将空字符串转换为word2[0..j-1]需要j次插入操作for (int j = 1; j <= n; ++j) {dp[0][j] = j;}// 将word1[0..i-1]转换为空字符串需要i次删除操作for (int i = 1; i <= m; ++i) {dp[i][0] = i;}// 填充dp数组for (int i = 1; i <= m; ++i) {for (int j = 1; j <= n; ++j) {// 如果当前字符相同,则不需要额外操作if (word1[i - 1] == word2[j - 1]) {dp[i][j] = dp[i - 1][j - 1];} else {// 否则,取三种操作(插入、删除、替换)中的最小值加1dp[i][j] = min({dp[i][j - 1], // 插入操作:在word1中插入word2[j-1]dp[i - 1][j], // 删除操作:删除word1[i-1]dp[i - 1][j - 1] // 替换操作:将word1[i-1]替换为word2[j-1]}) + 1;}}}return dp[m][n];
}
三、贪心算法
3.1跳跃游戏
给你一个非负整数数组 nums ,你最初位于数组的 第一个下标 。数组中的每个元素代表你在该位置可以跳跃的最大长度。
判断你是否能够到达最后一个下标,如果可以,返回 true ;否则,返回 false 。
bool canJump(vector<int>& nums) {int n = nums.size();// 记录当前能够到达的最远位置int farthest = 0;for (int i = 0; i < n; ++i) {// 如果当前位置已经超过了能到达的最远位置,说明无法继续前进if (i > farthest) {return false;}// 更新能够到达的最远位置farthest = max(farthest, i + nums[i]);// 如果已经能够到达或超过最后一个位置,直接返回trueif (farthest >= n - 1) {return true;}}// 循环结束仍未返回,说明无法到达最后一个位置return false;
}
3.2跳跃游戏2
给定一个长度为 n 的 0 索引整数数组 nums。初始位置在下标 0。
每个元素 nums[i] 表示从索引 i 向后跳转的最大长度。换句话说,如果你在索引 i 处,你可以跳转到任意 (i + j) 处:0 <= j <= nums[i] 且 i + j < n
返回到达 n - 1 的最小跳跃次数。测试用例保证可以到达 n - 1。
int jump(vector<int>& nums) {int n = nums.size();if (n == 1) return 0; // 特殊情况:只有一个元素,无需跳跃int jumpCount = 0; // 记录最小跳跃次数int currentEnd = 0; // 当前轮次跳跃的边界(到达此位置前,无需新跳跃)int farthest = 0; // 当前轮次能到达的最远位置// 遍历数组(不遍历最后一个元素,因为到达即停止)for (int i = 0; i < n - 1; ++i) {// 更新当前轮次能到达的最远位置(i + nums[i]是从i出发的最大跳跃距离)farthest = max(farthest, i + nums[i]);// 当遍历到当前边界的终点时,说明需要进行一次跳跃if (i == currentEnd) {jumpCount++; // 跳跃次数+1currentEnd = farthest; // 将边界更新为当前轮次的最远位置// 提前终止:若新边界已覆盖最后一个下标,无需继续遍历if (currentEnd >= n - 1) {break;}}}return jumpCount;
}
3.3划分字母区间
给你一个字符串 s 。我们要把这个字符串划分为尽可能多的片段,同一字母最多出现在一个片段中。例如,字符串 “ababcc” 能够被分为 [“abab”, “cc”],但类似 [“aba”, “bcc”] 或 [“ab”, “ab”, “cc”] 的划分是非法的。
注意,划分结果需要满足:将所有划分结果按顺序连接,得到的字符串仍然是 s 。返回一个表示每个字符串片段的长度的列表。
vector<int> partitionLabels(string s) {vector<int> result;int n = s.size();// 记录每个字符最后出现的位置vector<int> last_pos(26, 0);for (int i = 0; i < n; ++i) {last_pos[s[i] - 'a'] = i;}int start = 0; // 当前片段的起始位置int current_end = 0; // 当前片段的结束位置for (int i = 0; i < n; ++i) {// 更新当前片段的结束位置为当前字符最后出现位置和当前结束位置的最大值current_end = max(current_end, last_pos[s[i] - 'a']);// 当遍历到当前片段的结束位置时,说明可以划分出一个片段if (i == current_end) {result.push_back(current_end - start + 1);start = current_end + 1; // 下一个片段的起始位置}}return result;
}
四、回溯
4.1全排列
给定一个不含重复数字的数组 nums ,返回其 所有可能的全排列 。你可以 按任意顺序 返回答案。
class Solution {
public:vector<vector<int>> permute(vector<int>& nums) {vector<vector<int>> result; // 存储所有全排列结果vector<int> current; // 存储当前正在构建的排列vector<bool> used(nums.size(), false); // 标记元素是否已被使用backtrack(nums, current, used, result); // 启动回溯return result;}
private:// 回溯函数:参数分别为原数组、当前排列、使用状态、结果集void backtrack(const vector<int>& nums, vector<int>& current, vector<bool>& used, vector<vector<int>>& result) {// 终止条件:当前排列长度等于原数组长度,说明已构建一个完整排列if (current.size() == nums.size()) {result.push_back(current); // 将当前排列加入结果集return;}// 遍历原数组的每个元素,尝试加入当前排列for (int i = 0; i < nums.size(); ++i) {if (!used[i]) { // 只选择未被使用的元素// 1. 选择:标记元素为已使用,并加入当前排列used[i] = true;current.push_back(nums[i]);// 2. 探索:递归构建下一个位置的元素backtrack(nums, current, used, result);// 3. 撤销选择:回溯到上一步,恢复状态(为下一轮选择做准备)current.pop_back();used[i] = false;}}}
};
4.2子集
给你一个整数数组 nums ,数组中的元素 互不相同 。返回该数组所有可能的子集(幂集)。
解集 不能 包含重复的子集。你可以按 任意顺序 返回解集。
class Solution {
public:vector<vector<int>> subsets(vector<int>& nums) {vector<vector<int>> result; // 存储所有子集vector<int> current; // 存储当前正在构建的子集backtrack(nums, 0, current, result); // 从索引0开始回溯return result;}private:// 回溯函数:参数为原数组、当前探索的起始索引、当前子集、结果集void backtrack(const vector<int>& nums, int start, vector<int>& current, vector<vector<int>>& result) {// 终止条件:每进入一次递归,就将当前子集加入结果(包括空集)result.push_back(current);// 遍历从start开始的元素,避免重复选择(如[1,2]和[2,1]视为同一子集,需按顺序选)for (int i = start; i < nums.size(); ++i) {// 1. 选择:将当前元素加入子集current.push_back(nums[i]);// 2. 探索:递归处理下一个元素(起始索引+1,确保不回头选前面的元素)backtrack(nums, i + 1, current, result);// 3. 撤销选择:回溯到上一步,移除当前元素,尝试不选该元素的分支current.pop_back();}}
};
4.3字母组合
给定一个仅包含数字 2-9 的字符串,返回所有它能表示的字母组合。答案可以按 任意顺序 返回。
给出数字到字母的映射如下(与电话按键相同)。注意 1 不对应任何字母。
class Solution {
public:vector<string> letterCombinations(string digits) {vector<string> result;if (digits.empty()) return result; // 数字到字母的映射,索引对应数字vector<string> mapping = {"", // 0"", // 1"abc", // 2"def", // 3"ghi", // 4"jkl", // 5"mno", // 6"pqrs", // 7"tuv", // 8"wxyz" // 9};string current;backtrack(digits, 0, mapping, current, result);return result;}
private:// 回溯函数:生成所有可能的字母组合void backtrack(const string& digits, int index, const vector<string>& mapping,string& current, vector<string>& result) {// 终止条件:当前组合长度等于数字字符串长度if (index == digits.size()) {result.push_back(current);return;}// 获取当前数字对应的所有可能字母int digit = digits[index] - '0';const string& letters = mapping[digit];// 尝试当前数字对应的每个字母for (char c : letters) {// 选择当前字母current.push_back(c);// 递归处理下一个数字backtrack(digits, index + 1, mapping, current, result);// 撤销选择,回溯current.pop_back();}}
};
4.4组合总和
给你一个 无重复元素 的整数数组 candidates 和一个目标整数 target ,找出 candidates 中可以使数字和为目标数 target 的 所有 不同组合 ,并以列表形式返回。你可以按 任意顺序 返回这些组合。
candidates 中的 同一个 数字可以 无限制重复被选取 。如果至少一个数字的被选数量不同,则两种组合是不同的。
class Solution {
public:vector<vector<int>> combinationSum(vector<int>& candidates, int target) {vector<vector<int>> result; // 存储所有符合条件的组合vector<int> current; // 存储当前正在构建的组合// 排序:为剪枝做准备(确保按顺序选择元素,避免重复组合)sort(candidates.begin(), candidates.end());// 启动回溯:从索引0开始,当前和为0backtrack(candidates, target, 0, 0, current, result);return result;}private:// 回溯函数:参数分别为原数组、目标和、当前选择的起始索引、当前和、当前组合、结果集void backtrack(const vector<int>& candidates, int target, int start, int currentSum, vector<int>& current, vector<vector<int>>& result) {// 终止条件1:当前和等于目标和,记录组合if (currentSum == target) {result.push_back(current);return;}// 终止条件2:当前和超过目标和,直接返回(剪枝:因数组已排序,后续元素更大,无需继续)if (currentSum > target) {return;}// 遍历元素:从start开始,避免回头选前面的元素(防止重复组合)for (int i = start; i < candidates.size(); ++i) {int num = candidates[i];// 剪枝:若当前元素+当前和已超过目标,后续元素更大,直接break(数组已排序)if (currentSum + num > target) {break;}// 1. 选择:将当前元素加入组合,更新当前和current.push_back(num);currentSum += num;// 2. 探索:递归处理(起始索引仍为i,允许重复选取当前元素)backtrack(candidates, target, i, currentSum, current, result);// 3. 撤销选择:回溯到上一步,恢复当前和与当前组合currentSum -= num;current.pop_back();}}
};
4.5括号生成
数字 n 代表生成括号的对数,请你设计一个函数,用于能够生成所有可能的并且 有效的 括号组合。
class Solution {
public:vector<string> generateParenthesis(int n) {vector<string> result;string current;// 从0个左括号和0个右括号开始生成backtrack(result, current, 0, 0, n);return result;}private:// 回溯函数:生成所有有效的括号组合// 参数:结果集、当前组合、左括号数量、右括号数量、总对数void backtrack(vector<string>& result, string& current, int open, int close, int n) {// 终止条件:当前组合长度等于2n(n对括号)if (current.size() == 2 * n) {result.push_back(current);return;}// 如果左括号数量小于n,可以添加左括号if (open < n) {current.push_back('(');backtrack(result, current, open + 1, close, n);current.pop_back(); // 回溯}// 如果右括号数量小于左括号数量,可以添加右括号(保证有效性)if (close < open) {current.push_back(')');backtrack(result, current, open, close + 1, n);current.pop_back(); // 回溯}}
};
4.6单词搜索
给定一个 m x n 二维字符网格 board 和一个字符串单词 word 。如果 word 存在于网格中,返回 true ;否则,返回 false 。
单词必须按照字母顺序,通过相邻的单元格内的字母构成,其中“相邻”单元格是那些水平相邻或垂直相邻的单元格。同一个单元格内的字母不允许被重复使用。
class Solution {
public:bool exist(vector<vector<char>>& board, string word) {m = board.size();n = board[0].size();// 遍历网格中每个单元格,找到单词首字母的位置作为DFS起点for (int i = 0; i < m; ++i) {for (int j = 0; j < n; ++j) {if (board[i][j] == word[0]) { // 匹配首字母,开始DFSif (dfs(board, word, i, j, 0)) {return true;}}}}return false; // 所有起点均未找到匹配路径}private:int m, n; // 网格的行数和列数// 上下左右四个方向的偏移量(dx, dy)vector<pair<int, int>> dirs = {{-1, 0}, {1, 0}, {0, -1}, {0, 1}};// DFS函数:参数为网格、目标单词、当前坐标(i,j)、当前匹配的单词索引kbool dfs(vector<vector<char>>& board, string& word, int i, int j, int k) {// 终止条件1:已匹配完所有字符(k等于单词长度),说明找到路径if (k == word.size()) {return true;}// 终止条件2:当前坐标越界,或字符不匹配,或已访问过(直接返回false)if (i < 0 || i >= m || j < 0 || j >= n || board[i][j] != word[k]) {return false;}// 1. 标记当前单元格为已访问(临时修改为特殊字符,避免重复使用)char temp = board[i][j];board[i][j] = '#'; // 用'#'标记,后续递归中不会再次匹配// 2. 探索四个方向的相邻单元格for (auto& dir : dirs) {int ni = i + dir.first; // 新行坐标int nj = j + dir.second; // 新列坐标// 递归探索下一个字符(k+1),若找到路径则直接返回trueif (dfs(board, word, ni, nj, k + 1)) {return true;}}// 3. 回溯:恢复当前单元格的原始字符(不影响其他路径探索)board[i][j] = temp;// 4. 四个方向均未找到有效路径,返回falsereturn false;}
};
4.7分割回文串
给你一个字符串 s,请你将 s 分割成一些 子串,使每个子串都是 回文串 。返回 s 所有可能的分割方案。
class Solution {
public:vector<vector<string>> partition(string s) {vector<vector<string>> result; // 存储所有分割方案vector<string> current; // 存储当前分割方案backtrack(s, 0, current, result);return result;}private:// 回溯函数:从start位置开始分割字符串void backtrack(const string& s, int start, vector<string>& current, vector<vector<string>>& result) {// 终止条件:如果已分割完整个字符串,记录当前方案if (start == s.size()) {result.push_back(current);return;}// 尝试从start位置开始的所有可能的子串for (int end = start; end < s.size(); ++end) {// 检查子串s[start..end]是否为回文串if (isPalindrome(s, start, end)) {// 1. 选择:将回文子串加入当前方案current.push_back(s.substr(start, end - start + 1));// 2. 探索:递归处理剩余部分backtrack(s, end + 1, current, result);// 3. 撤销选择:回溯,移除当前子串current.pop_back();}}}// 辅助函数:检查s[left..right]是否为回文串bool isPalindrome(const string& s, int left, int right) {while (left < right) {if (s[left] != s[right]) {return false;}left++;right--;}return true;}
};
4.8N皇后
按照国际象棋的规则,皇后可以攻击与之处在同一行或同一列或同一斜线上的棋子。
n 皇后问题 研究的是如何将 n 个皇后放置在 n×n 的棋盘上,并且使皇后彼此之间不能相互攻击。
给你一个整数 n ,返回所有不同的 n 皇后问题 的解决方案。
每一种解法包含一个不同的 n 皇后问题 的棋子放置方案,该方案中 ‘Q’ 和 ‘.’ 分别代表了皇后和空位。
class Solution {
public:vector<vector<string>> solveNQueens(int n) {vector<vector<string>> result; // 存储所有解决方案vector<string> board(n, string(n, '.')); // 初始化棋盘,全部为空白// 用于记录已占用的列和两条对角线,避免皇后相互攻击unordered_set<int> cols, diag1, diag2;// 从第0行开始放置皇后backtrack(0, n, board, cols, diag1, diag2, result);return result;}private:// 回溯函数:尝试在第row行放置皇后void backtrack(int row, int n, vector<string>& board, unordered_set<int>& cols, unordered_set<int>& diag1, unordered_set<int>& diag2,vector<vector<string>>& result) {// 终止条件:如果已经处理完所有行,说明找到一个有效解决方案if (row == n) {result.push_back(board);return;}// 尝试在当前行的每一列放置皇后for (int col = 0; col < n; ++col) {// 检查当前位置是否会受到已有皇后的攻击// 同一列、同一主对角线(row-col相同)、同一副对角线(row+col相同)if (cols.count(col) || diag1.count(row - col) || diag2.count(row + col)) {continue; // 不能放置皇后,尝试下一列}// 1. 选择:在当前位置放置皇后board[row][col] = 'Q';cols.insert(col);diag1.insert(row - col);diag2.insert(row + col);// 2. 探索:递归处理下一行backtrack(row + 1, n, board, cols, diag1, diag2, result);// 3. 撤销选择:回溯,恢复状态board[row][col] = '.';cols.erase(col);diag1.erase(row - col);diag2.erase(row + col);}}
};