day24
93. 复原 IP 地址 - 力扣(LeetCode)
class Solution {
public:vector<string> res;bool isValid(string s, int start , int end){if(start > end) return false;if(s[start] == '0' && start != end) return false;int num = 0;for(int i = start; i <= end; i++){if(s[i] > '9' || s[i] < '0') return false;num = num * 10 + s[i] - '0';if(num > 255) return false;} return true;}void dfs(string s, int index, int pointNUum){if(pointNUum == 3){if(isValid(s, index, s.size() -1)){res.push_back(s);}return;}for(int i = index; i < s.size(); i++){if(isValid(s,index, i)){s.insert(s.begin() + i + 1, '.');pointNUum++;dfs(s, i+2, pointNUum);pointNUum--;s.erase(s.begin()+1+i);} else {break;}}}vector<string> restoreIpAddresses(string s) {dfs(s, 0 ,0);return res;}
};
78. 子集 - 力扣(LeetCode)
class Solution {
public:vector<vector<int>> subsets(vector<int>& nums) {vector<vector<int>> ans;vector<int> path;int n = nums.size();auto dfs = [&](this auto && dfs, int index){if(index == n){ans.push_back(path);return;}dfs(index+1); // 不选这个数字path.push_back(nums[index]);//选dfs(index+1);path.pop_back();};dfs(0);return ans;}
};
一句话核心
对每个元素做“两选一”:要它 / 不要它。从左到右把这 n 次二选一做完,就天然枚举出了 2^n
个子集;到头就把当前选择路径记录下来。
状态与不变式
在代码里,我们用两个量描述“走到哪儿了、当前拿了谁”:
-
i
:正在决定第i
个元素是否取用(范围 0..n)。 -
path
:目前已经“选进来”的元素(就是正在构造的那个子集)。
每进一层递归,就在第 i
个元素上做决定;每回到上一层,就撤销刚才的决定(回溯)。
分支(决策)怎么长
对 nums[i]
有两条路:
-
不选它:
self(i+1)
;path
不变 -
选它:先
path.push_back(nums[i])
,然后self(i+1)
,回来时path.pop_back()
当 i == n
,说明所有元素都做完“二选一”了,此时 path
就是一个完整子集,收集进 ans
。
你可以把整棵搜索树看成一个深度为 n 的二叉树:
每一层对应一个元素,左边“不选”,右边“选”,叶子结点就是一个子集。
为什么能覆盖所有子集、且不重不漏?
-
每个元素恰好被处理一次(要/不要),因此所有组合都被覆盖;
-
没有重复:因为每个元素的“取舍”只做一次,不会从别的路径再来一次;
-
包含空集:如果一路都“不选”,到
i==n
时path
为空,也会被收集; -
包含全集:如果一路都“选”,
path
就是所有元素,也会被收集。
和“排列/组合”有什么区别?
-
子集不关心顺序(
{1,2}
与{2,1}
同一个),所以我们只按原顺序往后做“要/不要”,不会调换位置; -
排列要考虑位置顺序,就需要“已用标记 + 枚举未用元素”;
-
**组合(选 k 个)**是在子集的框架上再加一个“已选数量==k 才收集”的条件。
走一遍小例子(nums = [1,2,3]
)
深度优先的顺序大致如下([]
表示 path):
-
不选1 → 不选2 → 不选3 → 收集
[]
-
不选1 → 不选2 → 选3 → 收集
[3]
-
不选1 → 选2 → 不选3 → 收集
[2]
-
不选1 → 选2 → 选3 → 收集
[2,3]
-
选1 → 不选2 → 不选3 → 收集
[1]
-
选1 → 不选2 → 选3 → 收集
[1,3]
-
选1 → 选2 → 不选3 → 收集
[1,2]
-
选1 → 选2 → 选3 → 收集
[1,2,3]
正好是 2^3 = 8
个子集,全覆盖、不重不漏。
复杂度
-
时间:
O(n · 2^n)
;共有2^n
个子集,每个子集输出平均要拷贝/遍历 O(n)。 -
空间:递归深度 + 路径
O(n)
。
这段代码里的小细节(为何这样写更顺)
-
ans.reserve(1<<n)
、path.reserve(n)
:预分配减少扩容; -
回溯的“选 → 递归 → pop”是原地修改再还原,比“复制一份再传”更省内存;
90. 子集 II - 力扣(LeetCode)
class Solution {
public:vector<vector<int>> subsetsWithDup(vector<int>& nums) {sort(nums.begin(), nums.end());vector<vector<int>> ans;vector<int> path;int n = nums.size();auto dfs = [&](this auto && dfs, int index)->void{ans.push_back(path);for(int i = index; i < n; i++){if(i > index && nums[i] == nums[i-1]) continue;path.push_back(nums[i]);dfs(i+1);path.pop_back();}};dfs(0);return ans;}
};