打卡回溯算法2

回溯法就是暴力搜索,并不是什么高效的算法,最多在剪枝一下。

回溯算法能解决如下问题:

  • 组合问题:N个数里面按一定规则找出k个数的集合
  • 排列问题:N个数按一定规则全排列,有几种排列方式
  • 切割问题:一个字符串按一定规则有几种切割方式
  • 子集问题:一个N个数的集合里有多少符合条件的子集
  • 棋盘问题:N皇后,解数独等等
void backtracking(参数) {
    if (终止条件) {
        存放结果;
        return;
    }

    for (选择:本层集合中元素(树中节点孩子的数量就是集合的大小)) {
        处理节点;
        backtracking(路径,选择列表); // 递归
        回溯,撤销处理结果
  

组合问题:组合问题,如从n个元素中选取k个元素的所有可能组合,可以视为一种树形搜索问题。树的每一层代表一个选择点,从n个元素中选择或不选择某个元素,然后递归到下一层。

StartIndex:在组合问题中,为了避免产生重复的组合,我们需要在每一层递归中从某个特定的位置开始遍历元素。startIndex 参数就是用来控制这一点的。在上一层选择了一个元素后,下一层应该从这个元素后面的位置开始遍历,以确保不会再次选择到它,从而产生重复的组合。

难点1 :去重问题:树枝去重和树层去重

构造了一个used数组,每层使用就标记1

(1)树枝去重:树枝去重是指在一个分支(或称为“树枝”)上,对于已经选择过的元素,不再进行选择。这通常通过一个标记数组( used 数组)来实现。而used数组,其实就是记录此时path里都有哪些元素使用了,一个排列里一个元素只能使用一次。当 used[i] == true 时,说明在当前分支(或路径)上,candidates[i] 这个元素已经被选择过了,因此不应该再次选择它。

(2)树层去重:

used数组去重版本:树层去重的话,需要对数组排序! (需要树层),有同层的重复的元素

使用set去重的版本如下

used数组去重版本:
// 而我们要对同一树层使用过的元素进行跳过
for (int i = startIndex; i < nums.size(); i++) {
     // used[i - 1] == true,说明同一树枝candidates[i - 1]使用过
     // used[i - 1] == false,说明同一树层candidates[i - 1]使用过
     // 而我们要对同一树层使用过的元素进行跳过
     if (i > 0 && nums[i] == nums[i - 1] && used[i - 1] == false) {
        continue;
     }
     path.push_back(nums[i]);
     used[i] = true;
     backtracking(nums, i + 1, used);
     used[i] = false;
     path.pop_back();
  }

使用set去重的版本如下:
unordered_set<int> uset; // 定义set对同一节点下的本层去重
for (int i = startIndex; i < nums.size(); i++) {
    if (uset.find(nums[i]) != uset.end()) { // 如果发现出现过就pass
         continue;
    }
    uset.insert(nums[i]); // set跟新元素
    path.push_back(nums[i]);
    backtracking(nums, i + 1, used);
    path.pop_back();
 

332.重新安排行程

好难理解

回溯法来找到一条从JFK出发并且使用所有机票一次且仅一次的行程。我们确实需要一个映射关系来跟踪从每个机场可以到达的下一个机场。

class Solution {  
private:  
    // 定义一个unordered_map,键是出发机场,值是一个map,该map的键是到达机场,值是航班次数  
    // 用来存储每个出发机场可以到达的机场以及相应的航班次数  
    // unordered_map<出发机场, map<到达机场, 航班次数>> targets  
    unordered_map<string, map<string, int>> targets;  
  
    // 回溯函数,用于搜索可能的行程  
    // ticketNum: 总共的机票数量  
    // result: 当前构建的行程,它是一个vector<string>,存储了已经走过的机场  
    bool backtracking(int ticketNum, vector<string>& result) {  
        // 如果当前行程的机场数量等于机票数量+1(因为起始机场已经加进去了),说明找到了一个有效的行程  
        if (result.size() == ticketNum + 1) {  
            return true;  
        }  
  
        // 遍历当前机场可以到达的所有机场  
        for (pair<const string, int>& target : targets[result[result.size() - 1]]) {  
            // 如果该到达机场的航班次数大于0,说明还有可用的航班  
            if (target.second > 0 ) { // 记录到达机场是否飞过了(这里实际上是通过减少航班次数来“标记”飞过了)  
                // 将该到达机场加入当前行程  
                result.push_back(target.first);  
  
                // 标记该航班已经被使用过(减少航班次数)  
                target.second--;  
  
                // 递归调用backtracking,继续搜索下一个机场  
                if (backtracking(ticketNum, result)) return true;  
  
                // 如果递归调用返回false,说明从当前机场出发无法找到有效的行程  
                // 回溯:从当前行程中移除最后一个机场,并恢复该航班的次数  
                result.pop_back();  
                target.second++;  
            }  
        }  
  
        // 所有可能的下一个机场都尝试过了,没有找到有效的行程  
        return false;  
    }  
  
public:  
    // 主函数,用于构建并返回一个有效的行程  
    vector<string> findItinerary(vector<vector<string>>& tickets) {  
        // 清空targets,确保每次调用findItinerary时都是新的开始  
        targets.clear();  
  
        // result用于存储构建的行程  
        vector<string> result;  
  
        // 遍历所有机票,构建targets映射关系  
        for (const vector<string>& vec : tickets) {  
            targets[vec[0]][vec[1]]++; // 记录每个出发机场可以到达的机场以及航班次数  
        }  
  
        // 起始机场是JFK,加入result  
        result.push_back("JFK");  
  
        // 调用回溯函数,搜索有效的行程  
        backtracking(tickets.size(), result);  
  
        // 返回结果  
        return result;  
    }  
};

51. N皇后

n皇后问题是一个很好的例子来展示回溯算法在解决二维矩阵和约束条件下的搜索问题时的应用。

初始化:

std::vector<std::string> chessboard(n, std::string(n, '.'));

需要检查每一行、每一列以及两个对角线方向,以确保新放置的皇后不会与已经放置的皇后冲突。在检查这些条件时,你会遍历之前的行(因为当前行之前的皇后位置已经确定),并且检查对应的列和对角线位置是否已经被其他皇后占据。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值