Day28【回溯算法】93.复原IP地址、78.子集、90.子集II

93.复原IP地址

力扣题目链接/文章讲解/视频讲解

在昨天那道131.分割回文串中,我们是将字符串截取成回文子串,然后记录下截取后得到的回文子串

这道题其实是完全相同的思路。区别只有如下:

  1. 上一道题是判断截取出来的子串是否为回文子串,这道题是判断截取出来的子串是否为有效ip地址的一段(点分十进制表示的ip地址共四段)
  2. 在做记录的时候,要按照特殊格式进行记录
  3. 终止条件不同,这里当已经截取了三段时,需要将剩余部分作为最后一段,然后终止 

Day28【回溯算法】93.复原IP地址、78.子集、90.子集II_第1张图片

我们就照着上一道题的写法来写

class Solution {
private:
    vector result;  // 用于储存结果
    string path;    // 用于在遍历过程中记录分割出来的ip段
    int part = 0;   // 用于记录已经截取到的段数
public:
    vector restoreIpAddresses(string s) {
        backtracking(s);
        return result;
    }
    void backtracking(const string & s) {
        if (part == 3) {    // 终止条件:当前面已经分割出三段了,则应该把剩余子串作为第四段
            if (isValid(s)) {
                path += s;  // 如果第四段也是有效段
                result.push_back(path); // 记录结果
            }
            return;
        } else if (s.size() == 0) return;
        for (int i = 1; i <= s.size() && i <= 3; ++i) {
            string sub = s.substr(0, i);
            if (isValid(sub)) {   // 是有效ip地址段才需要继续截取
                string inital = path;   // 用于回溯
                ++part;
                path += sub;
                path += '.';
                backtracking(s.substr(i));
                path = inital;  // 回溯
                --part;
            } else
                continue;
        }
        return;
    }
    bool isValid(const string & s) {
        if (s.empty()) return false;
        if (s.size() > 1 && s[0] == '0') return false;
        double num = atof(s.data());    // atof接收一个const char *,返回一个double,s.data从string对象中返回一个const char *
        if (num > 255) return false;
        return true;
    }
};

本题和上一道几乎相同,回溯函数的参数是“剩余子串”,可用来标记回溯函数走到了不同节点

注意substr的用法 

78.子集 

力扣题目链接/文章讲解/视频讲解

本题抽象为树形结构如下

Day28【回溯算法】93.复原IP地址、78.子集、90.子集II_第2张图片

本题和组合问题不一样的地方在于:组合问题只有到达叶子节点才可能收集结果。而由图可知:子集问题到每个节点都需要收集结果

本题的需要记录的结果仍然是路径,因此还是选择记录路径的回溯算法模板

需要全局变量数组path在遍历过程中记录子集元素,二维数组result存放结果

vector path;
vector > result;

终止条件:当剩余集合为空,返回,即当无可选择路径,就终止返回

换句话说就是startIndex已经大于数组的最大索引值时,就终止了,因为没有元素可取了(如果不理解可以往后继续看代码,后面for循环能够显示出startIndex和剩余可取元素的关系) 

if (startIndex >= nums.size()) {
    return;
}

startIndex能够标识节点:startIndex能够控制在该节点时,可以选择的路径有哪些(剩余的集合有哪几个元素) 

我们传入startIndex,很容易就能够知道在该节点,还能往哪些路径前行

如何表示“走向下一个节点”?注意我们的startIndex标记了不同节点,我们向回溯函数按题目需求传入不同的startIndex, 就能够表明回溯函数走向了不同的节点

for (int i = startIndex; i < nums.size(); i++) {    // 既然是无序集合,取过的元素不会重复取,写回溯算法的时候,for就要从startIndex开始
    path.push_back(nums[i]);    // 记录子集元素
    backtracking(nums, i + 1);  // 注意从i+1开始,元素不重复取,看图
    path.pop_back();            // 回溯
}

完整代码

class Solution {
private:
    vector path;
    vector > result;
public:
    vector> subsets(vector& nums) {
        backtracking(nums, 0);
        return result;
    }
    void backtracking(vector & nums, int startIndex) {
        result.push_back(path);    // 到每个节点都需要收集结果!
        if (startIndex >= nums.size())
            return;
        for (int i = startIndex; i < nums.size(); ++i) {
            path.push_back(nums[i]);
            backtracking(nums, i + 1);
            path.pop_back();
        }
        return;
    }
};

别忘了我们在推导回溯函数模板时,我们说过回溯函数的内容其实就是走到一个节点后,接下来应该做的事情。在这里我们到达一个节点,就应该收集结果,故一进backtracking函数就要往result添加结果 

90.子集II

力扣题目链接/文章讲解/视频讲解

本题其实就是40.组合总和II78.子集的缝合怪

40.组合总和II中最大的难题就是:站在一个节点的时候,有相同的前往后续节点的路径,我们要想办法跳过重复路径

我们当时的思路是用set记录在每个节点时,途径该节点已经去过的路径,这样就能够在前往下一个节点之前,看看这条路径是否已经去过,如果已经去过就跳过

当时需要注意的一个关键点就是需要先对数组排序!否则会出现重复的组合 

78.子集关键就是到每个节点都需要收集结果

这两种技巧缝合一下,这道题就出来了 

class Solution {
private:
    vector path;
    vector > result;
public:
    vector> subsetsWithDup(vector& nums) {
        sort(nums.begin(), nums.end());    // 树层去重需要先排序
        backtracking(nums, 0);
        return result;
    }
    void backtracking(const vector & nums, int startIndex) {
        result.push_back(path);    // 到每个节点都需要收集结果
        if (startIndex >= nums.size())    // 终止条件:当startIndex超过数组最大索引值,则返回
            return;
        unordered_set used;    // 用于储存在该节点已经选择过的路径
        for (int i = startIndex; i < nums.size(); ++i) {    // 遍历可选择的路径,startIndex控制了在某个节点的可选择路径
            if (used.find(nums[i]) != used.end())    // 如果已选择过这条路径,则跳过
                continue;
            used.insert(nums[i]);    // 标记这条路径为“已选择”
            path.push_back(nums[i]);    // 添加记录
            backtracking(nums, i + 1);    // 前往这样一个节点:在该节点的可选择元素在数组中的索引为i+1到nums.size() - 1
            path.pop_back();    // 撤销记录,回溯
        }
        return;    // 返回
    }
};

回顾总结 

今天都是一些小技巧,个人感觉难点还是在于“如何知道在某个节点,有哪些路径可去”,往往是通过额外传入参数确定在该节点可选择路径的。有时,终止条件也可以通过这个额外参数确定,当额外参数所表达的是“在该节点无可选择路径了”,就说明到终止条件了

此外,别忘了我们最开始的推导:回溯函数处理树节点的过程就像是人在行走的过程。 

你可能感兴趣的:(代码随想录,算法,数据结构,c++,leetcode)