题目:93.复原IP地址、78.子集、90.子集II
参考链接:代码随想录
93.复原IP地址
思路:本题的思路和上题切割回文串类似,也是先要写一个判断函数,然后一个个切割。对返回条件,如果路径长度已经为4,则可以判断是否切割完毕,如果已经切割到最后,则可以把path加入结果;for循环用于判断end即切割位置;递归用于逐个往后切割。时间复杂度O(n*2^n)。
class Solution {
public:bool isValidIp(string s,int begin,int end){//同样我们尽量不用子串,使用begin和end,[begin,end)if(end-begin==1||end-begin>1&&end-begin<=3&&s[begin]!='0'){//长度符合要求,长度为1时可以为任何值,长度为2和3的时候必须满足首位不为0string s1=string(s.begin()+begin,s.begin()+end);//生成子串,使用substr函数也可以int i=stoi(s1);//转换为intif(i>=0&&i<=255){return true;}}return false;}string strToIP(vector<string> &path){//将向量转换为IP地址string ip;ip+=path[0];ip.push_back('.');ip+=path[1];ip.push_back('.');ip+=path[2];ip.push_back('.');ip+=path[3];return ip;}vector<string> ans;vector<string> path;//用于记录路径void backtracking(const string &s,int begin,int end){//end才是第一次切割的位置!if(path.size()==4){//已经达到4的长度if(end>s.size()){//end越界说明已经全部选完了ans.push_back(strToIP(path));}return;}for(int i=end;i<=s.size();i++){if(!isValidIp(s,begin,i)){//第一次切割不满足,直接往后切continue;}path.push_back(string(s.begin()+begin,s.begin()+i));backtracking(s,i,i+1);//如果切到结尾,i+1=size+1path.pop_back();}}vector<string> restoreIpAddresses(string s) {backtracking(s,0,1);return ans;}
};
这里有一个问题,比如25525511135,如果切割了2,5,5,这时判断最后一个不管怎么切割,都要进入下一层递归,实际上这时候已经不可能完成结果,可以进行剪枝处理。可以根据begin和path大小来剪枝,当path长度为1时,如果剩余长度size-begin>9则剪枝,当path长度为2时,剩余长度size-begin>6则剪枝,当path长度为3时,剩余长度size-begin>3则剪枝。可以将其写入for循环的条件判断中。
for(int i=end;i<=s.size()&&s.size()-begin<=3*(4-path.size());i++)
想这个剪枝用了很长时间,在面试的时候如果能通过就不要细想剪枝了。
标答使用的是左闭右闭区间,增加了一个变量portNum,用于记录逗点个数,直接在原来的字符串上增加逗点分割,空间占用比我们更小。标答也没有完全剪枝,只是粗略的根据总长度剪枝了一下。
标答:
class Solution {
private:vector<string> result;// 记录结果// startIndex: 搜索的起始位置,pointNum:添加逗点的数量void backtracking(string& s, int startIndex, int pointNum) {if (pointNum == 3) { // 逗点数量为3时,分隔结束// 判断第四段子字符串是否合法,如果合法就放进result中if (isValid(s, startIndex, s.size() - 1)) {result.push_back(s);}return;}for (int i = startIndex; i < s.size(); i++) {if (isValid(s, startIndex, i)) { // 判断 [startIndex,i] 这个区间的子串是否合法s.insert(s.begin() + i + 1 , '.'); // 在i的后面插入一个逗点pointNum++;backtracking(s, i + 2, pointNum); // 插入逗点之后下一个子串的起始位置为i+2pointNum--; // 回溯s.erase(s.begin() + i + 1); // 回溯删掉逗点} else break; // 不合法,直接结束本层循环}}// 判断字符串s在左闭又闭区间[start, end]所组成的数字是否合法bool isValid(const string& s, int start, int end) {if (start > end) {return false;}if (s[start] == '0' && start != end) { // 0开头的数字不合法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) { // 如果大于255了不合法return false;}}return true;}
public:vector<string> restoreIpAddresses(string s) {result.clear();if (s.size() < 4 || s.size() > 12) return result; // 算是剪枝了backtracking(s, 0, 0);return result;}
};
时间复杂度O(3^4),直接根据IP地址的构成计算的时间复杂度。
78.子集
思路:本题要考虑子集的元素个数,在主函数中根据元素个数分类,从0到nums.size()。在回溯函数中,把元素个数k作为一个固定值,当路径长度等于k时返回。其他步骤和前面题目就类似了。剪枝不再过细考虑。时间复杂度O(n^3*2^n),和前面的题目我们多写了一个for循环。
class Solution {
public:vector<vector<int>> ans;vector<int> path;void backtracking(vector<int>& nums,int k,int begin){//k为长度,一直保持不变,begin为开始取的位置if(path.size()==k){//路径长度为k,返回ans.push_back(path);return;}for(int i=begin;i<=nums.size()-1;i++){path.push_back(nums[i]);backtracking(nums,k,i+1);//开始选下一个path.pop_back();}}vector<vector<int>> subsets(vector<int>& nums) {for(int k=0;k<=nums.size();k++){//根据长度分类,0为空集backtracking(nums,k,0);}return ans;}
};
看完标答发现可以不用在主函数中写for循环,可以在每一层取元素的时候就直接将其加入ans中,第一层取1个元素,第二层取2个元素。
标答:
class Solution {
public:vector<vector<int>> ans;vector<int> path;void backtracking(vector<int>& nums,int begin){//k为长度,一直保持不变,begin为开始取的位置if(begin==nums.size()){//开始位置已经到达末尾return;}for(int i=begin;i<=nums.size()-1;i++){path.push_back(nums[i]);ans.push_back(path);//每一层遍历都要直接将结果加入ansbacktracking(nums,i+1);//开始选下一个path.pop_back();}}vector<vector<int>> subsets(vector<int>& nums) {ans.push_back(path);//先加入一个空集backtracking(nums,0);return ans;}
};
时间复杂度O(n*2^n)。
90.子集II
思路:结合之前used去重的思路,加入上题,即为答案。时间复杂度O(n*2^n)。
class Solution {
public:vector<vector<int>> ans;vector<int> path;void backtracking(vector<int>& nums,int begin,vector<int>& used){ans.push_back(path);//先加入自己,这样不用额外添加空集if(begin==nums.size()){return;}for(int i=begin;i<nums.size();i++){if(i>0&&nums[i]==nums[i-1]&&used[i-1]==0){//两个相邻,且前一个未使用过,说明是同一层的,直接跳过continue;}path.push_back(nums[i]);used[i]=1;backtracking(nums,i+1,used);path.pop_back();used[i]=0;}}vector<vector<int>> subsetsWithDup(vector<int>& nums) {sort(nums.begin(),nums.end());vector<int> used(nums.size(),0);//先全部初始化为0backtracking(nums,0,used);return ans;}
};
也可以不用used,不过我们初学者最好用,好理解。