力扣完全平方数279和力扣91解码方法的做题笔记
力扣279完全平方数
思路
1.**重述问题:
-
给你一个数字n,拆分出它的平方数要求和为n,且这些平方数的个数要最少
-
给定一个整数
n,找到最少数量的完全平方数,使得它们的和等于n。
2.**找出问题的最后一步: -
找到以4为结尾的平方数恰好满足4 + 4 + 4 = 12,且最少
-
假设最后一步选择了一个完全平方数
k²,那么问题转化为:找到和为n - k²的最少完全平方数。
3.**划分子问题: -
找到以4为结尾最少的平方数组合,恰好满足12 - 4
-
对于每个
i(从 1 到n),我们需要找到所有可能的k²(k² <= i),然后递归求解i - k²的最少完全平方数。
4.**边界问题: -
考虑和不能大于n
-
当
i = 0时,不需要任何完全平方数,返回 0。 -
当
i本身是一个完全平方数时,直接返回 1。 -
dfs + 记忆化搜索
const int N = 100010;
class Solution
{
public:
int mem[N];
int dfs(int n)
{
int res = 1e9;
if (n == 0)
return 0;
if (mem[n])
return mem[n];
for (int i = 1; i * i <= n; i++)
{
res = min(res, dfs(n - i * i) + 1);
}
return mem[n] = res;
}
int numSquares(int n)
{
if (n == 0)
{
return 0;
}
int ans = dfs(n);
return ans;
}
};
- dp
class Solution
{
public:
int numSquares(int n)
{
if (n == 0)
{
return 0;
}
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], 1 + dp[i - j * j]);
}
}
return dp[n];
}
};
力扣91解码方法
思路
1.**重述问题:
- 给你一个数字字符串,找出它所有能支持的编码,前缀0的是非法
- 给定一个数字字符串
s,每个数字或两个数字的组合可以解码为字母(1→A,2→B,…,26→Z)。要求计算所有可能的解码方式总数。如果字符串中包含前导零(如 “06”)或无法解码的情况,返回 0。
2.**找出问题的最后一步: - 以226为例子,最后一步为 2 B 2B 6F ,最终问题是每一个字符单独成为编码
- 对于字符串
s,解码的最后一步有两种可能:
-
单独解码最后一个字符:如果最后一个字符是
1到9,则可以单独解码为一个字母。 -
解码最后两个字符:如果最后两个字符组成的数字在
10到26之间,则可以解码为一个字母。
例如,对于 s = "226":
-
最后一步可以是
6→F,此时问题转化为解码"22"。 -
最后一步也可以是
26→Z,此时问题转化为解码"2"。
3.**划分子问题: -
前一个问题就是22V 6F,那么子问题可划分为k个字符为一个编码?
-
根据最后一步的两种可能,可以将问题划分为两个子问题:
-
解码前
n-1个字符:如果最后一个字符可以单独解码。 -
解码前
n-2个字符:如果最后两个字符可以组合解码。
例如,对于 s = "226":
-
子问题 1:解码
"22"。 -
子问题 2:解码
"2"。
4.**边界问题: -
空字符串:如果字符串为空,只有一种解码方式(即空字符串本身)。
-
单个字符:
-
如果字符是
0,无法解码,返回 0。 -
如果字符是
1到9,可以解码,返回 1。
-
-
前导零:如果字符串以
0开头(如"06"),无法解码,返回 0。 -
无效组合:如果两个字符组成的数字不在
10到26之间(如"27"),则不能组合解码。
const int N = 10010;
class Solution
{
public:
int mem[N];
int dfs(string &s, int x)
{
int n = s.size();
if (n == x)
return 1;
if (s[x] == '0')
return 0;
if (mem[x])
return mem[x];
// 单个解码
int res = dfs(s, x + 1);
// 两个解码
if (x + 1 < n)
{
int two = ((s[x] - '0') * 10) + (s[x + 1] - '0');
if (two >= 10 && two <= 26)
{
res += dfs(s, x + 2);
}
}
return mem[x] = res;
}
int numDecodings(string s)
{
int res = 0;
int n = s.size();
if (n == 0 || s[0] == '0')
return 0;
// vector<int> dp(n + 1, 0);
// dp[0] = 1;
// dp[1] = s[0] != '0' ? 1 : 0;
int prevPrev = 1;
int prev = s[0] != '0' ? 1 : 0;
for (int i = 2; i <= n; i++)
{
int cur = 0;
if (s[i - 1] != '0')
cur += prev;
// dp[i] += dp[i - 1];
int two = ((s[i - 2] - '0') * 10) + (s[i - 1] - '0');
if (two >= 10 && two <= 26)
{
// dp[i] += dp[i - 2];
cur += prevPrev;
}
prevPrev = prev;
prev = cur;
}
return prev;
// return dp[n];
}
};
做题中我遇到的问题
为什么是i - 1而不是i呢
1. 动态规划的状态定义
在动态规划中,我们通常定义:
-
dp[i]:表示前i个字符的解码方式数。-
例如,
dp[1]表示前 1 个字符的解码方式数。 -
dp[2]表示前 2 个字符的解码方式数。 -
以此类推。
-
2. 字符串的索引
-
在 C++ 中,字符串的索引是从
0开始的。-
例如,字符串
s = "226":-
s[0] = '2'。 -
s[1] = '2'。 -
s[2] = '6'。
-
-
3. 为什么用 s[i - 1] 而不是 s[i]?
-
dp[i]表示前i个字符的解码方式数:-
前
i个字符对应字符串中的s[0]到s[i - 1]。 -
因此,
dp[i]的解码方式数依赖于s[i - 1](当前字符)和s[i - 2](前一个字符)。
-
-
举例说明:
-
对于字符串
s = "226":-
dp[1]表示前 1 个字符'2'的解码方式数。 -
dp[2]表示前 2 个字符"22"的解码方式数。 -
dp[3]表示前 3 个字符"226"的解码方式数。
-
-
4. 代码逻辑
在代码中,dp[i] 的解码方式数由两部分组成:
-
单独解码当前字符:
-
如果当前字符
s[i - 1]不是'0',则可以单独解码。 -
此时,
dp[i] += dp[i - 1],表示前i - 1个字符的解码方式数。
-
-
组合解码两个字符:
-
如果当前字符
s[i - 1]和前一个字符s[i - 2]可以组合解码(即组成的数字在10到26之间),则可以组合解码。 -
此时,
dp[i] += dp[i - 2],表示前i - 2个字符的解码方式数。
-
5. 示例分析
示例:s = "226"
-
初始化:
-
dp[0] = 1(空字符串)。 -
dp[1] = 1('2'可以单独解码)。
-
-
状态转移:
i | s[i-1] | 单独解码 (dp[i] += dp[i-1]) | 组合解码 (dp[i] += dp[i-2]) | dp[i] |
|---|---|---|---|---|
| 2 | '2' | dp[2] += dp[1] = 1 | two = 22,dp[2] += dp[0] = 1 | 2 |
| 3 | '6' | dp[3] += dp[2] = 2 | two = 26,dp[3] += dp[1] = 1 | 3 |
- 最终结果:
dp[3] = 3。
6. 为什么不用 s[i]?
-
如果使用
s[i],会导致索引越界或逻辑错误:-
对于
i = 1,s[1]是第二个字符,而dp[1]表示前 1 个字符的解码方式数。 -
这样会导致状态转移的逻辑混乱,无法正确计算解码方式数。
-
7. 总结
-
dp[i]表示前i个字符的解码方式数,因此当前字符是s[i - 1]。 -
s[i - 1]是当前字符,s[i - 2]是前一个字符。 -
这种设计保证了状态转移的正确性和逻辑清晰性。
