“动态规划(Dynamic Programming, DP)在查找有很多重叠子问题的情况的最优解时有效。它将问题重新组合成子问题。为了避免多次解决这些子问题,它们的结果都逐渐被计算并被保存,从简单的问题直到整个问题都被解决。因此,动态规划保存递归时的结果,因而不会在解决同样的问题时花费时间 · · · · · · 动态规划只能应用于有最优子结构的问题。最优子结构的意思是局部最优解能决定全局最优解(对有些问题这个要求并不能完全满足,故有时需要引入一定的近似)。简单地说,问题能够分解成子问题来解决。”
动态规划保存子问题的解,避免重复计算。解决动态规 划问题的关键是找到状态转移方程,这样我们可以通过计算和储存子问题的解来求解最终问题。
按摩师
题解
dp0:表示第i天不接
dp1:表示第i天接
状态转移方程:
第i天接的话,那么此时dp[i] = dp0+nums[i];
第i天不接的话,前面dp[i-1]接或不接的最大值,dp0 = max(dp0, dp1)
class Solution { public: int massage(vector<int>& nums) { int n= nums.size(); // 数组为0时的解 if(!n) return 0; // 数组长度为1时的解 int dp0 = 0,dp1=nums[0]; // dp0表示第一个不接,dp1表示第一个接 // 数组长度大于1时的解 for(int i = 1; i < n; ++i){ int tdp0 = max(dp0, dp1); // 第i个不接,此时tdp0等于前面dp[i-1]接或不接的最大值 int tdp1 = dp0 + nums[i]; // 第i个接,那么tdp1等于前面dp[i-1]不接的值加上接的这个数值 dp0 = tdp0; dp1 = tdp1; } return max(dp0, dp1); } };
爬楼梯
题解
状态转移方程
第i级阶梯可以从离2格阶梯上来和离一格阶梯上来之和求得
dp[i] = dp[i-1]+dp[i-2];
若要压缩内存的话,由于其只跟 dp[i-1]和dp[i-2]最后两个状态有关,可以用滚动的方式压缩内存,只使用三个变量first,second和sum
class Solution { public: int climbStairs(int n) { if(!n) return 0; int dp[n+1]; dp[0] = 1; dp[1] = 1; for(int i=2;i<n+1;++i) { dp[i] = dp[i-1] + dp[i-2]; } return dp[n]; // 压缩内存 int first = 1, second = 1; // 表示只有0级阶梯和一级阶梯的初始状态 for(int i=2;i<n;i++) { } };
可爱串
要解答这个问题,需要计算出长度为 \( n \) 的仅由字母 'r'、'e' 和 'd' 构成的字符串中,有多少是“可爱串”,即包含子序列 "red",但不包含子串 "red"。并且答案需要对 \( 10^9 + 7 \) 取模。
首先,注意区分**子序列**和**子串**的概念:
-
子序列可以不连续,必须包含 "r","e","d" 按顺序出现,但中间可以夹杂其他字符。
-
子串必须连续,必须严格连续包含 "red" 才算子串。
分析步骤
-
总共的“red”子序列数:先求出所有包含 "red" 子序列的字符串个数。这个问题可以通过动态规划来计算。
-
排除包含 "red" 子串的情况:为了计算不包含 "red" 子串的字符串数,需要从所有包含子序列 "red" 的字符串中减去那些包含子串 "red" 的情况。
-
模运算:由于 n 的范围较大(最多可达 \( 10^5 \)),因此需要在每一步都对结果进行 \( 10^9 + 7 \) 取模。
动态规划思路
-
使用三个数组 \( f[i] \)、\( g[i] \) 和 \( h[i] \) 来记录状态:
-
\( f[i] \) 记录长度为 \( i \) 的字符串中包含子序列 "red" 的个数。
-
\( g[i] \) 记录长度为 \( i \) 的字符串中包含子串 "red" 的个数。
-
\( h[i] \) 记录长度为 \( i \) 的字符串中包含子序列 "red" 且不包含子串 "red" 的个数。
-
修改建议
原代码中存在以下问题:
-
公式不正确:代码中的递推公式似乎没有明确表述问题所需的逻辑。
-
没有处理取模:由于数据范围较大,没有对每一步计算取模,可能会导致溢出。
我建议重新编写动态规划递推公式,明确 "red" 子序列和子串的关系。
为了计算长度为 \( n \) 的由字符 'r'、'e'、'd' 构成的字符串中有多少个是“可爱串”,我们可以设计一个动态规划的算法。目标是计算包含子序列 "red" 且不包含子串 "red" 的字符串数量。由于数据范围较大,最终结果需要对 \( 10^9 + 7 \) 取模。
解题思路
我们可以把问题拆解成以下几个步骤:
-
总共的 "red" 子序列数:计算长度为 \( n \) 的字符串中包含 "red" 作为子序列的字符串数量。这可以通过动态规划来处理。
-
包含 "red" 子串的情况:找到那些包含 "red" 作为子串的字符串数量。
-
计算可爱串:从包含 "red" 子序列的字符串中减去包含 "red" 子串的字符串,即为可爱串的数量。
动态规划转移方程
-
设 \( dp_{\text{subseq}}[i] \) 表示长度为 \( i \) 的字符串中包含 "red" 子序列的字符串数量。
-
设 \( dp_{\text{substr}}[i] \) 表示长度为 \( i \) 的字符串中包含 "red" 子串的字符串数量。
最后的答案就是 \( dp_{\text{subseq}}[n] - dp_{\text{substr}}[n] \),表示有 "red" 子序列但没有 "red" 子串的字符串数量。
动态规划实现
-
子序列递推:在每个位置,可以选择字符 'r',然后在剩下的部分继续构造子序列。
-
子串递推:子串必须是连续的,因此对于每个长度为 \( i \) 的字符串,检查是否有 "red" 连续出现的情况。
代码实现
#include <cstdio> #include <vector> const int MOD = 1e9 + 7; int main() { int n; scanf("%d", &n); // dp_subseq[i] 表示长度为 i 的字符串中包含 "red" 子序列的个数 std::vector<long long> dp_subseq(n + 1, 0); // dp_substr[i] 表示长度为 i 的字符串中包含 "red" 子串的个数 std::vector<long long> dp_substr(n + 1, 0); // 初始化前面的状态 if (n >= 3) { dp_substr[3] = 1; // "red" 本身是一个长度为 3 的包含 "red" 子串的字符串 } // 计算包含 "red" 子串的字符串数量 for (int i = 4; i <= n; ++i) { // 从长度为 i-1 的字符串构造,末尾加上 'r', 'e', 'd' 组成子串 "red",3是A(3,1)排列 dp_substr[i] = (3 * dp_substr[i - 1]) % MOD; // 递推公式 } // 计算包含 "red" 子序列的字符串数量 for (int i = 3; i <= n; ++i) { // dp_subseq[i] = (3 * dp_subseq[i - 1] + 1) % MOD; // 子序列公式 } // 计算可爱串的数量 long long result = (dp_subseq[n] - dp_substr[n] + MOD) % MOD; printf("%lld\n", result); return 0; }
代码说明
-
dp_subseq[i]
计算长度为 \( i \) 的字符串中包含 "red" 子序列的字符串数量。 -
dp_substr[i]
计算长度为 \( i \) 的字符串中包含 "red" 子串的字符串数量。 -
最后输出的结果是
dp_subseq[n] - dp_substr[n]
,表示包含子序列 "red" 但不包含子串 "red" 的字符串数量,并对结果取模。
时间复杂度
-
动态规划的转移方程是线性的,时间复杂度为 \( O(n) \),可以满足 \( n \leq 10^5 \) 的要求。
取模处理
-
每次计算时我们都取模 \( 10^9 + 7 \),避免了数值溢出的问题。
通过这种方法,我们可以有效地计算出长度为 \( n \) 的字符串中有多少个是符合条件的“可爱串”。