我是靠谱客的博主 虚幻烧鹅,这篇文章主要介绍高级数据结构与算法 | 回溯算法(Back Tracking Method)回溯,现在分享给大家,希望可以做个参考。

文章目录

  • 回溯
    • 电话号码的字母组合
      • 二进制手表
    • 组合总数
    • 全排列
      • 活字印刷
    • N皇后
      • N皇后II


回溯

回溯是一种通过穷举所有可能情况来找到所有解的算法。如果一个候选解最后被发现并不是可行解,回溯算法会舍弃它,并在前面的一些步骤做出一些修改,并重新尝试找到可行解。

当探索到某一步时,发现原先选择并不优或 达不到目标,就退回一步重新选择,这种走不通就退回再走的技术为回溯法,而满足回溯条件的某个状态的点称为“回溯点”。也可以称为剪枝点,所谓的剪枝,指的是把不会找到目标,或者不必要的路径裁剪掉。

从上面看出,回溯其实就是选优搜索法,按照深度优先搜索的方式进行搜索,再通过剪枝来避免不相关结果。

//模板
回溯()
{
    if(满足结束条件)
    {
        将结果加入结果集中
        return;
    }
    
    for(当前选择 : 选择列表)
    {
        1.完成这一步的选择
        2.回溯(下一步), 进行下一步的选择
        3.撤销选择,继续进行其他尝试
    }
}

电话号码的字母组合

给定一个仅包含数字 2-9 的字符串,返回所有它能表示的字母组合。

给出数字到字母的映射如下(与电话按键相同)。注意 1 不对应任何字母。

来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/letter-combinations-of-a-phone-number/

DFS + 回溯,如果当前组合行不通则回溯到上一步,然后继续尝试其他组合。

class Solution {
public:
    vector<string> numToStr = {"", "", "abc", "def", "ghi", "jkl", "mno", "pqrs", "tuv", "wxyz"};    //数字——字母映射
    void DFS(vector<string>& res, string& digits, string cur, int index)
    {
        //此时数字已遍历结束
        if(index == digits.size())
        {
            //如果组合不为空,则加入结果集中
            if(!cur.empty())
            {
                res.push_back(cur);
                return;
            }
        }

        //搜索当前所有可能的组合
        for(auto ch : numToStr[digits[index] - '0'])
        {
            cur.push_back(ch);
            DFS(res, digits, cur, index + 1);
            //回溯到上一步的状态
            cur.pop_back();
        }
    }

    vector<string> letterCombinations(string digits) {
        if(digits.empty())
        {
            return {};
        }
        
        vector<string> res;
        DFS(res, digits, "", 0);

        return res;
    }
};

二进制手表

二进制手表顶部有 4 个 LED 代表 小时(0-11),底部的 6 个 LED 代表 分钟(0-59)。

每个 LED 代表一个 0 或 1,最低位在右侧。

在这里插入图片描述

例如,上面的二进制手表读取 “3:25”。

给定一个非负整数 n 代表当前 LED 亮着的数量,返回所有可能的时间。

来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/binary-watch

和上题思路一样,不过变为了组合二进制。

class Solution {
public:
  
    vector<int> nToTime = {1, 2, 4, 8, 1, 2, 4, 8, 16, 32};
    void DFS(vector<string>& res, int num, int index, pair<int, int>& time)
    {
        //如果没有剩余的灯,就返回
        if(num == 0)
        {
            if(time.first > 11 || time.second > 59)
            {
                return;
            }
            string cur = (to_string(time.first) + ":");
            if(time.second < 10)    //小于10要补0
            {
                cur += '0';
            }
            cur += to_string(time.second);
            res.push_back(cur); //组合结果
 
            return;
        }
		
        //尝试当前每一种组合
        for(int i = index; i < 10; i++)
        {
            //超出范围,舍弃该数据
            if(time.first > 11 || time.second > 59)
            {
                continue;
            }

            pair<int, int> backUp = time;
            //前四个灯为小时
            if(i < 4)
            {
                time.first += nToTime[i];
            }
            //后六个灯为分钟
            else
            {
                time.second += nToTime[i];
            }
            DFS(res, num - 1, i + 1, time); //进入下一层搜索, 下次从第i + 1栈灯开始
            time = backUp;  //回溯状态
        }
    }

    vector<string> readBinaryWatch(int num) {
        vector<string> res;
        pair<int, int> time(0, 0);
        DFS(res, num, 0, time);

        return res;
    }
};

组合总数

给定一个无重复元素的数组 candidates 和一个目标数 target ,找出 candidates 中所有可以使数字和为 target 的组合。

candidates 中的数字可以无限制重复被选取。

说明:

所有数字(包括 target)都是正整数。
解集不能包含重复的组合。

来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/combination-sum

和上面的思路一样,不过此时由于数据可以无限被选取,所以搜索时下一步的位置还是从当前位置i开始,不需要继续推进。(这道题目也可以当成完全背包,与动态规划题解中的零钱问题II类似)

class Solution {
public:
    void DFS(vector<int>& candidates, vector<vector<int>>& res, vector<int>& cur, int target, int sum, int index)
    {
        //如果大于等于target,则不可再增加
        if(sum >= target)
        {
            //如果和target相等则返回结果集
            if(sum == target)
            {
                res.push_back(cur);
            }
            return;
        }

        //尝试当前各种结果
        for(int i = index; i < candidates.size(); i++)
        {
            //如果当前元素比target大,则不可能成立,直接跳过
            if(candidates[i] > target)
            {
                continue;
            }
            cur.push_back(candidates[i]);
            DFS(candidates, res, cur, target, sum + candidates[i], i);  //由于此时数据可以被无限选取,所以下一步的位置继续从i开始搜索
            cur.pop_back(); //回溯
        }
    }

    vector<vector<int>> combinationSum(vector<int>& candidates, int target) {
        if(candidates.empty())
        {
            return {};
        }

        vector<vector<int>> res;
        vector<int> cur;
        DFS(candidates, res, cur, target, 0, 0);
        return res;
    }
};

全排列

给定一个 没有重复 数字的序列,返回其所有可能的全排列。

示例:

输入: [1,2,3]
输出:
[
[1,2,3],
[1,3,2],
[2,1,3],
[2,3,1],
[3,1,2],
[3,2,1]
]

来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/permutations
著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。

照着模板就可以写出来

class Solution {
public:
    void DFS(vector<vector<int>>& allRet, vector<int>& curRet, vector<int>& nums, vector<bool>& visited)
    {
        //满足条件则加入结果集
        if(curRet.size() == nums.size())
        {
            allRet.push_back(curRet);
        }

        for(int i = 0; i < nums.size(); i++)
        {
            //跳过以访问数字
            if(visited[i] == false)
            {
                continue;
            }

            //记录当前结果
            curRet.push_back(nums[i]);
            visited[i] = false;

            //进行下一步的搜索
            DFS(allRet, curRet, nums, visited);
            
            //回溯
            curRet.pop_back();
            visited[i] = true;
        }
    }

    vector<vector<int>> permute(vector<int>& nums) {
        if(nums.empty())
        {
            return {};
        }

        vector<vector<int>> allRet;
        vector<int> curRet;
        vector<bool> visited(nums.size(), true);
        DFS(allRet, curRet, nums, visited);

        return allRet;
    }
};

活字印刷

你有一套活字字模 tiles,其中每个字模上都刻有一个字母 tiles[i]。返回你可以印出的非空字母序列的数目。

注意:本题中,每个活字字模只能使用一次。

来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/letter-tile-possibilities

全排列的变形,此时变为处理字符串,并且返回任意可以印出的组合

1.当前组合不为空, 则插入set中

2.继续给当前组合拼接新的组合,尝试拼接tiles每一个位置的字符

3.如果当前位置已在组合中出现过,跳过该位置,否则标记当前位置,继续拼接更长的组合

4.回溯,尝试组合其它位置,返回

class Solution {
public:
    void DFS(unordered_set<string>& set, vector<bool>& visited, string& tiles, string cur)
    {
        //结果不为空则记录下来
        if(!cur.empty())
        {
            set.insert(cur);
        }

        for(int i = 0; i < tiles.size(); i++)
        {
            //如果当前字模已经用过, 则跳过本轮
            if(visited[i] == false)
            {
                continue;
            }

            //记录当前位
            cur.push_back(tiles[i]);
            visited[i] = false;
            
            //查找下一位
            DFS(set, visited, tiles, cur);
            
            //回溯, 继续尝试其他组合
            visited[i] = true;
            cur.pop_back();
        }
    }

    int numTilePossibilities(string tiles) {
        if(tiles.empty())
        {
            return 0;
        }

        unordered_set<string> set;  //结果集, 去重
        vector<bool> visited(tiles.size(), true);   //标记矩阵
        DFS(set, visited, tiles, "");

        return set.size();
    }
};

N皇后

n 皇后问题研究的是如何将 n 个皇后放置在 n×n 的棋盘上,并且使皇后彼此之间不能相互攻击。

img

上图为 8 皇后问题的一种解法。

给定一个整数 n,返回所有不同的 n 皇后问题的解决方案。

每一种解法包含一个明确的 n 皇后问题的棋子放置方案,该方案中 ‘Q’ 和 ‘.’ 分别代表了皇后和空位。

来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/n-queens

具体思路看注释,主要就是尝试所有排列组合,并判断行,列,左右对角线是否存在其他皇后,然后将所有符合条件的结果保存下来即可。

class Solution {
public:
    void DFS(vector<vector<pair<int, int>>>& allRes, vector<pair<int, int>>& curRes, int curRow, int n)
    {
        //此时说明已经走完, 记录结果
        if(curRow == n)
        {
            allRes.push_back(curRes);
        }

        for(int i = 0; i < n; i++)
        {
            //如果当前符合规则, 则继续, 如果不符合直接忽略
            if(isVaild(curRes, curRow, i))
            {
                //记录当前行位置
                curRes.push_back(make_pair(curRow, i));
                //查找下一行
                DFS(allRes, curRes, curRow + 1, n);
                //回溯
                curRes.pop_back();
            }
        }
    }

    bool isVaild(vector<pair<int, int>>& curRes, int row, int col)
    {
        //判断当前位置是否合法, 各皇后不能放在同一条直线上
        for(const pair<int, int>& cur : curRes)
        {
            if(cur.first == row 						//判断当前行
            || cur.second == col 						//判断当前列
            || cur.first + cur.second == row + col		//判断左对角线
            || cur.first - cur.second == row - col)		//判断右对角线
            {
                return false;
            }
        }
        return true;
    }
	
    //将坐标结果转换为字符串的棋盘
    vector<vector<string>> resToString(vector<vector<pair<int, int>>>& allRes, int n)
    {
        vector<vector<string>> res;
        for(const vector<pair<int, int>>& cur : allRes)
        {
            vector<string> curRes(n, string(n, '.'));   //初始化棋盘

            for(const pair<int, int>& pos : cur)    //将皇后放入棋盘中
            {
                curRes[pos.first][pos.second] = 'Q';
            }
            res.push_back(curRes);	//保存结果
        }
        return res;
    }

    vector<vector<string>> solveNQueens(int n) {
        vector<vector<pair<int, int>>> allRes;
        vector<pair<int, int>> curRes;
        DFS(allRes, curRes, 0, n);

        return resToString(allRes, n);
    }
};

N皇后II

n 皇后问题研究的是如何将 n 个皇后放置在 n×n 的棋盘上,并且使皇后彼此之间不能相互攻击。

img

上图为 8 皇后问题的一种解法。

给定一个整数 n,返回 n 皇后不同的解决方案的数量。

来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/n-queens-ii

和上题思路一样,因为只需要返回解决方案的数量,所以不再保存结果,对符合条件的情况进行计数即可。

class Solution {
public:
    void DFS(vector<pair<int, int>>& curRes, int curRow, int n, int& count)
    {
        //此时不需要保存结果, 计数就行
        if(curRow == n)
        {
            ++count;
        }

        for(int i = 0; i < n; i++)
        {
            //如果当前符合规则, 则继续, 如果不符合直接忽略
            if(isVaild(curRes, curRow, i))
            {
                //记录当前行位置
                curRes.push_back(make_pair(curRow, i));
                //查找下一行
                DFS(curRes, curRow + 1, n, count);
                //回溯
                curRes.pop_back();
            }
        }
    }

    bool isVaild(vector<pair<int, int>>& curRes, int row, int col)
    {
        //判断当前位置是否合法, 各皇后不能放在同一条直线上
        for(const pair<int, int>& cur : curRes)
        {
            if(cur.first == row 						//判断当前行
            || cur.second == col 						//判断当前列
            || cur.first + cur.second == row + col		//判断左对角线
            || cur.first - cur.second == row - col)		//判断右对角线
            {
                return false;
            }
        }
        return true;
    }

    int totalNQueens(int n) {
        vector<pair<int, int>> curRes;
        int count = 0;
        DFS(curRes, 0, n, count);

        return count;
    }
};

最后

以上就是虚幻烧鹅最近收集整理的关于高级数据结构与算法 | 回溯算法(Back Tracking Method)回溯的全部内容,更多相关高级数据结构与算法内容请搜索靠谱客的其他文章。

本图文内容来源于网友提供,作为学习参考使用,或来自网络收集整理,版权属于原作者所有。
点赞(42)

评论列表共有 0 条评论

立即
投稿
返回
顶部