回溯算法OJ

回溯算法

  • 1. 回溯模板
  • 2. 组合问题
    • 2.1 LeetCode第77题---组合
    • 2.2 LeetCode第17题---电话号码的字母组合
    • 2.3 LeetCode第39题---组合总和
    • 2.4 LeetCode第40题---组合总和II
    • 2.5 LeetCode第216题---组合总和III
    • 2.6 LeetCode第22题---括号生成
  • 3. 子集问题
    • 3.1 LeetCode第78题---子集I
    • 3.2 LeetCode第90题---子集II
  • 4. 排列问题
    • 4.1 LeetCode第46题---全排列I
    • 4.2 LeetCode第47题---全排列II
    • 4.3 剑指offer第38题---字符串排列
    • 4.4 LeetCode第1079题---活字印刷
  • 5. 棋盘问题
    • 5.1 LeetCode第51题---N皇后I
    • 5.2 LeetCode第52题---N皇后II
    • 5.3 LeetCode第37题---解数独
  • 5. 分割问题
    • 5.1 LeetCode第131题---分割回文串
    • 5.2 LeetCode第93题---复原IP地址
  • 6. 普通回溯法
    • 6.1 剑指offer12题---矩阵中的路径
    • 6.2 二叉树路径问题
      • 6.2.1 LeetCode第257题---二叉树的所有路径
      • 6.2.2 LeetCode第113题---路径总和II
      • 6.2.3 剑指offer34题---二叉树和为某一值的路径(和上一题一样)
      • 6.2.4 LeetCode第437题---路径总和III
      • 6.2.5 LeetCode第112题---路径总和
      • 6.2.6 LeetCode第988题---从叶子节点开始的最小字符串
      • 6.3.1 LeetCode第543题---二叉树的直径
      • 6.3.2 LeetCode第124题---二叉树中的最大路径和

1. 回溯模板

for循环可以理解是横向遍历,backtracking(递归)就是纵向遍历,这样就把这一棵树全遍历完了,⼀般来说,搜索叶⼦节点就是找的其中⼀个结果了。
回溯算法OJ_第1张图片

2. 组合问题

对于组合问题是需要一个startindex的,因为只有你拥有了这个startindex你才能保证自己在选过第一个数以后,后面的起始位置不在重复选择

2.1 LeetCode第77题—组合

回溯算法OJ_第2张图片

class Solution {
public:
    void DFS(int n,int k,vector<vector<int>>& vv,vector<int>& v,int startindex)
    {
        if(v.size() == k)
        {
            vv.push_back(v);
            return;
        }
        //但是这里要考虑一个去重就是,如果你已经找到了所有1的排列,那么剩下的就不再考虑有1的存在了
        //这里的思考和原来是不一样的,我们需要一个起始位置
        //就是这句话,如何让他知道下一层应该从哪里开始遍历
        for(int i = startindex;i<=n;++i)
        {
            v.push_back(i);
            DFS(n,k,vv,v,i+1);
            v.pop_back();
        }
    }
    vector<vector<int>> combine(int n, int k) {
        //给你[1,n]的数组,然后让你在里面选择k个数来进行组合,只是这个过程中是不能够有重复的数组的答案的
        vector<vector<int>> vv;
        vector<int> v;
        DFS(n,k,vv,v,1);
        return vv;
    }
};

2.2 LeetCode第17题—电话号码的字母组合

回溯算法OJ_第3张图片
题解:
回溯算法OJ_第4张图片
首先通过unordered_map构建一个映射,,此时有些像字符串的拼接,当选择到字符2的时候,它里面的字母有"def",每一个都能够构成一种组合。

class Solution {
public:
    unordered_map<char,string> HashMap;
    //其实这道题仔细思考一下有点像是两层循环的味道
    //组合的长度应该和所提供的数字字符串长度一样
    void dfs(string digits,vector<string>& Allret,string Curstr,int index)
    {
        if(index == digits.size())
        {
            Allret.push_back(Curstr);
            return;//这个return是回退到了上一步的第二个字母不同的dfs中
        }
        //这一步相当于每次进行字符和字符串之间的转换,也就是需要我们所定义的unordered_map
        string curMap = HashMap[digits[index]];
        for(char ch : curMap)
        {
            //此时相当于拿到了第一个数字里面字符串所对应的字符
            dfs(digits,Allret,Curstr + ch,index+1);
        }
    }
    vector<string> letterCombinations(string digits) {
        vector<string> Allret;
        HashMap = {{'2',"abc"},{'3',"def"},{'4',"ghi"},{'5',"jkl"},{'6',"mno"},{'7',"pqrs"},{'8',"tuv"},{'9',"wxyz"}};
        if(digits.empty())
            return Allret;
        dfs(digits,Allret,"",0);
        return Allret;
    }
};

2.3 LeetCode第39题—组合总和

回溯算法OJ_第5张图片
这里举例子进行一下说明:
回溯算法OJ_第6张图片

回溯算法OJ_第7张图片
题解:这里需要一个startindex,为的就是能够不出现重复的组合,对于解这类组合问题需要一个临时的数组和一个临时的sum总和,保存每个小过程中组合情况

class Solution {
public:
    //candidates可以无限取,只要能凑出来就行
    //如果两个组合当中数字都是一样的,那么这两个组合只能够看作一个,也就是题目所要求的的不能够有重复的,比如[2,2,3] [3,2,2]是一样的
    //这里的startindex就是要处理避免重复的组合,例如上面这个例子
    //可以一直的重复加自己的元素,直到符合target或者大于了target也就是不满足
    void dfs(vector<int>& candidates, int target,vector<vector<int>>& allRet,vector<int>& tmp,int Cursum,int startindex) 
    {
        if(Cursum > target)
            return;
        if(Cursum == target)
        {
            allRet.push_back(tmp);
            return;
        }
        for(int i = startindex;i<candidates.size();++i)
        {
            Cursum += candidates[i];
            tmp.push_back(candidates[i]);
            dfs(candidates,target,allRet,tmp,Cursum,i); //这里不需要i+1,表示可以重复的读取当前的数
            Cursum -= candidates[i];
            tmp.pop_back();
        }
    }
    vector<vector<int>> combinationSum(vector<int>& candidates, int target) {
        vector<vector<int>> allRet;
        if(candidates.empty())
            return allRet;
        vector<int> tmp;
        dfs(candidates,target,allRet,tmp,0,0);
        return allRet;
    }
};

2.4 LeetCode第40题—组合总和II

这道题和上一道最大的区别在于集合当中是由重复的元素的,所以要考虑到去重问题
回溯算法OJ_第8张图片

class Solution {
public:
    //这道题是要参考全排列去重的
    void DFS(vector<int>& candidates, int target,vector<bool>& visited,vector<vector<int>>& vv,vector<int>& v,int cursum,int startindex)
    {
        if(cursum > target)
            return;
        if(cursum == target)
        {
            vv.push_back(v);
            return;
        }
        for(int i = startindex;i<candidates.size();++i)
        {
            //因为横向遍历是不能够有重复的,但是纵向遍历是可以有重复的,并且在写这一步的时候应该已经排号序了
            if(i > 0 && candidates[i] == candidates[i-1] && visited[i-1] == false)
                continue;
            //但是对于这道题还有一个地方,就是题目最后的限定,解集不能包含重复的组合
            if(visited[i] == false)
            {
                visited[i] = true;
                v.push_back(candidates[i]);
                cursum += candidates[i];
                DFS(candidates,target,visited,vv,v,cursum,i+1);
                v.pop_back();
                visited[i] = false;
                cursum -= candidates[i];
            }
        }
    }
    vector<vector<int>> combinationSum2(vector<int>& candidates, int target) {
        //每个数字在组合中只能使用一次
        vector<vector<int>> vv;
        vector<bool> visited(candidates.size(),false);
        sort(candidates.begin(),candidates.end());
        vector<int> v;
        DFS(candidates,target,visited,vv,v,0,0);
        return vv;
    }
};

2.5 LeetCode第216题—组合总和III

回溯算法OJ_第9张图片

class Solution {
public:
    void DFS(int k,int n,vector<int>& v,vector<vector<int>>& vv,int tmpsum,int startindex)
    {
        if(v.size() == k && tmpsum != n)
            return;
        if(tmpsum == n && v.size() == k)
        {
            vv.push_back(v);
            return;
        }
        for(int i = startindex;i<=9;++i)
        {
            tmpsum += i;
            v.push_back(i);
            DFS(k,n,v,vv,tmpsum,i+1);
            v.pop_back();
            tmpsum -= i;
        }
    }
    vector<vector<int>> combinationSum3(int k, int n) {
        vector<vector<int>> vv;
        vector<int> v;
        DFS(k,n,v,vv,0,1);
        return vv;
    }
};

2.6 LeetCode第22题—括号生成

LeetCodehttps://leetcode-cn.com/problems/generate-parentheses/
回溯算法OJ_第10张图片

①当前左右括号都有大于 0 个可以使用的时候,才产生分支;
②产生左分支的时候,只看当前是否还有左括号可以使用;
③产生右分支的时候,还受到左分支的限制,右边剩余可以使用的括号数量一定得在严格大于左边剩余的数量的时候,才可以产生分支;
④在左边和右边剩余的括号数都等于0的时候结算。
解题思路:对于回溯本质其实就是再求一颗隐式的树,这里使用到的方式就是回溯+剪枝
回溯算法OJ_第11张图片

class Solution {
public:
    //这道题到底是动态规划好做还是回溯算法好做呢?
    vector<string> generateParenthesis(int n) {
        //curstr 当前的字符串
        //第一个n表示左括号的个数
        //第二个n表示右括号的个数
        //res表示结果集
        vector<string> res;
        if(n == 0)
            return res;
        DFS("",n,n,res);
        return res;
    }
//当前左右括号都有大于 0 个可以使用的时候,才产生分支;
//产生左分支的时候,只看当前是否还有左括号可以使用;
//产生右分支的时候,还受到左分支的限制,右边剩余可以使用的括号数量一定得在严格大于左边剩余的数量的时候,才可以产生分支;
//在左边和右边剩余的括号数都等于0的时候结算。

    //回溯 + 剪枝
    //生成左枝干的条件:左括号的数量>0
    //生成右枝干的条件:
    void DFS(string curstr,int left,int right,vector<string>& res)
    {
        if(left == 0 && right == 0)
        {
            res.push_back(curstr);
            return;
        }
        //此时就需要剪枝
        if(left > right)
            return;
        if(left > 0)
            DFS(curstr +'(',left-1,right,res);
        if(right > 0)
            DFS(curstr + ')',left,right-1,res);
    }   
};

3. 子集问题

对于子集问题来说,就是要求得所有的节点,然而排序和组合问题都是求的叶子节点,对于自己来说也是需要一个startindex的

3.1 LeetCode第78题—子集I

回溯算法OJ_第12张图片

class Solution {
public:
    //这将是回溯的另一种情况
    //这个相对于组合问题,收集的是所有的节点情况,而组合问题收集的是所有的叶子节点情况
    void DFS(vector<int>& nums,vector<vector<int>>& vv,vector<int>& v,int startindex)
    {
        vv.push_back(v);
        if(startindex == nums.size())
            return;
        for(int i = startindex;i<nums.size();++i)
        {
            v.push_back(nums[i]);
            DFS(nums,vv,v,i+1);
            v.pop_back();
        }
    }
    vector<vector<int>> subsets(vector<int>& nums) {
        vector<vector<int>> vv;
        vector<int> v;
        DFS(nums,vv,v,0);
        return vv;
    }
};

3.2 LeetCode第90题—子集II

回溯算法OJ_第13张图片

相对于上屉屉就是多了一个数组里面可能有重复的值,所以这里要考虑到去重

class Solution {
public:
    void DFS(vector<int>& nums,vector<bool>& visited,vector<vector<int>>& vv,vector<int>& v,int startindex)
    {
        vv.push_back(v);
        if(startindex == nums.size())
            return;
        for(int i = startindex;i<nums.size();++i)
        {
            if(i > 0 && nums[i] == nums[i-1] && visited[i-1] == false)
                continue;
            if(visited[i] == false)
            {
                visited[i] = true;
                v.push_back(nums[i]);
                DFS(nums,visited,vv,v,i+1);
                v.pop_back();
                visited[i] = false;
            }
        }
    }
    //一想他和上一道题就肯定是存在里面包含重复的元素这种情况
    vector<vector<int>> subsetsWithDup(vector<int>& nums) {
        vector<vector<int>> vv;
        vector<bool> visited(nums.size(),false);
        vector<int> v;
        sort(nums.begin(),nums.end());
        DFS(nums,visited,vv,v,0);
        return vv;
    }
};

4. 排列问题

遇见排列问题如果是数组,那么遇见了里面都没有重复的元素的时候,题目还相对简单一些,但是如果数组里面有重复的元素的时候,就需要考虑到去重问题了,而且一定不能忘记要对nums数组早早的进行一次排序,这样在写去重判断条件的时候才会简单一些。如果是字符串一定要考虑使用unordered_set进行去重操作

4.1 LeetCode第46题—全排列I

链接:https://leetcode-cn.com/problems/permutations/
回溯算法OJ_第14张图片
排列问题和组合问题的不同在于,排列是每次都需要从头检查一遍数组的,也就是说他不需要startindex这个。
回溯算法OJ_第15张图片

class Solution {
public:
    void DFS(vector<int>& nums,vector<bool>& visited,vector<vector<int>>& ret,vector<int>& v)
    {
        if(v.size() == nums.size())
        {
            ret.push_back(v);
            return;
        }
        //排列问题时每次都要从头开始搜索的
        for(int i = 0;i<nums.size();++i)
        {
            if(visited[i] == true)
                continue;
            visited[i] = true;
            v.push_back(nums[i]);
            DFS(nums,visited,ret,v);
            v.pop_back();
            visited[i] = false;
        }
    }

    vector<vector<int>> permute(vector<int>& nums) {
        vector<bool> visited(nums.size(),false);
        vector<vector<int>> ret;
        vector<int> v;
        DFS(nums,visited,ret,v);
        return ret;
    }
};

4.2 LeetCode第47题—全排列II

回溯算法OJ_第16张图片

回溯算法OJ_第17张图片

class Solution {
public:
    //事实证明unordered_set是不能够写成 unordered_set> 错误的
    void DFS(vector<int>& nums,vector<bool>& visited,vector<int>& v,vector<vector<int>>& vv)
    {
        if(v.size() == nums.size())
        {
            vv.push_back(v);
            return;
        }
        for(int i = 0;i<nums.size();++i)
        {
            //去重应该在这一步完成
            //nums[i-1]和nums[i] 有可能是在同一层的,也有可能实在同一个枝干上的
            if(i > 0 && nums[i] == nums[i-1] && visited[i-1] == false)
                continue;
            if(visited[i] == false)
            {
                visited[i] = true;
                v.push_back(nums[i]);
                DFS(nums,visited,v,vv);
                v.pop_back();
                visited[i] = false;
            }
        }
    }
    vector<vector<int>> permuteUnique(vector<int>& nums) {
        //这次这个全排列里面就有了重复的数,所以这里应该要使用到unordered_set这个来进行去重
        vector<vector<int>> vv;
        vector<int> v;
        sort(nums.begin(),nums.end());
        vector<bool> visited(nums.size(),false);
        DFS(nums,visited,v,vv);
        return vv;
    }
};

4.3 剑指offer第38题—字符串排列

回溯算法OJ_第18张图片

链接:https://leetcode-cn.com/problems/zi-fu-chuan-de-pai-lie-lcof/
题解:对于字符串排序,组合这类题都是要使用拼接的思想的,一开始就是一个空字符串,然后不断的记录当前拼接的长度

class Solution {
public:
    void DFS(string& s,unordered_set<string>& us,vector<bool>& visited,string curstr,int k)
    {
        if(k == s.size())
        {
            us.insert(curstr);
            return;
        }
        for(int i = 0;i<s.size();++i)
        {
            if(visited[i] == false)
            {
                visited[i] = true;
                DFS(s,us,visited,curstr+s[i],k+1);
                visited[i] = false;
            }
        }
    }
    vector<string> permutation(string s) {
        unordered_set<string> us;
        vector<bool> visited(s.size(),false);
        DFS(s,us,visited,"",0);
        vector<string> v(us.begin(),us.end());
        return v;
    }
};

4.4 LeetCode第1079题—活字印刷

回溯算法OJ_第19张图片

回溯算法OJ_第20张图片
题解:对于这个图的解释其实在这段代码中还是不太一致的,因为他不管是否你字符串中是否有重复的,我都当做没有重复的在进行组合,那么如果出现了重复的,我只需要使用unordered_set进行一下去重即可

class Solution {
public:
    void DFS(string tiles,vector<bool>& visited,unordered_set<string>& us,string curstr)
    {
        if(!curstr.empty())
        {
            us.insert(curstr);
        }
        for(int i = 0;i<tiles.size();++i)
        {
            if(visited[i] == false)
            {
                visited[i] = true;
                DFS(tiles,visited,us,curstr+tiles[i]);
                visited[i] = false;
            }
        }
    }
    int numTilePossibilities(string tiles) {
        if(tiles.size() == 0)
            return 0;
        vector<bool> visited(tiles.size(),false);
        unordered_set<string> us;
        DFS(tiles,visited,us,"");
        return us.size();
    }
};

5. 棋盘问题

5.1 LeetCode第51题—N皇后I

回溯算法OJ_第21张图片
题解:传过来的是当前行,然后判断在所有列中是否有合适的位置,如果有这个位置就可以保存起来

class Solution {
public:
    void DFS(vector<vector<pair<int,int>>>& allRet,vector<pair<int,int>>& curRet,int curRow,int n)
    {
        //走到这里说明已经是一个完整的N皇后结果了,可以进行保存了
        if(curRow == n)
        {
            allRet.push_back(curRet);
            return;
        }
        //这里是在判断这一行的每一列
        for(int i = 0;i<n;++i)
        {
            //这个Valid的作用是判断是否当前的这一行中的第i个位置没有和当前这一轮结果中皇后的位置造成冲突
            if(isValid(curRow,i,curRet))
            {
                curRet.push_back(make_pair(curRow,i));
                DFS(allRet,curRet,curRow+1,n);
                curRet.pop_back();
            }
        }
    }

    bool isValid(int row,int col,vector<pair<int,int>>& curRet)
    {
        //表示这个坐标是合法的,可以防止皇后的条件是:
        //首先不能同一行,也不能同一列,还不能同一上斜线,也不能下斜线
        //仔细分析就回发现,肯定在下一行选的坐标,所以不需要在判断是否为同一行,但是是否在同一列还是需要进行判断的
        //其次就是如果是同一上斜线的,你会发现他们的坐标值相加是一样的
        //如果是同一下斜线,那么他们的坐标值相减是一样的
        for(pair<int,int>& e : curRet)
        {
            if(col == e.second || e.first + e.second == row + col || e.first - e.second == row - col)
                return false;
        }
        return true;
    }
    //这个函数就是一个把坐标值转换为所需的字符串形式的类型
    vector<vector<string>> transtr(vector<vector<pair<int,int>>>& allRet,int n)
    {
        vector<vector<string>> ret;
        //这一步相当于拿到了一个vector,里面存的都是当前这个方案中皇后存放的位置坐标
        for(vector<pair<int,int>>& e : allRet)
        {
            vector<string> v(n,string(n,'.'));
            for(pair<int,int>& i : e)
            {
                v[i.first][i.second] = 'Q';
            }
            ret.push_back(v);
        }
        return ret;
    }
    vector<vector<string>> solveNQueens(int n) {
        vector<vector<pair<int,int>>> allRet;
        vector<pair<int,int>> curRet; //此时这里面存放的是当前的皇后坐标情况
        DFS(allRet,curRet,0,n);
        return transtr(allRet,n);
    }
};

5.2 LeetCode第52题—N皇后II

链接:https://leetcode-cn.com/problems/n-queens-ii/
回溯算法OJ_第22张图片
题解:判断方案的个数反而更简单一下了。

class Solution {
public:
    void DFS(vector<vector<pair<int,int>>>& allRet,vector<pair<int,int>>& curRet,int curRow,int n)
    {
        //走到这里说明已经是一个完整的N皇后结果了,可以进行保存了
        if(curRow == n)
        {
            allRet.push_back(curRet);
            return;
        }
        //这里是在判断这一行的每一列
        for(int i = 0;i<n;++i)
        {
            //这个Valid的作用是判断是否当前的这一行中的第i个位置没有和当前这一轮结果中皇后的位置造成冲突
            if(isValid(curRow,i,curRet))
            {
                curRet.push_back(make_pair(curRow,i));
                DFS(allRet,curRet,curRow+1,n);
                curRet.pop_back();
            }
        }
    }

    bool isValid(int row,int col,vector<pair<int,int>>& curRet)
    {
        //表示这个坐标是合法的,可以防止皇后的条件是:
        //首先不能同一行,也不能同一列,还不能同一上斜线,也不能下斜线
        //仔细分析就回发现,肯定在下一行选的坐标,所以不需要在判断是否为同一行,但是是否在同一列还是需要进行判断的
        //其次就是如果是同一上斜线的,你会发现他们的坐标值相加是一样的
        //如果是同一下斜线,那么他们的坐标值相减是一样的
        for(pair<int,int>& e : curRet)
        {
            if(col == e.second || e.first + e.second == row + col || e.first - e.second == row - col)
                return false;
        }
        return true;
    }
    int totalNQueens(int n) {
        vector<vector<pair<int,int>>> allRet;
        vector<pair<int,int>> curRet; //此时这里面存放的是当前的皇后坐标情况
        DFS(allRet,curRet,0,n);
        return allRet.size();
    }
};

5.3 LeetCode第37题—解数独

回溯算法OJ_第23张图片
题解:对于解数独的判断条件就是,行不能有相同,列不能有相同,并且在一个九方格中也不能有相同,但是如何求得当前这个数是哪一个九方格的代码还需要好好的思考一下。

class Solution {
public:
    bool DFS(vector<vector<char>>& board)
    {
        for(int i = 0;i<board.size();++i)
        {
            for(int j = 0;j<board[0].size();++j)
            {
                if(board[i][j] != '.')
                    continue;
                for(char k = '1';k<='9';++k)
                {
                    if(Isvalid(i,j,k,board))
                    {
                        board[i][j] = k;
                        if(DFS(board))
                            return true;
                        board[i][j] = '.';
                    }
                }
                return false;
            }
        }
        return true;
    }
    bool Isvalid(int row,int col,char k,vector<vector<char>>& board)
    {
        //确保当前行不在有重复的
        for(int i = 0;i<9;++i)
        {
            if(board[row][i] == k)
                return false;
        }
        //确保当前列不再有重复的
        for(int j = 0;j<9;++j)
        {
            if(board[j][col] == k)
                return false;
        }
        
        //这里就是如何确保在同一个九方格内不会有重复的数字
        int startRow = (row / 3) * 3;
        int startCol = (col / 3) * 3;
        for (int i = startRow; i < startRow + 3; i++)
        { 
            //判断9方格里是否重复
            for (int j = startCol; j < startCol + 3; j++) 
            {
                if (board[i][j] == k)
                {
                    return false;
                }
            }
        }
        return true;
    }

    //解熟读我们能不能使用同样的N皇后方式来解决呢?
    void solveSudoku(vector<vector<char>>& board) {
        DFS(board);
    }
};

5. 分割问题

5.1 LeetCode第131题—分割回文串

回溯算法OJ_第24张图片
回溯算法OJ_第25张图片

class Solution {
public:
    void DFS(string s,vector<vector<string>>& vv,vector<string>& v,int startindex)
    {
        if(startindex == s.size())
        {
            vv.push_back(v);
            return;
        }
        //这个i可以形容为切割的位置
        //"a","aa","aab"
        for(int i = startindex;i<s.size();++i)
        {
            if(IsPalindrome(s,startindex,i))
            {
                //截取字符串这个地方还得好好在想一下,我感觉再来一遍我可能也会卡在这个地方
                //仔细想一下0,0肯定是不可以的
                string str = s.substr(startindex,i-startindex+1);
                v.push_back(str);
                DFS(s,vv,v,i+1);
                v.pop_back();
            }
            else
            {
                continue;
            }
        }
    }
    //如何判断回文
    bool IsPalindrome(string& s,int start,int end)
    {
        for(int i = start,j = end;i<j;i++,j--)
        {
            if(s[i] != s[j])
                return false;
        }
        return true;
    }
    //对于DFS中的for循环选择切割位置,第一个是"a",第二个是"aa"处切割,第三次是在"aab"处切割一次
    vector<vector<string>> partition(string s) {
        vector<vector<string>> vv;
        vector<string> v;
        DFS(s,vv,v,0);
        return vv;
    }
};

5.2 LeetCode第93题—复原IP地址

回溯算法OJ_第26张图片

回溯算法OJ_第27张图片

class Solution {
public:
    void DFS(string s,vector<string>& v,int startindex,int pointNum)
    {
        if(pointNum == 3)
        {
            //当此时.的个数达到3的时候,说明已经将该字符串分割为4部分了
            if(Isvalid(s,startindex,s.size()-1))
                v.push_back(s);
            //因为不管你满足不满足都是需要返回的
            return;
        }
        for(int i = startindex;i<s.size();++i)
        {
            if(Isvalid(s,startindex,i))
            {
                //如果从这个坐标它是合法的,我们应该在这个后面加入一个.
                s.insert(s.begin()+i+1,'.'); //这一步肯定是由问题的
                pointNum++;
                DFS(s,v,i+2,pointNum); //因为中间有一个.所以要跳过这个
                pointNum--;
                s.erase(s.begin()+i+1);
            }
            else
            {
                //如果发现此时切割的这一段是不合法的呢?
                break;//此时就不需要在递归下去了,因为剩下的都不会满足的
            }
        }
    }
    bool Isvalid(string& s,int start,int end)
    {
        //这一步判断应该怎样思考呢?感觉再来一次我在这里思考还是会打住
        if(start > end)
            return false;
        //首先就是前面不饿能含前导0,但是你仔细看测试用例不是有吗
        if(s[start] == '0' && start != end)
            return false;
        //不能够含有非法字符,必须都存在于字符0-9之间内
        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;
    }
    //对于这道题来说相对于上面的字符串切割,在判断条件上面有不一样
    vector<string> restoreIpAddresses(string s) {
        //这道题呢就很好的使用到了回溯算法,到底什么是剪枝,对于这个名词已经见过很多次了,能不能研究一下
        vector<string> v;
        int pointNum = 0;
        DFS(s,v,0,pointNum);
        return v;
    }
};

6. 普通回溯法

6.1 剑指offer12题—矩阵中的路径

链接: https://leetcode-cn.com/problems/ju-zhen-zhong-de-lu-jing-lcof/
回溯算法OJ_第28张图片

class Solution {
public:
    //这道题的标准解法是回溯法,做到这里也就说明了回溯法还是做得有问题,需要继续好好学习
    bool DFS(int i,int j,int row,int col,vector<vector<char>>& board, string word,vector<vector<int>>& visited,int k)
    {
        if(i < 0 || i >= row || j <0 || j >= col || board[i][j] != word[k] || visited[i][j] == 1)
            return false;
        //此时说明字符串已经完全匹配上了
        if(k == word.size()-1)
            return true;
        visited[i][j] = 1; //表示这个位置已经被访问过了
        //只需要任意一个满足即可,但是如果此时又多个位置都满足,但是第一个位置递归进去,后面发现是不合适的,所以此时需要一个返回值,所以后面才需要ret
        bool ret = DFS(i+1,j,row,col,board,word,visited,k+1) || DFS(i,j+1,row,col,board,word,visited,k+1) ||
        DFS(i-1,j,row,col,board,word,visited,k+1) || DFS(i,j-1,row,col,board,word,visited,k+1);
        //对于回溯一定要有回退的这一步
        visited[i][j] = 0;
        return ret;
    }
    bool exist(vector<vector<char>>& board, string word) {
        int row = board.size();
        int col = board[0].size();
        vector<vector<int>> visited(row,vector<int>(col,0));
        for(int i = 0;i<row;++i)
        {
            for(int j = 0;j<col;++j)
            {
                if(board[i][j] == word[0])
                {
                    //以第一个字符当做进入DFS的条件
                    if(DFS(i,j,row,col,board,word,visited,0))
                        return  true;
                }
            }
        }
        return false;
    }
};

6.2 二叉树路径问题

二叉树路径的问题大致可以分为两类

  1. 自顶向下
    顾名思义,就是从某一个节点(不一定是根节点),从上向下寻找路径,到某一个节点(不一定是叶节点)结束,而继续细分的话还可以分成一般路径给定和的路径
  2. 非自顶向下
    就是从任意节点到任意节点的路径,不需要自顶向下
  • 模板
    回溯算法OJ_第29张图片
    回溯算法OJ_第30张图片

自顶向下题目

6.2.1 LeetCode第257题—二叉树的所有路径

LeetCode题目链接:https://leetcode-cn.com/problems/binary-tree-paths/
回溯算法OJ_第31张图片

class Solution {
public:
    void construct_paths(TreeNode* root,string path,vector<string>& paths)
    {
        if(root == nullptr)
            return;
        path += to_string(root->val);
        if(root->left == nullptr && root->right == nullptr)
        {
            paths.push_back(path);
            return;
        }
        else
        {
            path += "->";
            construct_paths(root->left,path,paths);
            construct_paths(root->right,path,paths);
        }
    }
    vector<string> binaryTreePaths(TreeNode* root) {
        //这就是一道考察二叉树遍历的一道题
        vector<string> paths;
        string path;
        construct_paths(root,path,paths);
        return paths;
    }
};

6.2.2 LeetCode第113题—路径总和II

LeetCode题目链接:https://leetcode-cn.com/problems/path-sum-ii/
回溯算法OJ_第32张图片

class Solution {
public:
    vector<vector<int>> ret;
    vector<int> path;

    void dfs(TreeNode* root, int targetSum) {
        if (root == nullptr) {
            return;
        }
        path.emplace_back(root->val);
        targetSum -= root->val;
        if (root->left == nullptr && root->right == nullptr && targetSum == 0) {
            ret.emplace_back(path);
        }
        dfs(root->left, targetSum);
        dfs(root->right, targetSum);
        path.pop_back();
        return;
    }

    vector<vector<int>> pathSum(TreeNode* root, int targetSum) {
        dfs(root, targetSum);
        return ret;
    }
};

更加通俗的回溯方法

class Solution {
public:
    void helper(TreeNode* cur,int sum,vector<int>& v,vector<vector<int>>& vv)
    {
        if(cur->left == nullptr && cur->right == nullptr && sum == 0)
        {
            vv.push_back(v);
            return;
        }
        if(cur->left == nullptr && cur->right == nullptr)
            return;   
        if(cur->left)
        {
            v.push_back(cur->left->val);
            sum -= cur->left->val;
            helper(cur->left,sum,v,vv);
            sum += cur->left->val;
            v.pop_back();
        }

        if(cur->right)
        {
            v.push_back(cur->right->val);
            sum -= cur->right->val;
            helper(cur->right,sum,v,vv);
            sum += cur->right->val;
            v.pop_back();
        }
        return ;
    }
    vector<vector<int>> pathSum(TreeNode* root, int targetSum)
    {
        vector<vector<int>> vv;
        vector<int> v;
        if(root == nullptr)
            return vv;
        //这道题其实就是上一道题的一点点延申,我真的服我自己了
        v.push_back(root->val);
        helper(root,targetSum-root->val,v,vv);
        return vv;
    }
};

6.2.3 剑指offer34题—二叉树和为某一值的路径(和上一题一样)

链接:https://leetcode-cn.com/problems/er-cha-shu-zhong-he-wei-mou-yi-zhi-de-lu-jing-lcof/

回溯算法OJ_第33张图片
题解:这道题的解法其实是类似于二叉树的前序遍历的递归写法的,只是在这个前序遍历的过程中增加了几个判断条件。

class Solution {
public:
    //回溯法可以解决这个问题
    void DFS(TreeNode* root,int target,vector<vector<int>>& vv,vector<int>& v)
    {
        if(root == nullptr)
            return;
        v.push_back(root->val);
        target -= root->val;
        //也就是题目中所说的叶子结点的条件
        if(root->left == nullptr && root->right == nullptr && target == 0)
        {
            //说明此时这个就是我要找的路径
            vv.push_back(v);
            return;
        }
        DFS(root->left,target,vv,v);
        DFS(root->right,target,vv,v);
        //如果走到这里,所以当前的这个结点适不适合的,不满足条件的,所以应该回溯
        v.pop_back();
    }
    vector<vector<int>> pathSum(TreeNode* root, int target) {
        vector<vector<int>> vv;
        vector<int> v;
        DFS(root,target,vv,v);
        return vv;
    }
};

6.2.4 LeetCode第437题—路径总和III

LeetCode题目链接:https://leetcode-cn.com/problems/path-sum-iii/
回溯算法OJ_第34张图片
解题思路:定义一个全局变量count来记录符合targetSum路径的个数,首先从根节点root调用DFS函数求得从根节点出发符合的路径个数,然后在递归到左子树右子树,最终求得总的路径数量。

class Solution {
public:
    int count = 0;
    //双重递归:先调用dfs函数从root开始查找路径,再调用pathsum函数到root左右子树开始查找
    void DFS(TreeNode* root,int sum)
    {
        if(root == nullptr)
            return;
        sum -= root->val;
        //这里根本不需要那么多的判断条件
        if(sum == 0)
            //这个count千万不敢返回
            count++;
        DFS(root->left,sum);
        DFS(root->right,sum);
    }
    //此时明确的可以知道是不需要经过根节点的
    int pathSum(TreeNode* root, int targetSum) {
        //此时我们知道了以root节点为根所求targetSum的数量,那么我们想要求得所有的路径总数
        //这里的返回条件应该些什么?
        if(root == nullptr)
            return 0;
        DFS(root,targetSum); //以根为起点查找路径
        pathSum(root->left,targetSum); //递归到左树查找路径
        pathSum(root->right,targetSum); // 递归到右树查找路径
        return count;
    }
};

6.2.5 LeetCode第112题—路径总和

LeetCode题目链接:https://leetcode-cn.com/problems/path-sum/
回溯算法OJ_第35张图片
回溯算法OJ_第36张图片
普通回溯法

class Solution {
public:
    bool helper(TreeNode* cur,int sum)
    {
        if(cur->left == nullptr && cur->right == nullptr && sum == 0)
            return true;
        if(cur->left == nullptr && cur->right == nullptr)
            return false;
        if(cur->left)
        {
            sum -= cur->left->val;
            if(helper(cur->left,sum))
                return true;
            sum += cur->left->val;
        }

        if(cur->right)
        {
            sum -= cur->right->val;
            if(helper(cur->right,sum))
                return true;
            sum += cur->right->val;
        }
        //返回这个false才是这道题其实最精髓的点
        return false; 
    }
    //我发现这是二叉树最喜欢的一类题型就是判断路径和的问题
    //在这里我们使用回溯法,然后我们使用剑法的方式更容易去判断
    bool hasPathSum(TreeNode* root, int targetSum) {
        //其实测试用例3已经说明了情况
        if(root == nullptr)
            return false;
        return helper(root,targetSum-root->val);
    }
};

通用递归解法:

class Solution {
public:
    bool DFS(TreeNode* root,int sum)
    {
        if(root == nullptr)
            return false;
        sum -= root->val;
        if(root->left == nullptr && root->right == nullptr && sum == 0)
            return true;
        return DFS(root->left,sum) || DFS(root->right,sum);
    }
    //我发现这是二叉树最喜欢的一类题型就是判断路径和的问题
    //在这里我们使用回溯法,然后我们使用剑法的方式更容易去判断
    bool hasPathSum(TreeNode* root, int targetSum) {
        //其实测试用例3已经说明了情况
        if(root == nullptr)
            return false;
        return DFS(root,targetSum);
    }
};

6.2.6 LeetCode第988题—从叶子节点开始的最小字符串

LeetCode题目链接:https://leetcode-cn.com/problems/smallest-string-starting-from-leaf/
回溯算法OJ_第37张图片

class Solution {
public:
    void DFS(TreeNode* root,string s,vector<string>& path)
    {
        if(root == nullptr) 
            return;
        s += ('a' + root->val);
        if(root->left == nullptr && root->right == nullptr)
        {
            reverse(s.begin(),s.end());
            path.push_back(s);
            return;
        }
        DFS(root->left,s,path);
        DFS(root->right,s,path);
    }
    //0-a 1-b 2-c 3-d
    //其实根本不需要考虑那么多,我们为什么会在这里被卡住呢?
    string smallestFromLeaf(TreeNode* root) {
        vector<string> path;
        string s;
        if(root == nullptr)
            return s;
        DFS(root,s,path);
        sort(path.begin(),path.end());
        return path[0];
    }
};

非自顶向下
就是从任意节点到任意节点的路径,不需要自顶向下

  • 模板
    回溯算法OJ_第38张图片

6.3.1 LeetCode第543题—二叉树的直径

LeetCode题目链接:https://leetcode-cn.com/problems/diameter-of-binary-tree/
回溯算法OJ_第39张图片
对于直径来说,一定是两个节点之间的距离,所以判断条件应该是两个
回溯算法OJ_第40张图片

class Solution {
public:
    int res = INT_MIN;
    int diameterOfBinaryTree(TreeNode* root) {
        if(root == nullptr || (root->left == nullptr && root->right == nullptr))
            return 0;
        MaxPath(root);
        return res;
    }

    int MaxPath(TreeNode* root)
    {
        if(root == nullptr)
            return 0;
        int left = MaxPath(root->left);
        int right = MaxPath(root->right);
        //为啥这里不加1了呢?
        res = std::max(res,left + right);
        return 1 + std::max(left,right);
    }
};

6.3.2 LeetCode第124题—二叉树中的最大路径和

LeetCode题目链接:https://leetcode-cn.com/problems/binary-tree-maximum-path-sum/
回溯算法OJ_第41张图片

class Solution {
public:
    int res = INT_MIN;
    int maxPathSum(TreeNode* root) {
        if(root == nullptr)
            return 0;
        Maxpath(root);
        return res;
    }

    int Maxpath(TreeNode* root)
    {
        if(root == nullptr)
            return 0;
        //递归去算以根节点的左子树的最大路径和与右子树的最大路径和
        //因为左边和右边的最大路径和是有可能为负数的,那么就不能够给总的路径贡献,所以直接要抛弃掉这一路
        int left = std::max(Maxpath(root->left),0);
        int right = std::max(Maxpath(root->right),0);
        res = std::max(res,root->val + left + right);//每次去更新全局的最大路径和
        //但是对于每次递归是要有一个返回值的,让他不断地往上层去迭代
        //既然是路径和,那么肯定是左右中的一条
        return root->val + std::max(left,right);
    }
};

你可能感兴趣的:(回溯算法,算法,c++)