力扣第91题-解码方法

1.拿到题目后第一个想到的是递归,每次可以选择取1个或2个数字,最后通过深度搜索找出所有可能,代码如下:
1. // 全局变量:记录总解码方法数
2. int res;
3.
4. // 递归函数:从 start 位置开始解码
5. void dsf(int start, char* s, int len){
6. // 1. 递归终止条件:走到字符串末尾 → 找到一种合法方法
7. if (start == len){
8. res++; // 方法数 +1
9. return;
10. }
11.
12. // 2. 如果当前字符是 0 → 不能单独解码,直接返回(剪枝)
13. if (s[start] == '0'){
14. return;
15. }
16.
17. // 3. 选择一:单独解码当前字符(走 1 步)
18. dsf(start + 1, s, len);
19.
20. // 4. 选择二:把当前字符 + 下一个字符 组合解码(走 2 步)
21. // 先判断是否有下一个字符,防止越界
22. if (start < len - 1) {
23. // 把两个字符转成数字:如 '1'+'2' → 12
24. int num = (s[start] - '0') * 10 + (s[start + 1] - '0');
25. // 如果组合 ≤26 → 合法,可以组合解码
26. if (num <= 26){
27. dsf(start + 2, s, len);
28. }
29. }
30. }
31.
32. // 主函数
33. int numDecodings(char* s) {
34. int len = strlen(s);
35. res = 0; // 初始化答案为 0
36.
37. // 特判 1:第一个字符是 0 → 无法解码
38. if (s[0] == '0'){
39. return 0;
40. }
41.
42. // 特判 2:连续两个 0 → 无法解码
43. for (int i = 1; i < len; i++){
44. if (s[i] == '0' && s[i - 1] == '0'){
45. return 0;
46. }
47. }
48.
49. // 从第 0 位开始递归
50. dsf(0, s, len);
51.
52. // 返回总方法数
53. return res;
54. }
该算法的时间复杂度为O(2^n),本地测试可以通过,但提交后超时了!
2.本题正确的做法应该是使用动态规划DP,字符串每一位想要能到达,只能是前进1位或者前进2位到达,所以该位对应的路径数等于前1位的路径数+前2位的路径数,可以看作“爬楼梯问题”的变种。完整代码如下:
1. int numDecodings(char* s) {
2.
3. // 1. 获取字符串长度
4. int len = strlen(s);
5.
6. // 2. 特判:如果第一个字符是 0,直接无法解码
7. if (s[0] == '0'){
8. return 0;
9. }
10.
11. // 3. 创建 DP 数组
12. // dp[i] = 前 i 个字符的解码方法总数
13. int dp[101] = {0};
14.
15. // 4. 基准条件(非常关键)
16. dp[0] = 1; // 空字符串,解法数 = 1(为了组合计算正确)
17. dp[1] = 1; // 第一个字符非 0,一定有 1 种解法
18.
19. // 5. 从第 2 个字符开始递推
20. for (int i = 2; i <= len; i++){
21.
22. // ======================
23. // 情况 1:单独解码当前 1 位
24. // ======================
25. // 当前位不是 0 才能单独解码
26. if (s[i - 1] != '0'){
27. dp[i] += dp[i - 1];
28. }
29.
30. // ======================
31. // 情况 2:组合解码前 2 位
32. // ======================
33. // 把 s[i-2] 和 s[i-1] 拼成一个两位数
34. int num = (s[i - 2] - '0') * 10 + (s[i - 1] - '0');
35.
36. // 必须是 10~26 才合法
37. if (num <= 26 && num >= 10){
38. dp[i] += dp[i - 2];
39. }
40. }
41.
42. // 6. 答案就是前 len 个字符的解法总数
43. return dp[len];
44. }
其中dp[i]的意思为前i个字符的路径数,dp[0] = 1是动态规划的标准初始化流程。该算法时间复杂度和空间复杂度均为O(n),是本题的最优解。
3.算法巧妙的通过三个判断解决了三种特殊情况:
①通过if (s[0] == '0')判断第一位是否为’0’,如果是的话就没有可行路径。
②通过if (s[i - 1] != '0')判断本位是否为’0’,如果是的话说明在选择路径时不能跳到前一位,因为后续字符串首位为’0’是一定没有解的。
③通过if (num <= 26 && num >= 10)判断本位和前一位组成的二位数是否为[10, 26]的合法范围内,可以排除前一位是’0’(即十位为0)的不合法情况。
力扣第93题-复原IP地址

1.本题是“全组合”类型题,那肯定是使用深度搜索进行递归了。对于本题的终止条件,应该是IP块mem等于4并且暂存路径长度pathSize等于字符串s的长度len,如果提前出现了传入的当前指针start越界或者mem已经等于4了但是字符串s没有被全部读取完的情况,说明本条路径是不合法的,都应该返回。在递归逻辑上,需要判断是否为第一个IP块,如果不是就先写入’.’,之后依次进行写入1位、写入2位、写入3位的递归逻辑。
2.基于以上思想,可写出完整代码如下:
1. /**
2. * Note: The returned array must be malloced, assume caller calls free().
3. */
4.
5. // 全局变量:用于DFS回溯
6. char* path; // 存储当前正在拼接的IP地址(如 255.255.11.135)
7. int mems; // 已经分割成几段IP(必须=4才合法)
8. int pathSize; // 当前path字符串的长度(下标)
9. char** res; // 存储所有合法IP结果
10. int resSize; // 结果总数
11.
12. // DFS函数:从start位置开始,尝试分割IP
13. void dfs(int start, char* s, int len){
14. // ==========================
15. // 递归终止条件
16. // 已经遍历完字符串 或 已经分成4段IP
17. // ==========================
18. if (start >= len || mems == 4){
19. // 只有【刚好用完字符串】+【分成4段】才是合法IP
20. if (start == len && mems == 4){
21. path[pathSize] = '\0'; // 字符串结束符
22. // 复制一份存入结果数组
23. char* tmp = (char*)calloc((len + 4), sizeof(char));
24. strcpy(tmp, path);
25. res[resSize++] = tmp;
26. }
27. return;
28. }
29.
30. // 保存回溯现场:当前path长度,方便递归后一键恢复
31. int lastSize = pathSize;
32.
33. // ==========================
34. // 情况1:截取 1 位数字作为一段IP
35. // ==========================
36. if (mems > 0){ // 不是第一段,前面要加 '.'
37. path[pathSize++] = '.';
38. }
39. mems++; // 段数 +1
40. path[pathSize++] = s[start]; // 写入当前数字
41. dfs(start + 1, s, len); // 继续递归下一个位置
42. mems--; // 回溯:段数-1
43. pathSize = lastSize; // 回溯:恢复path长度
44.
45. // ==========================
46. // 情况2:截取 2 位数字作为一段IP
47. // ==========================
48. if (start < len - 1){
49. // 计算两位数字的数值
50. int two = (s[start] - '0') * 10 + s[start + 1] - '0';
51. if (two >= 10){ // 必须 >=10,保证无前导零(如 01 不合法)
52. if (mems > 0){ // 非第一段加 '.'
53. path[pathSize++] = '.';
54. }
55. mems++;
56. path[pathSize++] = s[start];
57. path[pathSize++] = s[start + 1];
58. dfs(start + 2, s, len);
59. mems--; // 回溯
60. pathSize = lastSize; // 恢复path
61. }
62. }
63.
64. // ==========================
65. // 情况3:截取 3 位数字作为一段IP
66. // ==========================
67. if (start < len - 2){
68. // 计算三位数字的数值
69. int three = (s[start] - '0') * 100 + (s[start + 1] - '0') * 10 + s[start + 2] - '0';
70. if (three >= 100 && three <= 255){ // 必须 100~255,合法且无前导零
71. if (mems > 0){ // 非第一段加 '.'
72. path[pathSize++] = '.';
73. }
74. mems++;
75. path[pathSize++] = s[start];
76. path[pathSize++] = s[start + 1];
77. path[pathSize++] = s[start + 2];
78. dfs(start + 3, s, len);
79. mems--; // 回溯
80. pathSize = lastSize; // 恢复path
81. }
82. }
83. }
84.
85. // 主函数:入口
86. char** restoreIpAddresses(char* s, int* returnSize) {
87. // 全局变量初始化
88. mems = pathSize = resSize = 0;
89. int len = strlen(s);
90.
91. res = (char**)malloc(sizeof(char*) * 100); // 结果数组(足够大)
92. path = (char*)calloc(len + 4, sizeof(char)); // 存储拼接路径
93.
94. dfs(0, s, len); // 开始DFS
95.
96. *returnSize = resSize; // 返回答案数量
97. return res; // 返回所有合法IP
98. }
该算法的时间复杂度为O(1),原因是IP的组合是有限的,是常数级固定大小,空间复杂度也是O(1)。
3.思考:如果本题是要求输出可行IP组合数,而不是输出所有IP组合,是不是也可以像力扣第91题一样使用DP?答案是可行的。DFS更注重每一种组合的路径,即记录每一条路径的具体情况;而DP更注重组合的状态,只关心在该状态下有多少种路径可以到达这里,不关系具体是什么内容。所以以后遇到需要输出所有组合情况的题目就优先考虑DFS,如果只要求输出所有组合数就优先考虑DP。

&spm=1001.2101.3001.5002&articleId=159354098&d=1&t=3&u=064cfc85b9724b24897623561d34ae22)
511

被折叠的 条评论
为什么被折叠?



