diff --git "a/topic03/submit/LC0918._\351\273\204\346\263\275\351\272\237cpp" "b/topic03/submit/LC0918_\351\273\204\346\263\275\351\272\237.cpp" similarity index 100% rename from "topic03/submit/LC0918._\351\273\204\346\263\275\351\272\237cpp" rename to "topic03/submit/LC0918_\351\273\204\346\263\275\351\272\237.cpp" diff --git "a/topic05/submit/LC0070_\351\273\204\346\263\275\351\272\237.cpp" "b/topic05/submit/LC0070_\351\273\204\346\263\275\351\272\237.cpp" new file mode 100644 index 0000000000000000000000000000000000000000..ca4e566a516d171e2b43d54b70dcf0c664c58c43 --- /dev/null +++ "b/topic05/submit/LC0070_\351\273\204\346\263\275\351\272\237.cpp" @@ -0,0 +1,81 @@ +#include +using namespace std; + +// 【题目】力扣70. 爬楼梯 +// 【难度】简单 +// 【提交】2025.10.21 https://leetcode.cn/problems/climbing-stairs/submissions/672439203/ +// 【标签】动态规划;数学 +class Solution { +public: + int climbStairs(int n) { + if(n == 1) return 1; + if(n == 2) return 2; + int a = 1; + int b = 2; + for(int i = 3; i < n + 1; ++i) { + int c = a + b; + a = b; + b = c; + } + return b; + } +}; + +/** + * @brief 学习总结: + * 一、题意与模型 + * 假设你正在爬楼梯,需要n阶才能到达楼顶。 + * 每次可以爬1或2个台阶,问有多少种不同的方法可以爬到楼顶。 + * 模型:动态规划(斐波那契数列),通过状态转移方程求解。 + * + * 二、标准解法状态设计 + * 设dp[i]表示到达第i阶楼梯的方法数,则有: + * dp[i] = dp[i-1] + dp[i-2] + * 初始状态:dp[1] = 1, dp[2] = 2 + * 由于只需要前两个状态,可以使用滚动数组优化空间复杂度。 + * + * 三、你的实现思路 + * 使用动态规划思想,通过滚动数组优化空间复杂度。 + * 只保存前两个状态a和b,在循环中不断更新。 + * + * 四、逐行注释(带细节提醒) + * if(n == 1) return 1; // 只有1阶,只有1种方法 + * if(n == 2) return 2; // 有2阶,有2种方法(1+1或2) + * + * int a = 1; // 初始化:到达第1阶的方法数 + * int b = 2; // 初始化:到达第2阶的方法数 + * + * for(int i = 3; i < n + 1; ++i) { // 从第3阶计算到第n阶 + * int c = a + b; // 当前阶的方法数等于前两阶方法数之和 + * a = b; // 更新a为前一阶的方法数 + * b = c; // 更新b为当前阶的方法数 + * } + * return b; // 返回到达第n阶的方法数 + * + * 五、正确性证明 + * 算法基于斐波那契数列的性质: + * 到达第i阶楼梯的方法数 = 到达第i-1阶的方法数(再爬1阶)+ 到达第i-2阶的方法数(再爬2阶) + * 通过数学归纳法可以证明该递推关系的正确性。 + * + * 六、复杂度 + * 时间:O(n),需要循环n-2次。 + * 空间:O(1),只使用了常数级别的额外空间。 + * + * 七、优缺点分析 + * 优点: + * - 时间复杂度低,只需线性时间; + * - 空间复杂度低,使用滚动数组优化; + * - 代码简洁,实现高效。 + * 缺点: + * - 对于非常大的n,可能存在整数溢出问题; + * - 无法直接获取所有可能的爬楼梯方案。 + * + * 八、改进建议 + * 1. 可以添加输入验证:if (n <= 0) return 0; + * 2. 可以使用矩阵快速幂将时间复杂度优化到O(log n); + * 3. 对于教学场景,可以添加注释解释状态转移方程的推导过程。 + * + * 九、一句话总结 + * 通过动态规划和滚动数组优化,你的实现高效地解决了爬楼梯问题, + * 展现了基础动态规划问题的经典解法。 + */ \ No newline at end of file diff --git "a/topic05/submit/LC0198_\351\273\204\346\263\275\351\272\237.cpp" "b/topic05/submit/LC0198_\351\273\204\346\263\275\351\272\237.cpp" new file mode 100644 index 0000000000000000000000000000000000000000..d762dd7a3be970f829f7aa338fa32cbcfc7900f2 --- /dev/null +++ "b/topic05/submit/LC0198_\351\273\204\346\263\275\351\272\237.cpp" @@ -0,0 +1,56 @@ +#include +using namespace std; + +// 【题目】力扣198. 打家劫舍 +// 【难度】中等 +// 【提交】2025.10.21 https://leetcode.cn/problems/house-robber/submissions/672449056/ +// 【标签】动态规划 +class Solution_LC0198 { +public: + int rob(vector& nums) { + vector dp(nums.size() + 1, 0); + dp[0] = 0; + dp[1] = nums[0]; + for(int i = 2; i < nums.size() + 1; ++i) { + dp[i] = max(dp[i - 1], dp[i - 2] + nums[i - 1]); + } + return dp[nums.size()]; + } +}; + +/** + * @brief 学习总结: + * 一、题意与模型 + * 给定一个整数数组nums,表示每个房屋存放的金额。如果两间相邻的房屋在同一晚上被偷,系统会自动报警。 + * 计算在不触动警报装置的情况下,能够偷窃到的最高金额。 + * 模型:动态规划,通过状态转移计算最大收益。 + * + * 二、标准解法状态设计 + * 设dp[i]表示前i个房屋能偷窃到的最高金额。 + * 状态转移方程:dp[i] = max(dp[i-1], dp[i-2] + nums[i-1]) + * 初始状态:dp[0] = 0, dp[1] = nums[0] + * + * 三、你的实现思路 + * 使用动态规划方法,通过dp数组记录前i个房屋的最大收益。 + * 对于每个房屋,选择偷或不偷,取两者中的最大值。 + * + * 四、复杂度 + * 时间:O(n),需要遍历一次数组。 + * 空间:O(n),需要dp数组存储中间结果。 + * + * 五、优缺点分析 + * 优点: + * - 算法思路清晰,易于理解; + * - 时间复杂度低,能够处理较大规模数据。 + * 缺点: + * - 空间复杂度可以进一步优化; + * - 对于边界情况需要特殊处理。 + * + * 六、改进建议 + * 1. 可以优化空间复杂度到O(1),只保存前两个状态; + * 2. 可以添加输入验证:if (nums.empty()) return 0; + * 3. 对于只有一个房屋的情况可以直接返回。 + * + * 七、一句话总结 + * 动态规划是解决打家劫舍问题的标准方法,你的实现准确且高效。 + */ \ No newline at end of file diff --git "a/topic05/submit/LC0300_\351\273\204\346\263\275\351\272\237.cpp" "b/topic05/submit/LC0300_\351\273\204\346\263\275\351\272\237.cpp" new file mode 100644 index 0000000000000000000000000000000000000000..67a314284c2464d18b2b19dec9520e2a3c2b3400 --- /dev/null +++ "b/topic05/submit/LC0300_\351\273\204\346\263\275\351\272\237.cpp" @@ -0,0 +1,59 @@ +#include +using namespace std; + +// 【题目】力扣300. 最长递增子序列 +// 【难度】中等 +// 【提交】2025.10.21 https://leetcode.cn/problems/longest-increasing-subsequence/submissions/ +// 【标签】动态规划;二分查找 +class Solution_LC0300 { +public: + int lengthOfLIS(vector& nums) { + int n = nums.size(); + vector dp; + int ans = 1; + for(auto i : nums) { + auto it = ranges::lower_bound(dp, i); + if(it == dp.end()) dp.push_back(i); + else *it = i; + } + return dp.size(); + } +}; + +/** + * @brief 学习总结: + * 一、题意与模型 + * 给定一个整数数组nums,找到其中最长严格递增子序列的长度。 + * 模型:动态规划+二分查找,通过维护一个递增序列来优化求解过程。 + * + * 二、标准解法状态设计 + * 使用贪心+二分查找的方法: + * 1. 维护一个dp数组,其中dp[i]表示长度为i+1的递增子序列的末尾元素的最小值 + * 2. 遍历数组,对于每个元素,使用二分查找在dp数组中找到第一个大于等于该元素的位置 + * 3. 如果找到,替换该位置的元素;如果没找到,将元素添加到dp数组末尾 + * 4. 最终dp数组的长度即为最长递增子序列的长度 + * + * 三、你的实现思路 + * 使用C++20的ranges::lower_bound进行二分查找,维护一个递增序列。 + * 通过不断更新序列中的元素,保证序列长度的最大化。 + * + * 四、复杂度 + * 时间:O(n log n),每个元素需要O(log n)时间进行二分查找。 + * 空间:O(n),需要dp数组存储中间结果。 + * + * 五、优缺点分析 + * 优点: + * - 时间复杂度低,比O(n²)的DP解法更高效; + * - 代码简洁,实现优雅。 + * 缺点: + * - 无法直接获取最长递增子序列的具体内容; + * - 需要C++20支持(ranges::lower_bound)。 + * + * 六、改进建议 + * 1. 可以使用标准库的lower_bound替代ranges::lower_bound以提高兼容性; + * 2. 可以添加输入验证:if (nums.empty()) return 0; + * 3. 可以扩展算法以记录具体的最长递增子序列。 + * + * 七、一句话总结 + * 贪心+二分查找是解决最长递增子序列问题的高效方法,你的实现简洁且高效。 + */ \ No newline at end of file diff --git "a/topic05/submit/LC0343_\351\273\204\346\263\275\351\272\237.cpp" "b/topic05/submit/LC0343_\351\273\204\346\263\275\351\272\237.cpp" new file mode 100644 index 0000000000000000000000000000000000000000..fd4d54a386508c94c38be2602fd39dbc3808d92b --- /dev/null +++ "b/topic05/submit/LC0343_\351\273\204\346\263\275\351\272\237.cpp" @@ -0,0 +1,74 @@ +#include +using namespace std; + +// 【题目】力扣343. 整数拆分 +// 【难度】中等 +// 【提交】2025.10.21 https://leetcode.cn/problems/integer-break/submissions/672444548/ +// 【提交】2025.10.21 https://leetcode.cn/problems/integer-break/submissions/672440751/ +// 【标签】数学;动态规划 +class Solution_LC0343 { +public: + int integerBreak(int n) { + if(n == 2) return 1; + if(n == 3) return 2; + int res = 1; + while(n > 4) { + res *= 3; + n -= 3; + } + res *= n; + return res; + } + int integerBreak(int n) { + vector dp(n + 1); + dp[2] = 1; + for(int i = 3; i <= n; ++i) { + for(int j = 1; j < i - 1; ++j) { + dp[i] = max(dp[i], max((i - j) * j, dp[i - j] * j)); + } + } + return dp[n]; + } +}; + +/** + * @brief 学习总结: + * 一、题意与模型 + * 给定一个正整数n,将其拆分为至少两个正整数的和,并使这些整数的乘积最大化。 + * 返回可以获得的最大乘积。 + * + * 二、两种解法对比 + * + * 解法1:数学解法(贪心) + * 核心思想:尽可能多地拆分出3,当剩余4时拆分为2+2 + * 时间复杂度:O(n) + * 空间复杂度:O(1) + * 优点:效率高,代码简洁 + * 缺点:需要数学证明,理解难度较大 + * + * 解法2:动态规划 + * 状态定义:dp[i]表示将整数i拆分后得到的最大乘积 + * 状态转移:dp[i] = max(dp[i], max((i-j)*j, dp[i-j]*j)) + * 时间复杂度:O(n²) + * 空间复杂度:O(n) + * 优点:思路直观,易于理解 + * 缺点:效率较低,需要额外空间 + * + * 三、数学原理证明 + * 1. 任何大于4的数都可以拆分成3和更小的数 + * 2. 当n=2时,最大乘积为1(1×1) + * 3. 当n=3时,最大乘积为2(1×2) + * 4. 当n=4时,拆分为2+2(乘积4)优于3+1(乘积3) + * 5. 当n>4时,拆出3总是比拆出2能得到更大的乘积 + * + * 四、复杂度分析 + * 数学解法:时间O(n),空间O(1) + * 动态规划:时间O(n²),空间O(n) + * + * 五、应用场景 + * 数学解法:适合对性能要求高的场景 + * 动态规划:适合教学和理解问题本质 + * + * 六、一句话总结 + * 数学解法高效但需要证明,动态规划直观但效率较低,根据场景选择合适的解法。 + */ \ No newline at end of file diff --git "a/topic05/submit/LC0377_\351\273\204\346\263\275\351\272\237.cpp" "b/topic05/submit/LC0377_\351\273\204\346\263\275\351\272\237.cpp" new file mode 100644 index 0000000000000000000000000000000000000000..38523982b8c98be50e477495e51964b26c53f95f --- /dev/null +++ "b/topic05/submit/LC0377_\351\273\204\346\263\275\351\272\237.cpp" @@ -0,0 +1,50 @@ +#include +using namespace std; + +// 【题目】力扣377. 组合总和 Ⅳ +// 【难度】中等 +// 【提交】2025.10.23 https://leetcode.cn/problems/combination-sum-iv/submissions/673018929/ +// 【标签】动态规划;记忆化搜索 +class Solution_LC0377 { +public: + int combinationSum4(vector& nums, int target) { + vector memo(target + 1, -1); + auto dfs = [&](this auto&& dfs, int i) { + if (i == 0) return 1; + int &res = memo[i]; + if (res != -1) return res; + res = 0; + for (int x : nums) if (x <= i) res += dfs(i - x); + return res; + }; + return dfs(target); + } +}; + +/** + * @brief 学习总结: + * 本题使用记忆化递归(DFS+Memo)实现,思路清晰直观。 + * 优点: + * - 记忆化搜索避免了重复计算,时间复杂度为O(target * n) + * - 递归思路自然,符合组合问题的数学定义 + * + * 缺点: + * - 递归深度可能较大,存在栈溢出风险 + * - 相比迭代DP,常数开销稍大 + * + * 改进建议: + * 可改用迭代DP写法,避免递归开销: + * + * int combinationSum4(vector& nums, int target) { + * vector dp(target + 1, 0); + * dp[0] = 1; + * for (int i = 1; i <= target; ++i) { + * for (int x : nums) { + * if (i >= x) dp[i] += dp[i - x]; + * } + * } + * return dp[target]; + * } + * + * 注意:使用unsigned long long防止整数溢出 + */ \ No newline at end of file diff --git "a/topic05/submit/LC0746_\351\273\204\346\263\275\351\272\237.cpp" "b/topic05/submit/LC0746_\351\273\204\346\263\275\351\272\237.cpp" new file mode 100644 index 0000000000000000000000000000000000000000..ef3861c377053717543de9ec2723be182b5f66cb --- /dev/null +++ "b/topic05/submit/LC0746_\351\273\204\346\263\275\351\272\237.cpp" @@ -0,0 +1,57 @@ +#include +using namespace std; + +// 【题目】力扣746. 使用最小花费爬楼梯 +// 【难度】简单 +// 【提交】2025.10.23 https://leetcode.cn/problems/min-cost-climbing-stairs/submissions/673016658/ +// 【标签】动态规划 +class Solution_LC0746 { +public: + int minCostClimbingStairs(vector& cost) { + int n = cost.size(); + vector dp(n + 1); + dp[0] = dp[1] = 0; + for(int i = 2; i <= n; ++i) { + dp[i] = min(dp[i - 1] + cost[i - 1], dp[i - 2] + cost[i - 2]); + } + return dp[n]; + } +}; + +/** + * @brief 学习总结: + * 一、题意与模型 + * 给定一个整数数组cost,其中cost[i]是从楼梯第i个台阶向上爬需要支付的费用。 + * 一旦支付此费用,即可选择向上爬一个或者两个台阶。可以从下标为0或1的台阶开始爬楼梯。 + * 计算并返回达到楼梯顶部的最低花费。 + * 模型:动态规划,通过状态转移计算最小花费。 + * + * 二、标准解法状态设计 + * 设dp[i]表示到达第i个台阶的最小花费。 + * 状态转移方程:dp[i] = min(dp[i-1] + cost[i-1], dp[i-2] + cost[i-2]) + * 初始状态:dp[0] = 0, dp[1] = 0 + * + * 三、你的实现思路 + * 使用动态规划方法,通过dp数组记录到达每个台阶的最小花费。 + * 对于每个台阶,选择从i-1台阶爬1步或从i-2台阶爬2步,取两者中的最小值。 + * + * 四、复杂度 + * 时间:O(n),需要遍历一次数组。 + * 空间:O(n),需要dp数组存储中间结果。 + * + * 五、优缺点分析 + * 优点: + * - 算法思路清晰,易于理解; + * - 时间复杂度低,能够处理较大规模数据。 + * 缺点: + * - 空间复杂度可以进一步优化; + * - 对于边界情况需要特殊处理。 + * + * 六、改进建议 + * 1. 可以优化空间复杂度到O(1),只保存前两个状态; + * 2. 可以添加输入验证:if (cost.empty()) return 0; + * 3. 对于只有两个台阶的情况可以直接返回min(cost[0], cost[1])。 + * + * 七、一句话总结 + * 动态规划是解决最小花费爬楼梯问题的标准方法,你的实现准确且高效。 + */ \ No newline at end of file diff --git "a/topic05/submit/LC2466_\351\273\204\346\263\275\351\272\237.cpp" "b/topic05/submit/LC2466_\351\273\204\346\263\275\351\272\237.cpp" new file mode 100644 index 0000000000000000000000000000000000000000..8b072a6a9bc88f7f749e56a5acaa5235a48b5073 --- /dev/null +++ "b/topic05/submit/LC2466_\351\273\204\346\263\275\351\272\237.cpp" @@ -0,0 +1,61 @@ +#include +using namespace std; + +// 【题目】力扣2466. 统计构造好字符串的方案数 +// 【难度】中等 +// 【提交】2025.10.23 https://leetcode.cn/problems/count-ways-to-build-good-strings/submissions/673029080/ +// 【标签】动态规划 +class Solution { +public: + int countGoodStrings(int low, int high, int zero, int one) { + const int MOD = 1'000'000'007; + int ans = 0; + vector f(high + 1); + f[0] = 1; + for (int i = 1; i <= high; i++) { + if (i >= zero) f[i] = f[i - zero]; + if (i >= one) f[i] = (f[i] + f[i - one]) % MOD; + if (i >= low) ans = (ans + f[i]) % MOD; + } + return ans; + } +}; + +/** + * @brief 学习总结: + * 一、题意与模型 + * 给定整数low, high, zero, one,使用0和1构造字符串,每次可以添加zero个0或one个1。 + * 统计所有长度在[low, high]范围内且满足条件的字符串数目。 + * 模型:动态规划,计算构造特定长度字符串的方案数。 + * + * 二、标准解法状态设计 + * 设f[i]表示构造长度为i的字符串的方案数。 + * 状态转移方程:f[i] = f[i-zero] + f[i-one] + * 初始状态:f[0] = 1(空字符串) + * 在计算过程中累加长度在[low, high]范围内的方案数 + * + * 三、你的实现思路 + * 使用动态规划方法,通过f数组记录构造每个长度字符串的方案数。 + * 在循环中同时计算方案数和累加结果,减少一次额外循环。 + * + * 四、复杂度 + * 时间:O(high),需要计算从1到high的每个长度。 + * 空间:O(high),需要f数组存储中间结果。 + * + * 五、优缺点分析 + * 优点: + * - 算法高效,只需一次循环; + * - 在计算过程中直接累加结果,减少额外循环; + * - 使用模运算防止整数溢出。 + * 缺点: + * - 空间复杂度为O(high),对于较大的high值可能占用较多内存; + * - 无法直接获取每种长度的具体方案。 + * + * 六、改进建议 + * 1. 可以优化空间复杂度到O(max(zero, one)),使用滚动数组; + * 2. 可以添加输入验证,确保参数有效性; + * 3. 对于极大的high值,可以考虑数学优化方法。 + * + * 七、一句话总结 + * 动态规划是解决字符串构造方案数问题的标准方法,你的实现高效且优化良好。 + */ \ No newline at end of file