找工作准备刷题Day13 回溯算法 (卡尔41期训练营 7.27)

第一题:Leetcode 491. 非递减子序列

题目描述

解题思路

  1. 由于 nums 中可能存在重复元素,因此在某一层访问时,需要记录某个数字是否已经被使用(这是在同层中剪枝)
  2. 一般来说:组合问题和排列问题是在树形结构的叶子节点上收集结果,而子集问题就是取树上所有节点的结果

题解

class Solution {
public:
    vector<vector<int>> ans;
    vector<int> path;
    vector<vector<int>> findSubsequences(vector<int>& nums) {
        backTracking(nums, 0);
        return ans;
    }

    void backTracking(const vector<int>& nums, int startIdx) {
        if (path.size() >= 2)
            ans.push_back(path);

        if (startIdx >= nums.size())
            return;

        vector<int> uesed(201, 0); // 代表本层是否用过
        for (int i = startIdx; i < nums.size(); i++) {
            if ((path.size() > 0 && nums[i] < path[path.size() - 1]) ||
                uesed[nums[i] + 100] == 1)
                continue;

            uesed[nums[i] + 100] = 1;
            path.push_back(nums[i]);
            backTracking(nums, i + 1);
            path.pop_back();
        }
    }
};
  1. uesed是表示本层是否用过某个数,不用传递到下一层。此处使用 vector 是因为数字位于 [-100,100]。如果不限制范围,则需要使用 unordered_set ,使用 insert 和 find 函数来实现。

第二题:Leetcode46. 全排列

题目描述

给定一个不含重复数字的数组 nums ,返回其 所有可能的全排列 。你可以 按任意顺序 返回答案。

题解

class Solution {
public:
    vector<vector<int>> ans;
    vector<int> path;
    vector<vector<int>> permute(vector<int>& nums) {
        vector<bool> used(nums.size(), false);
        backTracking(nums, used);

        return ans;
    }

    void backTracking(const vector<int>& nums, vector<bool>& used) {
        if (path.size() == nums.size()) {
            ans.push_back(path);
            return;
        }

        for (int i = 0; i < nums.size(); i++) {
            if (used[i] == true)
                continue;
            path.push_back(nums[i]);
            used[i] = true;
            backTracking(nums, used);
            path.pop_back();
            used[i] = false;
        }
    }
};
  1. 排列组合问题,因此在叶子结点收集结果;
  2. nums 没有重复元素,因此不用进行同层剪枝。

第三题:Leetcode47. 全排列 II

题目描述

给定一个可包含重复数字的序列 nums ,按任意顺序 返回所有不重复的全排列。

题解

class Solution {
public:
    vector<vector<int>> ans;
    vector<int> path;
    vector<vector<int>> permuteUnique(vector<int>& nums) {
        sort(nums.begin(), nums.end());
        vector<bool> used(nums.size(), false);
        backTracking(nums, used);

        return ans;
    }

    void backTracking(const vector<int>& nums, vector<bool>& used) {
        if (path.size() == nums.size()) {
            ans.push_back(path);
            return;
        }

        for (int i = 0; i < nums.size(); i++) {
            if (used[i] == true)
                continue;

            if (i > 0 && nums[i] == nums[i - 1] && used[i - 1] == false)
                continue;

            path.push_back(nums[i]);
            used[i] = true;
            backTracking(nums, used);
            path.pop_back();
            used[i] = false;
        }
    }
};

由于存在重复元素,因此首先进行排序,然后使用

if (i > 0 && nums[i] == nums[i - 1] && used[i - 1] == false)
                continue;

进行同层剪枝。

第四题:Leetcode332. 重新安排行程

题目描述

解题思路

此题重点在于:需要返回字典序顺序最小的结果,从“JKF”机场出发。因此,可以以字典序从小到大的顺序依次乘坐以JKF为出发机场的机票。

那么现在问题是:用什么数据结果存储飞机票,可以指定出发机场 + 可以以字典序顺序 访问

答案一:

unordered_map<string, multiset<string>> tickit_times

答案二:

unordered_map<string, map<string, int>> tickit_times

答案二更好,原因是:坐过的机票需要“撕毁”,第二种只需要将 int -- 即可,而第一种需要删除键,回溯时需要再insert,会导致迭代器失效。

tickit_times[city1][city2]表示从 city1 到 city2 的直达航班数(在乘坐过后需要--,回溯后++)

题解

class Solution {
public:
    unordered_map<string, map<string, int>> tickit_times;
    vector<string> findItinerary(vector<vector<string>>& tickets) {
        vector<string> ans;
        tickit_times.clear();
        // tickit_times记录从fly[0]-> fly[1] 的次数
        for (auto fly : tickets)
            tickit_times[fly[0]][fly[1]]++;

        ans.push_back("JFK");
        backTracking(tickets.size(), ans);
        return ans;
    }

    bool backTracking(const size_t ticketNum, vector<string>& ans) {
        // 四场票需要ans中有五个机场
        if (ans.size() == ticketNum + 1)
            return true;

        // 访问按照字典升序进行,结果自然是字典序最小的结果——map性质
        // 此处重点:flight需要作为引用,否则无法对航班数进行--,会导致重复飞行。
        for (auto& flight : tickit_times[ans.back()) {
            if (flight.second > 0) {
                flight.second--;
                ans.push_back(flight.first);
                // 此处需要剪枝,其实有点贪心算法的意思
                if (backTracking(ticketNum, ans))
                    return true;
                flight.second++;
                ans.pop_back();
            }
        }

        return false;
    }
};

这里的重点是,在遍历的时候,需要使用引用。

for (auto& flight : tickit_times[ans.back())

由于已经是字典序升序访问,因此在这一枝找到结果以后,就直接返回,无需回溯。

第五题:Leetcode 51.N皇后

解题思路

  1. 按行放置皇后——因此不存在同行冲突;
  2. 使用isAvailable[i]表示i列是否已经放置皇后——因此避免同列冲突;
  3. isValidPosition(const vector<string> board, const int row, const int col)

    函数用于判断在 (row,col)位置放置皇后是否会存在斜线冲突。

题解

class Solution {
public:
    vector<vector<string>> solveNQueens(int n) {
        vector<vector<string>> ans;

        vector<string> board(n, string(n, '.'));
        vector<bool> isAvailable(n, true);

        backTrackingNQueen(board, isAvailable, 0, ans);

        return ans;
    }

    // toPlaceRowIdx 代表即将放置的行数
    // isAvailable[col]表示第i列已经放置过了
    void backTrackingNQueen(vector<string>& board, vector<bool>& isAvailable,
                            int toPlaceRowIdx, vector<vector<string>>& ans) {
        if (toPlaceRowIdx == board.size()) {
            ans.push_back(board);
            return;
        }

        for (int col = 0; col < board.size(); col++) {
            // 如果 col 这一列还没有放置
            // 且 toPlaceRowIdx,col 放置合法
            if (isAvailable[col] &&
                isValidPosition(board, toPlaceRowIdx, col)) {
                board[toPlaceRowIdx][col] = 'Q';
                isAvailable[col] = false;

                backTrackingNQueen(board, isAvailable, toPlaceRowIdx + 1,ans);

                board[toPlaceRowIdx][col] = '.';
                isAvailable[col] = true;
            }
        }
    }

    bool isValidPosition(const vector<string> board, const int row,
                         const int col) {
        const int n = board.size();
        int step_r = -1, step_c = -1;
        int r = row + step_r, c = col + step_c;
        while (r >= 0 && r < n && c >= 0 && c < n) {
            if (board[r][c] == 'Q')
                return false;
            r += step_r, c += step_c;
        }

        step_r = -1, step_c = 1;
        r = row + step_r, c = col + step_c;
        while (r >= 0 && r < n && c >= 0 && c < n) {
            if (board[r][c] == 'Q')
                return false;
            r += step_r, c += step_c;
        }

        return true;
    }
};

第六题:Leetcode37.解数独

解题思路

仿照人类思维,一个一个填。

使用 toFill 表示从何处填起,然后使用 toFill 计算 row和col。

题解

class Solution {
public:
    void solveSudoku(vector<vector<char>>& board) { backTracking(board, 0); }

    bool backTracking(vector<vector<char>>& board, int toFill) {
        for (int n = toFill; n < 81; n++) {
            int r = n / 9;
            int c = n % 9;
            if (board[r][c] == '.') {
                // 从 1-9 挨个试
                for (char i = '1'; i <= '9'; i++) {
                    if (isValid(board, r, c, i)) {
                        board[r][c] = i;
                        if (backTracking(board, n + 1))
                            return true;
                        board[r][c] = '.';
                    }
                }

                return false;
            }
        }

        return true;
    }

    bool isValid(const vector<vector<char>>& board, const int row,
                 const int col, const char num) {

        for (int i = 0; i < 9; i++) {
            if (board[row][i] == num || board[i][col] == num)
                return false;
        }

        const int startRow = (row / 3) * 3;
        const int startCol = (col / 3) * 3;
        for (int r = startRow; r < startRow + 3; r++) {
            for (int c = startCol; c < startCol + 3; c++) {
                if (board[r][c] == num)
                    return false;
            }
        }

        return true;
    }
};

要点

由于题目保证只有一个解,因此在这一枝已经找到返回合适值的时候,无需继续回溯。

  • 25
    点赞
  • 14
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值