刷题笔记:力扣第91、93题(全排列)

力扣第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。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值