复原IP地址
链接: 复原IP地址
有效 IP 地址 正好由四个整数(每个整数位于 0 到 255 之间组成,且不能含有前导 0),整数之间用 ‘.’ 分隔。
例如:“0.1.2.201” 和 “192.168.1.1” 是 有效 IP 地址,但是 “0.011.255.245”、“192.168.1.312” 和 “192.168@1.1” 是 无效 IP 地址。
给定一个只包含数字的字符串 s ,用以表示一个 IP 地址,返回所有可能的有效 IP 地址,这些地址可以通过在 s 中插入 ‘.’ 来形成。你 不能 重新排序或删除 s 中的任何数字。你可以按 任何 顺序返回答案。
示例 1:
输入:s = “25525511135”
输出:[“255.255.11.135”,“255.255.111.35”]
示例 2:
输入:s = “0000”
输出:[“0.0.0.0”]
示例 3:
输入:s = “101023”
输出:[“1.0.10.23”,“1.0.102.3”,“10.1.0.23”,“10.10.2.3”,“101.0.2.3”]
思路
这道题是经典的回溯算法,完全遵循回溯算法模板
做这道题之前我建议先做一下 131. 分割回文串,因为稍微改改 131 题就可以解决这道题了。
131 题的要求是:让你把字符串 s 切分成若干个合法的回文串,返回所有的切分方法。
本题的要求是:让你把字符串 s 切分成 4 个合法的 IP 数字,返回所有的切分方法。
所以我们只要把 131 题的解法稍微改改,重写一个 isValid 函数判断合法的 IP 数字,并保证整棵回溯树只有 4 层(即 track 中只有 4 个子串)即可。
解题方法
具体看代码吧,基本逻辑和 131 题一模一样
复杂度
- 时间复杂度:O(3^n)
- 空间复杂度:O(n)
Code
class Solution {
public:
vector<string> res;
deque<string> track; // 用双端队列实现列表
vector<string> restoreIpAddresses(string s) {
backtrack(s, 0);
return res;
}
// 回溯算法框架
void backtrack(string s, int start) {
if (start == s.size() && track.size() == 4) {
// base case,走到叶子节点
// 即整个 s 被成功分割为合法的四部分,记下答案
res.push_back(join_track(track));
return;
}
for (int i = start; i < s.size(); i++) {
if (!is_valid(s, start, i)) {
// s[start..i] 不是合法的 ip 数字,不能分割
continue;
}
if (track.size() >= 4) {
// 已经分解成 4 部分了,不能再分解了
break;
}
string str = s.substr(start, i - start + 1);
track.push_back(str); // 做选择,把 s[start..i] 放入路径列表中
// 进入回溯树的下一层,继续切分 s[i+1..]
backtrack(s, i + 1);
track.pop_back(); // 撤销选择
}
}
// 判断 s[start..end] 是否是一个合法的 ip 段
bool is_valid(string s, int start, int end) {
int len = end - start + 1;
if (len > 3 || len == 0) { // 长度超出了合理范围
return false;
}
if (len > 1 && s[start] == '0') { // 0x、0xx 不是合法情况
return false;
}
if (stoi(s.substr(start, len)) > 255) { // 判断是否越界
return false;
}
return true;
}
// 把路径(双端队列)转成字符串
string join_track(deque<string> track) {
string res = "";
for (int i = 0; i < track.size(); i++) {
res += track[i];
if (i < track.size() - 1) {
res += ".";
}
}
return res;
}
};
子集
链接: 子集
给你一个整数数组 nums ,数组中的元素 互不相同 。返回该数组所有可能的
子集(幂集)。
解集 不能 包含重复的子集。你可以按 任意顺序 返回解集。
示例 1:
输入:nums = [1,2,3]
输出:[[],[1],[2],[1,2],[3],[1,3],[2,3],[1,2,3]]
示例 2:
输入:nums = [0]
输出:[[],[0]]
思路
有两种方法解决这道题,这里主要说回溯算法思路,因为比较通用,可以套回溯算法模板。
解题方法
看代码
复杂度
- 时间复杂度: O(n * 2^n)
- 空间复杂度:O(n)
Code
class Solution {
public:
vector<vector<int>> res;
vector<vector<int>> subsets(vector<int>& nums) {
// 记录走过的路径
vector<int> track;
backtrack(nums, 0, track);
return res;
}
void backtrack(vector<int>& nums, int start, vector<int>& track) {
res.push_back(track);
for (int i = start; i < nums.size(); i++) {
// 做选择
track.push_back(nums[i]);
// 回溯
backtrack(nums, i + 1, track);
// 撤销选择
track.pop_back();
}
}
};
子集II
链接: 子集II
给你一个整数数组 nums ,其中可能包含重复元素,请你返回该数组所有可能的
子集
(幂集)。
解集 不能 包含重复的子集。返回的解集中,子集可以按 任意顺序 排列。
示例 1:
输入:nums = [1,2,2]
输出:[[],[1],[1,2],[1,2,2],[2],[2,2]]
示例 2:
输入:nums = [0]
输出:[[],[0]]
思路
还是使用回溯算法,但本题的元素可以重复
体现在代码上,需要先进行排序,让相同的元素靠在一起,如果发现 nums[i] == nums[i-1],则跳过
解题方法
看代码
复杂度
- 时间复杂度:O(n * 2^n)
- 空间复杂度:O(n)
Code
class Solution {
vector<vector<int>> res; // 输出结果
vector<int> track; // 搜索路径
public:
vector<vector<int>> subsetsWithDup(vector<int>& nums) {
sort(nums.begin(), nums.end()); // 排序,让相同的元素靠在一起
backtrack(nums, 0);
return res; // 返回结果
}
void backtrack(vector<int>& nums, int start) { // start 为当前的枚举位置
res.emplace_back(track); // 前序位置,每个节点的值都是一个子集
for(int i = start; i < nums.size(); i++) {
if (i > start && nums[i] == nums[i - 1]) { // 剪枝逻辑,值相同的相邻树枝,只遍历第一条
continue;
}
track.emplace_back(nums[i]); // 添加至路径
backtrack(nums, i + 1); // 进入下一层决策树
track.pop_back(); // 回溯
}
}
};
总结
- 第一题本来是很有难度的,不过 做完 分割回文串 之后,本题就容易很多了
- 第二题子集问题,就是收集树形结构中,每一个节点的结果。 整体代码其实和 回溯模板都是差不多的。
- 第三题做了 40.组合总和II 和 78.子集 ,本题就是这两道题目的结合。