# Ca1mCpp **Repository Path**: ccjabc/ca1m-cpp ## Basic Information - **Project Name**: Ca1mCpp - **Description**: 部分c++笔记和算法刷题笔记。 - **Primary Language**: Unknown - **License**: Not specified - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 1 - **Created**: 2025-10-21 - **Last Updated**: 2025-10-21 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README | 章节 | 内容 | | ---- | --------------------------------------- | | | [C++基础](./CppBasic.md) | | | [C++类](./CppClass.md) | | | [C++11](./Cpp11.md) | | | [C++部分设计模式](./DesignPattern.md) | | | [C++ Socket简记](./SocketNetwork.md) | | | [C++ Primer部分笔记](./Cprimer.md) | ## 算法一刷部分笔记 → [代码随想录](https://programmercarl.com/) ## 链表 ```cpp // 单链表 struct ListNode { int val; // 节点上存储的元素 ListNode *next; // 指向下一个节点的指针 ListNode(int x) : val(x), next(NULL) {} // 节点的构造函数 }; ``` ### 两两交换链表中的节点 ![](./res/img1.png) - 不允许交换节点内部的值! - 三步交换! ```cpp ListNode *swapPairs(ListNode *head) { if (head == nullptr || head->next == nullptr) return head; // 设置辅助"头节点" ListNode *dummyHead = new ListNode(); dummyHead->next = head; ListNode *p, *l, *r, *temp; // 遍历初始化 p = dummyHead; l = head; r = head->next; while (l != nullptr && r != nullptr) { temp = r->next; // 辅助节点,防止“断开” // 交换相邻节点 p->next = r; r->next = l; l->next = temp; // 依次遍历 p = l; l = temp; if (l == nullptr || l->next == nullptr) break; r = l->next; } // 释放空间,返回头节点 head = dummyHead->next; delete dummyHead; return head; } ``` ### 相交链表 - 计算链表长度之差 - 长链表向前遍历差值步 - 依次遍历以判断链表是否有交点 ```cpp class Solution { public: ListNode *getIntersectionNode(ListNode *headA, ListNode *headB) { int numA, numB; // 链表长度 numA = numB = 0; ListNode* p = headA; ListNode* q = headB; // 计算链表长度之差 while (p != nullptr) { numA++; p = p->next; } while (q != nullptr) { numB++; q = q->next; } int diff = numA > numB ? numA - numB : numB - numA; p = headA; q = headB; // 长链表向前遍历差值步 if (numA > numB) while (diff--) p = p->next; else while (diff--) q = q->next; // 判断是否相交 while (p != nullptr) { // 相交 -> 返回交点 if (p == q) return p; p = p->next; q = q->next; } return nullptr; } }; ``` ### 环形链表 - 判断是否有环 -> 快慢指针法 - 如果有环,利用双指针寻找入环节点 ```cpp class Solution { public: ListNode* detectCycle(ListNode* head) { if (head == nullptr) return nullptr; // 判断链表是否有环 -> 快慢指针法 ListNode *slow, *fast; slow = fast = head; while (fast != nullptr && fast->next != nullptr) { slow = slow->next; fast = fast->next->next; if (fast == slow) { // 链表有环 -> 利用双指针寻找入环第1个节点 ListNode* p = head; while (p != slow) { p = p->next; slow = slow->next; } return p; } } // 若程序未在while循环中结束,说明无环 return nullptr; } }; ``` 补充:在判断链表有环之后,如何寻找环的入口? 假设从头结点到环形入口节点的节点数为x,环形入口节点到fast指针与slow指针相遇节点节点数为y,从相遇节点再到环形入口节点节点数为 z。 ![image.png](./res/img2.png) 相遇时slow指针遍历过的节点数为 x + y,fast指针遍历过的节点数为 x + y + n(y + z),n代表fast指针在环内走了n圈才遇到slow指针。注意到fast指针一步走两个节点,slow指针一步走一个节点,则有: $(x + y) * 2 = x + y + n(y + z) → x + y = n(y + z) (n >= 1).$ - n = 1时,x = z,即从头结点出发一个指针,从相遇节点 也出发一个指针,这两个指针每次只走一个节点, 那么当这两个指针相遇的时候就是 环形入口的节点 - n > 1时,同理! ## 哈希表 [哈希表理论基础](https://programmercarl.com/%E5%93%88%E5%B8%8C%E8%A1%A8%E7%90%86%E8%AE%BA%E5%9F%BA%E7%A1%80.html) ## 双指针法 ### 双指针法的核心 利用两个“指针”解决问题(反转数组) ### 反转链表 ![image.png](./res/img3.png) ```cpp ListNode* reverseList(ListNode* head) { ListNode *pre = nullptr; ListNode *cur = head; while (cur != nullptr) { ListNode *temp = cur->next; // 辅助节点 → 保存cur的下一节点 cur->next = pre; // 改变指针方向 // 更新cur和pre指针 pre = cur; cur = temp; } return pre; } ``` - 时间复杂度:O(n) - 空间复杂度:O(1) ### 删除链表的倒数第N个结点 ![image.png](./res/img4.png) ```cpp class Solution { public: ListNode* removeNthFromEnd(ListNode* head, int n) { // 设置辅助“头节点” ListNode* dummyHead = new ListNode(); dummyHead->next = head; // 快慢指针法 ListNode* slow = head; ListNode* fast = head; // 辅助指针 ListNode* pre = dummyHead; while (n--) { fast = fast->next; } while (fast != nullptr) { pre = pre->next; slow= slow->next; fast = fast->next; } // 删除第n个节点 pre->next = slow->next; delete slow; // 释放空间,返回结果 head = dummyHead->next; delete dummyHead; return head; } }; ``` - 时间复杂度:O(n) - 空间复杂度:O(1) ## 栈与队列 [栈与队列理论基础](https://programmercarl.com/%E6%A0%88%E4%B8%8E%E9%98%9F%E5%88%97%E7%90%86%E8%AE%BA%E5%9F%BA%E7%A1%80.html) ## 二叉树 [二叉树理论基础](https://programmercarl.com/%E4%BA%8C%E5%8F%89%E6%A0%91%E7%90%86%E8%AE%BA%E5%9F%BA%E7%A1%80.html) ## 回溯算法 ### 回溯算法的核心 - 本质上是穷举,筛选所有可能中符合条件的结果 - 剪枝可以提高效率 - 回溯可以解决的问题都可以抽象为树形结构 ![](./res/img5.png) ```cpp // framework of backtracking void backtracking(参数) { if (终止条件) { 存放结果; return; } for (选择:本层集合中元素(树中节点孩子的数量就是集合的大小)) { 处理节点; backtracking(路径,选择列表); // 递归 回溯,撤销处理结果 } } ``` ### 电话号码的字符组合 ![](./res/img6.png) ```cpp class Solution { public: vector res; string str; // 字母数字对应表 const string letterMap[10] = { "", "", "abc", // 2 "def", // 3 "ghi", // 4 "jkl", // 5 "mno", // 6 "pqrs", // 7 "tuv", // 8 "wxyz", // 9 }; void backtracking(string digits, int index) { if (index == digits.length()) { // 终止条件 res.push_back(str); return; } int numIndex = int(digits[index] - '0'); // 将index指向的数字转为int for (int i = 0; i < letterMap[numIndex].size(); i++) { str.push_back(letterMap[numIndex][i]); backtracking(digits, index + 1); str.pop_back(); } } vector letterCombinations(string digits) { if (digits.length() == 0) return {}; backtracking(digits, 0); // 回溯 return res; } }; ``` - 时间复杂度:O(3 ^ m × 4 ^ n) - 空间复杂度:O(3 ^ m × 4 ^ n) - m:对应3个字母的数字个数 - n:对应4个字母的数字个数 ### 组合总和II 给定一个候选人编号的集合 candidates 和一个目标数 target ,找出candidates 中所有可以使数字和为 target 的组合。candidates 中的每个数字在每个组合中只能使用 一次 。注:解集不能包含重复的组合。 示例 - 输入:candidates = [10, 1, 2, 7, 6, 1, 5], target = 8 - 输出:[[1, 1, 6], [1, 2, 5], [1, 7], [2, 6]] ![](./res/img7.png) - used[i - 1] == true → 同一树枝candidates[i - 1]使用过 - used[i - 1] == false → 同一树层candidates[i - 1]使用过 → 去重! ```cpp class Solution { public: vector> res; vector vec; void backtracking(vector& candidates, int target, int index, vector& used) { if (accumulate(vec.begin(), vec.end(), 0) == target) res.push_back(vec); // 终止条件 for (int i = index; i < candidates.size(); i++) { // used[i - 1] == true → 同一树枝candidates[i - 1]使用过 // used[i - 1] == false → 同一树层candidates[i - 1]使用过 if (i > 0 && candidates[i] == candidates[i - 1] && used[i - 1] == false) continue; vec.push_back(candidates[i]); used[i] = true; backtracking(candidates, target, i + 1, used); // 回溯! vec.pop_back(); used[i] = false; } } vector> combinationSum2(vector& candidates, int target) { res.clear(); vec.clear(); sort(candidates.begin(), candidates.end()); vector used(candidates.size(), false); // 回溯 backtracking(candidates, target, 0, used); return res; } }; ``` - 时间复杂度:O(n * 2 ^ n) - 空间复杂度:O(n) 组合总和:给定一个无重复元素的数组 candidates 和一个目标数 target ,找出candidates 中所有可以使数字和为 target 的组合。candidates 中的数字可以无限制重复被选取。 - 与组合总和II 相比,组合总和无需去重! 组合总和III:找出所有相加之和为 n 的 k 个数的组合。组合中只允许含有 1 - 9 的正整数,并且每种组合中不存在重复的数字。说明:所有数字都是正整数且解集不能包含重复的组合。 - 与组合总和相似,亦无需去重! ### 分割回文串 - 给定字符串s,请将s分割成一些子串,使每个子串都是回文串,返回s的所有可能的分割方案 - 输入:s="aab",输出:[["a","a","b"], ["aa", "b"]] ```cpp class Solution { public: vector> res; vector path; // 判断是否是回文数 bool isPalindromic(string s, int start, int end) { for (int i = start, j = end; i < j; i++, j--) { if (s[i] != s[j]) return false; } return true; } void backtracking(const string& s, int startIndex) { // 终止条件 if (startIndex >= s.size()) { res.push_back(path); return; } for (int i = startIndex; i < s.size(); i++) { if (isPalindromic(s, startIndex, i)) { // 获取[startIndex, i]在s中的子串 string str = s.substr(startIndex, i - startIndex + 1); path.push_back(str); } else continue; backtracking(s, i + 1); path.pop_back(); } } vector> partition(string s) { res.clear(); path.clear(); backtracking(s, 0); // 回溯 return res; } }; ``` ### 非递减子序列 ```cpp class Solution { public: vector> result; vector path; // 判断序列是否非递减 bool isIncreasing(vector vec) { for (int i = 0; i < vec.size() - 1; i++) { if (vec[i] > vec[i + 1]) return false; } return true; } void backtracking(vector& nums, int startIndex) { if (path.size() >= 2 && isIncreasing(path)) result.push_back(path); unordered_set uset; // 使用set对本层元素进行去重 for (int i = startIndex; i < nums.size(); i++) { // 去重 // if (i > startIndex && nums[i] == nums[i - 1]) // continue; if ((uset.find(nums[i]) != uset.end())) { continue; } uset.insert(nums[i]); // 记录这个元素在本层用过了,本层后面不能再用了 path.push_back(nums[i]); backtracking(nums, i + 1); path.pop_back(); } } vector> findSubsequences(vector& nums) { result.clear(); path.clear(); backtracking(nums, 0); return result; } }; ``` - 时间复杂度:O(n * 2 ^ n) - 空间复杂度:O(n) - 若按注释部分的方式去重,则无法对本层元素进行去重,即无法处理形如[1,2,1,1]这样的整数数组! - 而用unordered_set进行去重,可保证本层元素不重复,且数组亦可实现此功能(数值范围小的话用数组效率会更高!) ### N皇后与解数独 - N皇后套用回溯框架的思路:在处理节点之前先判断某位置放置皇后是否合理(isValid函数),其他逻辑相同! ```cpp class Solution { public: vector> result; // 判断当前棋子摆放是否合理 row -> 行 col -> 列 bool isValid(int row, int col, vector& chessboard, int n) { // 检查列 for (int i = 0; i < row; i++) { if (chessboard[i][col] == 'Q') return false; } // 检查45度角是否有皇后 for (int i = row - 1, j = col - 1; i >=0 && j >= 0; i--, j--) { if (chessboard[i][j] == 'Q') return false; } // 检查135度角是否有皇后 for (int i = row - 1, j = col + 1; i >= 0 && j < n; i--, j++) { if (chessboard[i][j] == 'Q') return false; } return true; } // n -> 输入的棋盘大小 row -> 当前递归到的行数 void backtracking(int n, int row, vector& chessboard) { if (row == n) { // 终止条件 result.push_back(chessboard); return; } for (int col = 0; col < n; col++) { if (isValid(row, col, chessboard, n)) { // 位置合理 -> 放置皇后 chessboard[row][col] = 'Q'; // 放置皇后 backtracking(n, row + 1, chessboard); chessboard[row][col] = '.'; // 回撤,撤销皇后 } } } vector> solveNQueens(int n) { result.clear(); vector chessboard(n, string(n, '.')); // 棋盘初始化 backtracking(n, 0, chessboard); return result; } }; ``` - 解数独错误思路:沿用N皇后代码框架 → 运行结果:直接死循环 ```cpp // row -> 当前递归到的行数 void backtracking(vector>& board, int row) { if (row == board.size()) { // 终止条件 return; } for (int col = 0; col < board[0].size(); col++) { if (board[row][col] == '.') { for (char i = '1'; i <= '9'; i++) { if (isValid(row, col, board, i)) { // 位置合理 board[row][col] = i; // 放置 backtracking(board, row + 1); board[row][col] = '.'; // 回撤 } } } } } ``` - 问题1:N皇后在棋盘上摆放时,每行只摆放1个皇后,但在解数独问题中,每行不止要填1个数字,因此for循环这一层逻辑只能位于回溯函数内部! - 问题2:以上回溯函数的终止条件是参数row遍历至等于board.size(),而某位置可能1~9都无法填入,即回溯可能进行不下去,故无法退出! ```cpp class Solution { public: // 判断(row, col)位置填入某个数是否合理 bool isValid(int row, int col, vector>& board, char val) { // 检查行 for (int j = 0; j < 9; j++) { if (board[row][j] == val) return false; } // 检查列 for (int i = 0; i < 9; i++) { if (board[i][col] == val) return false; } // 检查3 × 3宫内是否合理 for (int i = 3 * (row / 3); i < 3 * (row / 3) + 3; i++) { for (int j = 3 * (col / 3); j < 3 * (col / 3) + 3; j++) { if (board[i][j] == val) return false; } } return true; } // row -> 当前递归到的行数 bool backtracking(vector>& board) { for (int row = 0; row < 9; row++) { for (int col = 0; col < 9; col++) { if (board[row][col] == '.') { for (char i = '1'; i <= '9'; i++) { if (isValid(row, col, board, i)) { // 位置合理 board[row][col] = i; // 放置 if (backtracking(board)) return true; // 找到最终位置 board[row][col] = '.'; // 回撤 } } return false; // 某个位置1~9均无法放置,则返回false } } } return true; } void solveSudoku(vector>& board) { backtracking(board); } }; ``` ## 贪心算法 ### 贪心算法的核心 - 本质是选择每一阶段的局部最优,从而达到全局最优 - 难点 → 通过局部最优,推出整体最优 - 四步骤:划分子问题 → 选择贪心策略 → 求解子问题 → 堆叠全局最优解 ### 无重叠区间 给定一个区间的集合,找到需要移除区间的最小数量,使剩余区间互不重叠! 1. 按左边界从小到大排序 2. 从下标1开始,依次判断第i个区间是否于第i-1区间重叠 3. 若重叠,则需要移除的区间数量加1,且有限保留右区间小的区间(贪心思想:尽可能减小对之后区间的影响) ```cpp class Solution { public: // 按照左边界排序 static bool cmp(const vector& a, const vector& b) { return a[0] < b[0]; } int eraseOverlapIntervals(vector>& intervals) { sort(intervals.begin(), intervals.end(), cmp); int count = 0; // 统计需要移除区间的最小数量 for (int i = 1; i < intervals.size(); i++) { if (intervals[i][0] < intervals[i - 1][1]) { // 重叠 → 需要移除的区间数自增 count++; // 贪心思想:移除右区间数值较大的区间(这样的区间对之后的区间影响更大) intervals[i][1] = min(intervals[i - 1][1], intervals[i][1]); } } return count; } }; ``` ### 跳跃游戏II - 若当前覆盖最远距离下标非终点,则步数加一,仍需继续前进 - 若当前覆盖最远距离下标是重点,则步数无需加一,相当于到达nums[n-1] ```cpp int jump(vector& nums) { if (nums.size() == 1) return 0; int curDistance = 0; // 当前覆盖最远距离下标 int ans = 0; // 记录走的最大步数 int nextDistance = 0; // 下一步覆盖最远距离下标 for (int i = 0; i < nums.size(); i++) { nextDistance = max(nums[i] + i, nextDistance); // 更新下一步覆盖最远距离下标 if (i == curDistance) { // 遇到当前覆盖最远距离下标 ans++; // 需要走下一步 curDistance = nextDistance; // 更新当前覆盖最远距离下标(相当于加油了) if (nextDistance >= nums.size() - 1) break; // 当前覆盖最远距到达集合终点,不用做ans++操作了,直接结束 } } return ans; } ``` - 时间复杂度:O(n) - 空间复杂度:O(1) ### 划分字母区间 - 借助哈希数组以统计每个字母的最远出现位置 - 遍历字符串,依据哈希数组划分字符串 ```cpp class Solution { public: vector partitionLabels(string s) { int hash[27] = {0}; // 哈希数组 -> 用以记录每个字母的最远出现位置 // 统计各字母出现的最远位置 for (int i = 0; i < s.size(); i++) { hash[s[i] - 'a'] = i; } vector result; result.clear(); int left = 0, right = 0; // 遍历字符串,进行划分 for (int i = 0; i < s.size(); i++) { right = max(right, hash[s[i] - 'a']); // 更新当前片段的右区间 if (i == right) { // 找到分割线 result.push_back(right - left + 1); left = right + 1; // 更新下一片段的左区间 } } return result; } }; ``` - 时间复杂度:O(n) - 空间复杂度:O(1) ## 动态规划 ### 动态规划的核心 1. 确定dp数组(dp table)以及下标的含义 2. 确定递推公式 3. dp数组如何初始化 4. 确定遍历顺序 5. 举例推导dp数组 ### 不同的二叉搜索树 ![image.png](./res/img8.png) - n = 3为例,分别考虑头节点为1、2、3时二叉搜索树的数量 - 头节点为1时,左子树0个节点,右子树2个节点 → dp[0] * dp[2] - 头节点为2时,左子树1个节点,右子树1个节点 → dp[1] * dp[1] - 头节点为3时,左子树2个节点,右子树0个节点 → dp[2] * dp[0] ```cpp class Solution { public: int numTrees(int n) { // dp[i]表示由i个节点组成互不相同的二叉搜索树的数量 vector dp(n + 1); dp[0] = 1; for (int i = 1; i <= n; i++) { // i个节点时分别以每个节点为头节点计算二叉搜索树的数量 for (int j = 1; j <= i; j++) { // 以j为头节点时二叉搜索树的个数 // j - 1: 比j小的节点数 -> 左子树 // i - j: 比j大的节点数 -> 右子树 dp[i] += dp[j - 1] * dp[i - j]; } } return dp[n]; } }; ``` - 时间复杂度:O(n ^ 2) - 空间复杂度:O(n) ### 0 - 1背包问题 有n件物品和一个最多能背重量为w 的背包。第i件物品的重量是weight[i],得到的价值是value[i] 。每件物品只能用一次,求解将哪些物品装入背包里物品价值总和最大? ![image.png](./res/img9.png) #### 例题 背包最大重量为4 物品为: ![image.png](./res/img10.png) #### 二维数组解法 1. 确定dp数组(dp table)以及下标的含义 dp[i][j] 表示从下标为[0-i]的物品里任意取,放进容量为j的背包,价值总和最大是多少! ![image.png](./res/img11.png) 2. 确定递推公式 遍历dp[i][j]: - 不放物品i:由dp[i - 1][j]推出,即背包容量为j,里面不放物品i的最大价值,此时dp[i][j]就是dp[i - 1][j] - 放物品i:由dp[i - 1][j - weight[i]] + value[i]推出,dp[i - 1][j - weight[i]]为背包容量为j - weight[i]时不放物品i的最大价值,则dp[i - 1][j - weight[i]] + value[i]就是背包放入物品i得到的最大价值 `dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - weight[i]] + value[i]);` 3. dp数组如何初始化 - 若背包容量j为0的话,即dp[i][0],无论是选取哪些物品,背包价值总和一定为0 - 若只放物品0,即dp[0][j],考虑weight[0]和背包容量 ```cpp // 初始化 dp vector> dp(weight.size(), vector(bagweight + 1, 0)); for (int j = weight[0]; j <= bagweight; j++) { dp[0][j] = value[0]; } ``` ![image.png](./res/img12.png) 4. 确定遍历顺序 - 先遍历物品和先遍历背包都可以! 5. 举例推导dp数组 ![image.png](./res/img13.png) #### 滚动数组解法 - 二维数组解法中递推公式为:`dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - weight[i]] + value[i]);` - 若将dp[i - 1]那一层“拷贝”到dp[i]上,表达式完全可以是:`dp[i][j] = max(dp[i][j], dp[i][j - weight[i]] + value[i]);` 1. 确定dp数组(dp table)以及下标的含义 - dp[j] 表示容量为j的背包能背的最大价值 2. 确定递推公式 - dp[j] = max(dp[j], dp[j - weight[i]] + value[i]); 3. dp数组如何初始化 - dp[0] = 0; 4. 确定遍历顺序 ```cpp // 倒序遍历保证物品i只被放入一次! for(int i = 0; i < weight.size(); i++) { // 遍历物品 for(int j = bagWeight; j >= weight[i]; j--) { // 遍历背包容量 dp[j] = max(dp[j], dp[j - weight[i]] + value[i]); } } ``` 5. 举例推导dp数组 ```cpp #include #include using namespace std; void test_bag_problem() { vector weight = {1, 3, 4}; vector value = {15, 20, 30}; int bagWeight = 4; // 初始化 -> dp[j] 表示容量为j的背包能背的最大价值 vector dp(bagWeight + 1, 0); for(int i = 0; i < weight.size(); i++) { // 遍历物品 for(int j = bagWeight; j >= weight[i]; j--) { // 遍历背包容量 dp[j] = max(dp[j], dp[j - weight[i]] + value[i]); } } cout << dp[bagWeight] << endl; } int main() { test_bag_problem(); } ``` ![image.png](./res/img14.png) #### 细节问题 1. 二维数组实现递推逻辑(两层循环顺序可颠倒) ```cpp // weight数组的大小就是物品个数 for (int i = 1; i < weight.size(); i++) { // 遍历物品 for (int j = 0; j <= bagweight; j++) { // 遍历背包 if (j < weight[i]) dp[i][j] = dp[i - 1][j]; else dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - weight[i]] + value[i]); } } // weight数组的大小就是物品个数 for (int j = 0; j <= bagweight; j++) { // 遍历背包 for (int i = 0; i < weight.size(); i++) { // 遍历物品 if (j < weight[i]) dp[i][j] = dp[i - 1][j]; else dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - weight[i]] + value[i]); } } ``` 2. 滚动数组实现递推逻辑时为什么倒序遍历? ```cpp // weight = {1, 3, 4}; value = {15, 20, 30}; bagweight = 2; // 正序遍历 dp[0] = 0; for(int i = 0; i < weight.size(); i++) { // 遍历物品 for(int j = weight[i]; j <= bagweight; j++) { // 遍历背包容量 dp[j] = max(dp[j], dp[j - weight[i]] + value[i]); } } // dp[1] = dp[1 - weight[0]] + value[0] = 15; // dp[2] = dp[2 - weight[0]] + value[0] = 30; // 意味着物品0被放入了两次,故无法正序遍历 // 倒序遍历 dp[0] = 0; for(int i = 0; i < weight.size(); i++) { // 遍历物品 for(int j = bagWeight; j >= weight[i]; j--) { // 遍历背包容量 dp[j] = max(dp[j], dp[j - weight[i]] + value[i]); } } // dp[2] = dp[2 - weight[0]] + value[0] = 15; (dp数组初始化为0) // dp[1] = dp[1 - weight[0]] + value[0] = 15; // 从后往前循环,每次取得状态不会和之前取得状态重合,保证物品i只被放一次! ``` 3. 二维数组两层for循环是否可以倒序遍历? 否,滚动数组倒序遍历的本质是遍历二维数组,并且右下角的值依赖上一层左上角的值,因此需要保证左边的值仍是上一层的,从右向左覆盖 #### 目标和 - 0 - 1背包原始模型 → “装满背包的最大价值是多少?” - 目标和 → “给定背包容量,有多少种方式将背包装满?” ```cpp class Solution { public: int findTargetSumWays(vector& nums, int target) { // 数组元素和为sum,添加"+"的元素之和为left,添加"-"的元素之和为right // left = sum - right, right - (sum - right) = target // right = (sum + target) / 2 int sum = accumulate(nums.begin(), nums.end(), 0); if ((sum + target) % 2 == 1 || abs(target) > sum) return 0; int bagweight = (sum + target) / 2; // dp[i][j]:在数组nums的前i个数中选取元素,使元素之和等于j的方案数 vector> dp(nums.size() + 1, vector(bagweight + 1, 0)); dp[0][0] = 1; for (int i = 1; i <= nums.size(); i++) { // 遍历物品 for (int j = 0; j <= bagweight; j++) { if (j < nums[i - 1]) // 元素之和 < nums[i] dp[i][j] = dp[i - 1][j]; else dp[i][j] = dp[i - 1][j] + dp[i - 1][j - nums[i - 1]]; } } return dp[nums.size()][bagweight]; } }; ``` - 时间复杂度:O(n × m) - 空间复杂度:O(n × m) #### 一和零 - 给定二进制字符串数组strs和两个整数m和n,返回strs的最大子集的长度,该子集中最多有m个0和n个1 - 输入:strs = ["10", "0001", "111001", "1", "0"], m = 5, n = 3,输出:4 - m和n相当于是一个背包,两个维度的背包 - 使用二维滚动数组 ```cpp class Solution { public: int findMaxForm(vector& strs, int m, int n) { // 两个维度的0 - 1背包:m个0,n个1 // dp[i][j]是指最多有m个0和n个1的最大子集大小 vector> dp(m + 1, vector (n + 1, 0)); // 默认初始化0 for (string str : strs) { // 遍历物品 int oneNum = 0, zeroNum = 0; // 相当于是物品的重量(重量0和重量1) for (char c : str) { if (c == '0') zeroNum++; else oneNum++; } // 遍历背包容量 -> 从后往前遍历 for (int i = m; i >= zeroNum; i--) { for (int j = n; j >= oneNum; j--) { dp[i][j] = max(dp[i][j], dp[i - zeroNum][j - oneNum] + 1); } } } return dp[m][n]; } }; ``` #### 完全背包问题 - 完全背包与0 - 1背包问题唯一不同的地方是每件物品有无限件! - 完全背包的物品可以添加很多次,因此用滚动数组时要从小到大遍历!(0 - 1背包从大到小遍历是为了保证每个物品仅被添加一次) - 完全背包问题中先遍历背包容量和先遍历物品均可! ```cpp // weight - 物品重量,value - 物品价值 bagweight - 背包容量 // 先物品再背包容量 for (int i = 0; i < weight.size(); i++) { for (int j = weight[i]; j <= bagweight; j++) { dp[j] = max(dp[j], dp[j - weight[i]] + value[i]); } } // 先背包容量再物品 for (int j = 0; j <= bagweight; j++) { for (int i = 0; i < weight.size(); i++) { if (j - weight[i] >= 0) dp[j] = max(dp[j], dp[j - weight[i]] + value[i]); } } ``` #### 零钱兑换II - 给定不同面额的硬币和一个总金额。写出函数来计算可以凑成总金额的硬币组合数。假设每一种面额的硬币有无限个。 - 计算组合数 → 外层物品,内层背包容量! - 计算排列数 → 外层背包容量,内层物品! ```cpp class Solution { public: int change(int amount, vector& coins) { // dp[j]:装满容量为j的背包的组合数 vector dp(amount + 1, 0); dp[0] = 1; for (int i = 0; i < coins.size(); i++) { // 遍历硬币 for (int j = coins[i]; j <= amount; j++) { // 遍历背包容量 dp[j] += dp[j - coins[i]]; } } return dp[amount]; } }; ``` - 时间复杂度:O(mn),其中m是amount,n是coins的长度 - 空间复杂度:O(m) #### 多重背包问题 ![image.png](./res/img15.png) ### 打家劫舍 III - 小偷发现只有一个入口的可行窃地区,称为root,除了root外,每栋房子有且只有一个“父”房子与之相连。(若两个直接相连的房子在同一天晚上被打劫,房屋将会自动报警) - 返回在不触动警报的前提下,小偷行窃能获取的最高金额 - 输入:[3, 2, 3, null, 3, null, 1] - 输出:7(3 + 3 + 1) - dp[2]:dp[0]:不偷该节点所得到的最大金钱,dp[1]偷该节点所得到的最大金钱 ```cpp class Solution { public: int rob(TreeNode* root) { vector result = robTree(root); return max(result[0], result[1]); } // 长度为2的数组,0:不偷,1:偷 vector robTree(TreeNode* cur) { if (cur == nullptr) return vector{0, 0}; vector left = robTree(cur->left); vector right = robTree(cur->right); // 偷cur,则不能偷左右节点 int val1 = cur->val + left[0] + right[0]; // 不偷cur,则可以选择偷左右节点 int val2 = max(left[0], left[1]) + max(right[0], right[1]); return {val2, val1}; } }; ``` - 时间复杂度:O(n) - 空间复杂度(算上递推系统栈的空间):O(log n) ### 买卖股票的最佳时机 III - 给定一个数组,它的第i个元素是一支给定的股票在第i天的价格,设计一个算法计算所能获得的最大利润,最多可以完成两笔交易(不能同时参加多笔交易) - 输入:[3, 3, 5, 0, 0, 3, 1, 4] - 输出:6(第3天购入,第5天卖出;第6天购入,第7天卖出) ```cpp class Solution { public: int maxProfit(vector& prices) { /* 0 - 没有操作 (其实我们也可以不设置这个状态) 1 - 第一次持有股票 2 - 第一次不持有股票 3 - 第二次持有股票 4 - 第二次不持有股票 */ // dp[i][1]:第i天,买入股票的状态(并不一定是一定要第i天买入股票),此时可获的最大利润 vector> dp(prices.size(), vector(5, 0)); dp[0][1] = -prices[0]; // 相当于第0天时第一次持有股票,初始化为-prices[0] dp[0][3] = -prices[0]; // 同 for (int i = 1; i < prices.size(); i++) { // dp[i][1]: (1)第i天购入股票了,则dp[i][1] = dp[i - 1][0] - prices[i]; (2)第i天未购股票 dp[i][1] = max(dp[i - 1][1], dp[i - 1][0] - prices[i]); // dp[i][2]: (1)第i天卖出股票了,则dp[i][2] = dp[i - 1][1] + prices[i]; (2)第i天未卖股票 dp[i][2] = max(dp[i - 1][2], dp[i - 1][1] + prices[i]); dp[i][3] = max(dp[i - 1][3], dp[i - 1][2] - prices[i]); dp[i][4] = max(dp[i - 1][4], dp[i - 1][3] + prices[i]); } return dp[prices.size() - 1][4]; } }; ``` - 时间复杂度:O(n) - 空间复杂度:O(n × 5) ### 买卖股票的最佳时机含冷冻期 ![image.png](./res/img16.png) ```cpp class Solution { public: int maxProfit(vector& prices) { /* j下标0 - 持有股票状态 j下标1 - 保持卖出股票状态(但非冷冻期内) j下标2 - 今天卖出股票 j下标3 - 今天为冷冻期 */ // 第i天状态为j,所剩的最多现金为dp[i][j] vector> dp(prices.size(), vector(4, 0)); dp[0][0] = -prices[0]; for (int i = 1; i < prices.size(); ++i) { // dp[i][0]:(1)保持购入的状态; (2)前一天为冷冻期或卖出股票状态,更新为购入状态 dp[i][0] = max(dp[i - 1][0], max(dp[i - 1][3], dp[i - 1][1]) - prices[i]); // dp[i][1]:(1)前一天为卖出股票状态; (2)前一天为冷冻状态 dp[i][1] = max(dp[i - 1][1], dp[i - 1][3]); // dp[i][2]:前一天必为持股状态 dp[i][2] = dp[i - 1][0] + prices[i]; // dp[i][3]:前一天卖出股票 - 状态2 dp[i][3] = dp[i - 1][2]; } return max(dp[prices.size() - 1][3], max(dp[prices.size() - 1][1], dp[prices.size() - 1][2])); } }; ``` - 时间复杂度:O(n) - 空间复杂度:O(n) ### 最长重复子数组 - 给两个整数数组nums1和nums2,返回两个数组中公共的,长度最长的子数组的长度。 ```cpp int findLength(vector& nums1, vector& nums2) { int result = 0; // dp[i][j] :以下标i - 1为结尾的A,和以下标j - 1为结尾的B,最长重复子数组长度 vector> dp(nums1.size() + 1, vector(nums2.size() + 1, 0)); for (int i = 1; i <= nums1.size(); ++i) { for (int j = 1; j <= nums2.size(); ++j) { if (nums1[i - 1] == nums2[j - 1]) dp[i][j] = dp[i - 1][j - 1] + 1; if (dp[i][j] > result) // 若出现长度更长的公共数组,则更新 result = dp[i][j]; } } return result; } ``` - 时间复杂度:O(n × m) - 空间复杂度:O(n × m) ### 最长公共子序列 ![image.png](./res/img17.png) ```cpp int longestCommonSubsequence(string text1, string text2) { // dp[i][j]:长度为[0, i - 1]的字符串text1与长度为[0, j - 1]的字符串text2的最长公共子序列 vector> dp(text1.size() + 1, vector(text2.size() + 1, 0)); for (int i = 1; i <= text1.size(); ++i) { for (int j = 1; j <= text2.size(); ++j) { if (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]); // 取较大值! } } return dp[text1.size()][text2.size()]; } ``` - 时间复杂度:O(n × m) - 空间复杂度:O(n × m) ### 回文子串 ![image.png](./res/img18.png) - 考虑dp数组定义 - 考虑遍历顺序 ### 完全平方数 - 给定一个整数n ,返回和为n的完全平方数的最少数量 ```cpp class Solution { public: int numSquares(int n) { // dp[j]:和为n的完全平方数的最少数量 vector dp(n + 1, INT_MAX - 1); dp[0] = 0; for (int i = 1; i <= n; i++) { // 1 - n for (int j = i * i; j <= n; j++) { // i * i dp[j] = min(dp[j], dp[j - i * i] + 1); } } return dp[n]; } }; ``` ### 单词拆分 ![image.png](./res/img19.png) ```cpp class Solution { public: bool wordBreak(string s, vector& wordDict) { // dp[j]:字符串长度为j的话,dp[j]为true,表示可以拆分为一个或多个在字典中出现的单词。 vector dp(s.size() + 1, false); dp[0] = true; unordered_set wordSet(wordDict.begin(), wordDict.end()); for (int j = 1; j <= s.size(); ++j) { // 遍历背包 for (int i = 0; i < j; ++i) { // 遍历物品 string word = s.substr(i, j - i); //substr(起始位置,截取的个数) if (wordSet.find(word) != wordSet.end() && dp[i]) dp[j] = true; } } return dp[s.size()]; } }; ``` - 时间复杂度:O(n ^ 3) - 空间复杂度:O(n) ### 编辑距离 ![image.png](./res/img20.png) - 遵循动规模板即可 ```cpp class Solution { public: int minDistance(string word1, string word2) { // dp[i][j]: 以下标i-1为结尾的字符串word1,和以下标j-1为结尾的字符串word2的最近编辑距离 vector> dp(word1.size() + 1, vector(word2.size() + 1, 0)); for (int i = 0; i <= word1.size(); i++) dp[i][0] = i; for (int j = 0; j <= word2.size(); j++) dp[0][j] = j; for (int i = 1; i <= word1.size(); ++i) { for (int j = 1; j <= word2.size(); ++j) { if (word1[i - 1] == word2[j - 1]) dp[i][j] = dp[i - 1][j - 1]; else dp[i][j] = min({dp[i - 1][j - 1], dp[i - 1][j], dp[i][j - 1]}) + 1; // 增、删、改 } } return dp[word1.size()][word2.size()]; } }; ``` - 时间复杂度:O(n * m) - 空间复杂度:O(n * m)