通关算法题之 ⌈回溯算法⌋

回溯算法

子集组合排列

78. 子集

给你一个整数数组 nums数组中的元素互不相同 ,返回该数组所有可能的子集(幂集)。解集不能包含重复的子集,你可以按任意顺序 返回解集。

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

本质上子集问题就是遍历这样用一棵回溯树:

image-20220611165427827
class Solution {
    
    
public:
    vector<vector<int>> res;
    vector<int> path;
    vector<vector<int>> subsets(vector<int>& nums) {
    
    
        backtrack(nums, 0);
        return res;
    }

    void backtrack(vector<int>& nums, int start){
    
    
        res.push_back(path);
        for(int i = start; i < nums.size(); i++){
    
    
            path.push_back(nums[i]);
            backtrack(nums, i + 1);
            path.pop_back();
        }
    }
};

90. 子集 II

给你一个整数数组nums其中可能包含重复元素,请你返回该数组所有可能的子集(幂集)。解集不能包含重复的子集,返回的解集中,子集可以按任意顺序排列。

输入:nums = [1,2,2]
输出:[[],[1],[1,2],[1,2,2],[2],[2,2]]

这道题目和78.子集的区别就是集合里有重复的元素,而且求取的子集path要去重。需要先进行排序,让相同的元素靠在一起,如果发现nums[i] == nums[i - 1],则跳过。path中加入一个元素,不会再加入相同的元素。

image-20220611173659959
class Solution {
    
    
public:
    vector<vector<int>> res;
    vector<int> path;
    vector<vector<int>> subsetsWithDup(vector<int>& nums) {
    
    
        sort(nums.begin(), nums.end());	// 去重前需要排序
        backtrack(nums, 0);
        return res;
    }

    void backtrack(vector<int>& nums, int start){
    
    
        res.push_back(path);
        for(int i = start; i < nums.size(); i++){
    
    
            if(i > start && nums[i] == nums[i - 1]) continue; // 去重
            path.push_back(nums[i]);
            backtrack(nums, i + 1);
            path.pop_back();
        }
    }
};

77. 组合

给定两个整数 nk,返回范围 [1, n] 中所有可能的 k 个数的组合,你可以按 任何顺序 返回答案。

输入:n = 4, k = 2
输出:
[
  [2,4],
  [3,4],
  [2,3],
  [1,2],
  [1,3],
  [1,4],
]

大小为 k 的组合就是大小为 k 的子集,还是以 nums = [1,2,3] 为例,刚才求所有子集,就是把所有节点的值都收集起来;现在只需要把第 2 层(根节点视为第 0 层)的节点收集起来,就是大小为 2 的所有组合:

image-20220611175811776
class Solution {
    
    
public:
    vector<vector<int>> res;
    vector<int> path;
    vector<vector<int>> combine(int n, int k) {
    
    
        backtrack(n, k, 1);
        return res;
    }

    void backtrack(int n, int k, int start){
    
    
        if(k == path.size()){
    
    
            res.push_back(path);
            return;
        }
        for(int i = start; i <= n; i++){
    
    
            path.push_back(i);
            backtrack(n, k, i + 1);
            path.pop_back();
        }
    }
};

39. 组合总和

给你一个无重复元素的整数数组 candidates 和一个目标整数 target ,找出 candidates 中可以使数字和为目标数 target 的 所有不同组合 ,并以列表形式返回。你可以按任意顺序返回这些组合。candidates 中的同一个数字可以无限制重复被选取 。如果至少一个数字的被选数量不同,则两种组合是不同的。 对于给定的输入,保证和为 target 的不同组合数少于 150 个。

输入:candidates = [2,3,6,7], target = 7
输出:[[2,2,3],[7]]
解释:
23 可以形成一组候选,2 + 2 + 3 = 7 。注意 2 可以使用多次。7 也是一个候选, 7 = 7 。仅有这两种组合。

本道题中:输入数组无重复元素,每个元素可以被无限次使用。

标准的子集/组合问题是如何保证不重复使用元素的?在于 backtrack 递归时输入的参数 startistart 开始,那么下一层回溯树就是从 start + 1 开始,从而保证 nums[start] 这个元素不会被重复使用。

image-20220612114218321

想让每个元素被重复使用,我只要把 i + 1 改成 i 即可。这相当于给之前的回溯树添加了一条树枝,在遍历这棵树的过程中,一个元素可以被无限次使用。

image-20220612114503646
class Solution {
    
    
public:
    vector<vector<int>> res;
    vector<int> path;
    int sum = 0;
    vector<vector<int>> combinationSum(vector<int>& candidates, int target) {
    
    
        backtrack(candidates, target, 0);
        return res;
    }

    void backtrack(vector<int>& candidates, int target, int start){
    
    
        if(sum > target) return;
        if(sum == target){
    
    
            res.push_back(path);
            return;
        }
        for(int i = start; i < candidates.size(); i++){
    
    
            path.push_back(candidates[i]);
            sum += candidates[i];
            backtrack(candidates, target, i);
            path.pop_back();
            sum -= candidates[i];
        }
    }
};

40. 组合总和 II

给定一个候选人编号的集合 candidates 和一个目标数 target ,找出 candidates 中所有可以使数字和为 target 的组合。candidates 中的每个数字在每个组合中只能使用一次。注意:解集不能包含重复的组合

输入: candidates = [10,1,2,7,6,1,5], target = 8,
输出:
[
	[1,1,6],
	[1,2,5],
	[1,7],
	[2,6]
]

本道题中:输入数组有重复元素,每个元素只能使用一次,需要去重。

class Solution {
    
    
public:
    vector<vector<int>> res;
    vector<int> path;
    int sum = 0;
    vector<vector<int>> combinationSum2(vector<int>& candidates, int target) {
    
    
        sort(candidates.begin(), candidates.end());
        backtrack(candidates, target, 0);
        return res;
    }

    void backtrack(vector<int>& candidates, int target, int start){
    
    
        if(sum > target) return;
        if(sum == target){
    
    
            res.push_back(path);
            return;
        }
        for(int i = start; i < candidates.size(); i++){
    
    
            if(i > start && candidates[i - 1] == candidates[i]) continue;
            sum += candidates[i];
            path.push_back(candidates[i]);
            backtrack(candidates, target, i + 1);
            sum -= candidates[i];
            path.pop_back();
        }
    }
};

216. 组合总和 III

找出所有相加之和为 nk 个数的组合,且满足下列条件:只使用数字1到9,每个数字最多使用一次,返回所有可能的有效组合的列表。该列表不能包含相同的组合两次,组合可以以任何顺序返回。

输入: k = 3, n = 7
输出: [[1,2,4]]
解释:1 + 2 + 4 = 7,没有其他符合的组合了。

本道题中:输入数组无重复元素,每个元素只能使用一次。当满足sum == n && k == path.size()时,path加入res

class Solution {
    
    
public:
    vector<vector<int>> res;
    vector<int> path;
    int sum = 0;
    vector<vector<int>> combinationSum3(int k, int n) {
    
    
        backtrack(k, n, 1);
        return res;
    }

    void backtrack(int k, int n, int start){
    
    
        if(sum > n) return;
        if(sum == n && k == path.size()){
    
    
            res.push_back(path);
            return;
        }
        for(int i = start; i <= 9; i++){
    
    
            sum += i;
            path.push_back(i);
            backtrack(k, n, i + 1);
            sum -= i;
            path.pop_back();
        }
    }  
};

46. 全排列

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

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

排列问题就是穷举元素,nums[i] 之后也可以出现 nums[i] 左边的元素,所以之前的那一套玩不转了,需要额外使用 used 数组来标记哪些元素还可以被选择。

image-20220612143008374
class Solution {
    
    
public:
    vector<vector<int>> res;
    vector<int> path;
    vector<vector<int>> permute(vector<int>& nums) {
    
    
        vector<bool> used(nums.size(), false);
        backtrack(nums, used);
        return res;
    }

    void backtrack(vector<int>& nums, vector<bool>& used){
    
    
        if(path.size() == nums.size()){
    
    
            res.push_back(path);
            return;
        }
        for(int i = 0; i < nums.size(); i++){
    
    
            // path里已经收录的元素,直接跳过
            if(used[i]) continue;
            path.push_back(nums[i]);
            used[i] = true;
            backtrack(nums, used);
            path.pop_back();
            used[i] = false;
        }
    }
};

path里已经收录的元素,直接跳过:

image-20220612144916318

47. 全排列 II

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

输入:nums = [1,1,2]
输出:
[[1,1,2],
 [1,2,1],
 [2,1,1]]

nums 数组当中包含重复的元素,如何去掉重复的全排列呢?保证相同元素在排列中的相对位置保持不变即可。首先对 nums 进行排序,然后添加了一句额外的剪枝逻辑。

那么反映到代码上,剪枝逻辑如下:

// 新添加的剪枝逻辑,固定相同的元素在排列中的相对位置
if (i > 0 && nums[i] == nums[i - 1] && !used[i - 1]) {
    
    
    // 如果前面的相邻相等元素没有用过,则跳过
    continue;
}
// 选择 nums[i]

当出现重复元素时,比如输入 nums = [1,2,2',2'']2' 只有在 2 已经被使用的情况下才会被选择,同理,2'' 只有在 2' 已经被使用的情况下才会被选择,这就保证了相同元素在排列中的相对位置保证固定。

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

    void backtrack(vector<int>& nums, vector<bool>& used){
    
    
        if(path.size() == nums.size()){
    
    
            res.push_back(path);
            return;
        }
        for(int i = 0; i < nums.size(); i++){
    
    
            if(used[i]) continue;
            if(i > 0 && nums[i] == nums[i - 1] && !used[i - 1]){
    
    
                continue;
            }
            path.push_back(nums[i]);
            used[i] = true;
            backtrack(nums, used);
            path.pop_back();
            used[i] = false;
        }
    }
};

491. 递增子序列

给你一个整数数组 nums ,找出并返回所有该数组中不同的递增子序列,递增子序列中至少有两个元素 。你可以按任意顺序返回答案。数组中可能含有重复元素,如出现两个整数相等,也可以视作递增序列的一种特殊情况。

输入:nums = [4,6,7,7]
输出:[[4,6],[4,6,7],[4,6,7,7],[4,7],[4,7,7],[6,7],[6,7,7],[7,7]]

本题求自增子序列,是不能对原数组经行排序的,排完序的数组都是自增子序列了,所以不能使用之前的去重逻辑!使用unordered_set<int>来记录本层元素是否重复使用。

class Solution {
    
    
public:
    vector<vector<int>> result;
    vector<int> path;
    vector<vector<int>> findSubsequences(vector<int>& nums) {
    
    
        backtracking(nums, 0);
        return result;
    }
    void backtracking(vector<int>& nums, int startIndex) {
    
    
        if (path.size() > 1) {
    
    
            result.push_back(path);
            // 注意这里不要加return,要取树上的节点
        }
        unordered_set<int> uset; // 使用set对本层元素进行去重
        for (int i = startIndex; i < nums.size(); i++) {
    
    
            if ((!path.empty() && nums[i] < path.back()) || uset.count(nums[i])) {
    
    
                continue;
            }
            uset.insert(nums[i]); // 记录这个元素在本层用过了,本层后面不能再用了
            path.push_back(nums[i]);
            backtracking(nums, i + 1);
            path.pop_back();
        }
    }
};

切割问题

131. 分割回文串

给你一个字符串 s,请你将 s 分割成一些子串,使每个子串都是回文串,返回 s 所有可能的分割方案。回文串是正着读和反着读都一样的字符串。

输入:s = "aab"
输出:[["a","a","b"],["aa","b"]]

切割问题可以抽象为组合问题。

class Solution {
    
    
public:
    vector<vector<string>> res;
    vector<string> strs;
    vector<vector<string>> partition(string s) {
    
    
        get(s, 0);
        return res;
    }

    void get(string s, int start){
    
    
        // 如果起始位置已经大于s的大小,说明已经找到了一组分割方案了
        if(start >= s.size()){
    
    
            res.push_back(strs);
            return;
        }
        for(int i = start; i < s.size(); i++){
    
    
            if(!judge(s, start, i)) continue;
            string str = s.substr(start, i - start + 1);
            strs.push_back(str);
            get(s, i + 1);   // 寻找i + 1为起始位置的子串
            strs.pop_back(); // 回溯过程,弹出本次已经填在的子串
        }
    }

    bool judge(string s, int i, int j){
    
    
        while(i < j){
    
    
            if(s[i] != s[j]){
    
    
                return false;
            }
            i++;
            j--;
        }
        return true;
    }
};

93. 复原 IP 地址

有效 IP 地址 正好由四个整数(每个整数位于 0 到 255 之间组成,且不能含有前导 0),整数之间用 ‘.’ 分隔。例如:“0.1.2.201” 和 “192.168.1.1” 是 有效 IP 地址,但是 “0.011.255.245”、“192.168.1.312” 和 “[email protected]” 是 无效 IP 地址。

给定一个只包含数字的字符串 s ,用以表示一个 IP 地址,返回所有可能的有效 IP 地址,这些地址可以通过在 s 中插入 ‘.’ 来形成。你不能重新排序或删除 s 中的任何数字。你可以按任何顺序返回答案。

输入:s = "25525511135"
输出:["255.255.11.135","255.255.111.35"]

切割问题可以抽象为组合问题,本题还需要操作字符串添加逗号作为分隔符,并验证区间的合法性。

class Solution {
    
    
public:
    vector<string> res;
    int point = 0;
    vector<string> restoreIpAddresses(string s) {
    
    
        get(s, 0);
        return res;
    }
    
    // start: 搜索的起始位置,point: 添加逗点的数量
    void get(string s, int start){
    
    
        // 逗点数量为3时,分隔结束
        if(point == 3){
    
    
            // 判断第四段子字符串是否合法,如果合法就放进res中
            if(judge(s, start, s.size() - 1)){
    
    
                res.push_back(s);
            }
            return;
        }
        for(int i = start; i < s.size(); i++){
    
    
            if(!judge(s, start, i)) continue;
            s.insert(s.begin() + i + 1, '.');
            point++;
            get(s, i + 2); // 插入逗点之后下一个子串的起始位置为i + 2
            s.erase(s.begin() + i + 1);
            point--;
        }
    }

    // 判断字符串s在左闭又闭区间[start, end]所组成的数字是否合法
    bool judge(string s, int i, int j){
    
    
        if(i > j) return false;
        if(s[i] == '0' && i != j) return false; // 前导0不合法
        int num = 0;
        while(i <= j){
    
    
            num = num * 10 + (s[i] - '0');
            if(num > 255) return false; // 大于255不合法
            i++;
        }
        return true;
    }

};

468. 验证IP地址

给定一个字符串 queryIP。如果是有效的 IPv4 地址,返回 “IPv4” ;如果是有效的 IPv6 地址,返回 “IPv6” ;如果不是上述类型的 IP 地址,返回 “Neither” 。

有效的IPv4地址 是 “x1.x2.x3.x4” 形式的IP地址。 其中 0 <= xi <= 255 且 xi 不能包含前导零。例如: “192.168.1.1” 、 “192.168.1.0” 为有效IPv4地址, “192.168.01.1” 为无效IPv4地址; “192.168.1.00” 、 “[email protected]” 为无效IPv4地址。

一个有效的IPv6地址 是一个格式为“x1:x2:x3:x4:x5:x6:x7:x8” 的IP地址,其中: 1 <= xi.length <= 4;xi 是一个 十六进制字符串 ,可以包含数字、小写英文字母( ‘a’ 到 ‘f’ )和大写英文字母( ‘A’ 到 ‘F’ );在 xi 中允许前导零。

例如 “2001:0db8:85a3:0000:0000:8a2e:0370:7334” 和 “2001:db8:85a3:0:0:8A2E:0370:7334” 是有效的 IPv6 地址,而 “2001:0db8:85a3::8A2E:037j:7334” 和 “02001:0db8:85a3:0000:0000:8a2e:0370:7334” 是无效的 IPv6 地址。

输入:queryIP = "172.16.254.1"
输出:"IPv4"
解释:有效的 IPv4 地址,返回 "IPv4"
class Solution {
    
    
public:
    string validIPAddress(string queryIP) {
    
    
        if(ipv4(queryIP)) return "IPv4";
        if(ipv6(queryIP)) return "IPv6";
        return "Neither";
    }

    // 去掉字符串中的分隔符
    vector<string> get(string s, char flag){
    
    
        string tmp;
        s += flag;
        vector<string> strs;
        for(char ch : s){
    
    
            if(ch != flag){
    
    
                tmp += ch;
            }else{
    
    
                strs.push_back(tmp);
                tmp.clear();
            }
        }
        return strs;
    }

    bool ipv4(string s){
    
    
        vector<string> strs = get(s, '.');
        if(strs.size() != 4) return false;
        for(string str : strs){
    
    
            if(str.empty() || str.size() > 3){
    
    
                return false;
            }
            if(str[0] == '0' && str.size() != 1){
    
    
                return false;
            }
            for(char ch : str){
    
    
                if(!isdigit(ch)) return false;
            }
            int num = stoi(str);
            if(num > 255) return false;
        }
        return true;
    }

    bool ipv6(string s){
    
    
        vector<string> strs = get(s, ':');
        if(strs.size() != 8) return false;
        for(string str : strs){
    
    
            if(str.empty() || str.size() > 4){
    
    
                return false;
            }
            for(char ch : str){
    
    
                if(!((ch >= '0' && ch <= '9') 
                    || (ch >= 'a' && ch <= 'f') 
                    || (ch >= 'A' && ch <= 'F'))){
    
    
                    return false;
                }
            }
        }
        return true;
    }

};

岛屿问题

岛屿系列题目的核心考点就是用 DFS/BFS 算法遍历二维数组

200. 岛屿数量

给你一个由 ‘1’(陆地)和 ‘0’(水)组成的的二维网格,请你计算网格中岛屿的数量。岛屿总是被水包围,并且每座岛屿只能由水平方向和/或竖直方向上相邻的陆地连接形成。此外,你可以假设该网格的四条边均被水包围。

输入:grid = [
 ["1","1","1","1","0"],
 ["1","1","0","1","0"],
 ["1","1","0","0","0"],
 ["0","0","0","0","0"]
]
输出:1

用 DFS 算法解决岛屿题目是最常见的,每次遇到一个岛屿中的陆地,就用 DFS 算法吧这个岛屿「淹掉」。

如何使用 DFS 算法遍历二维数组?把二维数组中的每个格子看做「图」中的一个节点,这个节点和周围的四个节点连通,这样二维矩阵就被抽象成了一幅网状的「图」。

为什么每次遇到岛屿,都要用 DFS 算法把岛屿「淹了」呢?主要是为了省事,避免维护 visited 数组。遍历图是需要 visited 数组记录遍历过的节点防止走回头路。因为 dfs 函数遍历到值为 0 的位置会直接返回,所以只要把经过的位置都设置为 0,就可以起到不走回头路的作用。

class Solution {
    
    
public:
    int numIslands(vector<vector<char>>& grid) {
    
    
        int count = 0;
        int m = grid.size(), n = grid[0].size();
        for (int i = 0; i < m; i++) {
    
    
            for (int j = 0; j < n; j++) {
    
    
                if (grid[i][j] == '1') {
    
    
                    count++;            // 每发现一个岛屿,岛屿数量加一
                    dfs(grid, i, j);    // 然后使用 DFS 将岛屿淹了
                }
            }
        }
        return count;
    }

    // 从 (i, j) 开始,将与之相邻的陆地都变成海水
    void dfs(vector<vector<char>>& grid, int i, int j){
    
    
        int m = grid.size(), n = grid[0].size();
        if(i < 0 || j < 0 || i >= m || j >= n) return;
        if(grid[i][j] == '0') return;
        grid[i][j] = '0';
        dfs(grid, i + 1, j);
        dfs(grid, i, j + 1);
        dfs(grid, i - 1, j);
        dfs(grid, i, j - 1);
    }
};

1254. 统计封闭岛屿的数目

二维矩阵 grid 由 0 (土地)和 1 (水)组成。岛是由最大的4个方向连通的 0 组成的群,封闭岛是一个 完全 由1包围(左、上、右、下)的岛,请返回 封闭岛屿 的数目。

img
输入:grid = [[1,1,1,1,1,1,1,0],[1,0,0,0,0,1,1,0],[1,0,1,0,1,1,1,0],[1,0,0,0,0,1,0,1],[1,1,1,1,1,1,1,0]]
输出:2
解释:灰色区域的岛屿是封闭岛屿,因为这座岛屿完全被水域包围(即被 1 区域包围)。

和上一题有两点不同:

  1. 0 表示陆地,用 1 表示海水。

  2. 让你计算「封闭岛屿」的数目。所谓「封闭岛屿」就是上下左右全部被 1 包围的 0,也就是说靠边的陆地不算作「封闭岛屿」

那么如何判断「封闭岛屿」呢?把上一题中那些靠边的岛屿排除掉,剩下的就是「封闭岛屿」。

class Solution {
    
    
public:
    int closedIsland(vector<vector<int>>& grid) {
    
    
        int m = grid.size(), n = grid[0].size();
        for (int j = 0; j < n; j++) {
    
    
            dfs(grid, 0, j);        // 把靠上边的岛屿淹掉
            dfs(grid, m - 1, j);    // 把靠下边的岛屿淹掉
        }
        for (int i = 0; i < m; i++) {
    
    
            dfs(grid, i, 0);        // 把靠左边的岛屿淹掉
            dfs(grid, i, n - 1);    // 把靠右边的岛屿淹掉
        }
        // 遍历 grid,剩下的岛屿都是封闭岛屿
        int count = 0;
        for (int i = 0; i < m; i++) {
    
    
            for (int j = 0; j < n; j++) {
    
    
                if (grid[i][j] == 0) {
    
    
                    count++;            // 每发现一个岛屿,岛屿数量加一
                    dfs(grid, i, j);    // 然后使用 DFS 将岛屿淹了
                }
            }
        }
        return count;
    }

    // 从 (i, j) 开始,将与之相邻的陆地都变成海水
    void dfs(vector<vector<int>>& grid, int i, int j){
    
    
        int m = grid.size(), n = grid[0].size();
        if(i < 0 || j < 0 || i >= m || j >= n) return;
        if(grid[i][j] == 1) return;
        grid[i][j] = 1;
        dfs(grid, i + 1, j);
        dfs(grid, i, j + 1);
        dfs(grid, i - 1, j);
        dfs(grid, i, j - 1);
    }
};

1020. 飞地的数量

给你一个大小为 m x n 的二进制矩阵 grid ,其中 0 表示一个海洋单元格、1 表示一个陆地单元格。一次移动是指从一个陆地单元格走到另一个相邻(上、下、左、右)的陆地单元格或跨过 grid 的边界。返回网格中无法在任意次数的移动中离开网格边界的陆地单元格的数量。

img
输入:grid = [[0,0,0,0],[1,0,1,0],[0,1,1,0],[0,0,0,0]]
输出:3
解释:有三个 10 包围。一个 1 没有被包围,因为它在边界上。

这题不求封闭岛屿的数量,而是求封闭岛屿的面积总和,本质上和上一题是一样的。思路都是一样的,先把靠边的陆地淹掉,然后去数剩下的陆地数量。

class Solution {
    
    
public:
    int numEnclaves(vector<vector<int>>& grid) {
    
    
        int m = grid.size(), n = grid[0].size();
        for (int j = 0; j < n; j++) {
    
    
            dfs(grid, 0, j);        // 把靠上边的岛屿淹掉
            dfs(grid, m - 1, j);    // 把靠下边的岛屿淹掉
        }
        for (int i = 0; i < m; i++) {
    
    
            dfs(grid, i, 0);        // 把靠左边的岛屿淹掉
            dfs(grid, i, n - 1);    // 把靠右边的岛屿淹掉
        }
        // 遍历 grid,剩下的岛屿都是封闭岛屿
        int count = 0;
        for (int i = 0; i < m; i++) {
    
    
            for (int j = 0; j < n; j++) {
    
    
                if (grid[i][j] == 1) {
    
    
                    count++;            // 每发现一个岛屿,岛屿数量加一
                }
            }
        }
        return count;
    }

    // 从 (i, j) 开始,将与之相邻的陆地都变成海水
    void dfs(vector<vector<int>>& grid, int i, int j){
    
    
        int m = grid.size(), n = grid[0].size();
        if(i < 0 || j < 0 || i >= m || j >= n) return;
        if(grid[i][j] == 0) return;
        grid[i][j] = 0;
        dfs(grid, i + 1, j);
        dfs(grid, i, j + 1);
        dfs(grid, i - 1, j);
        dfs(grid, i, j - 1);
    }
};

695. 岛屿的最大面积

给你一个大小为 m x n 的二进制矩阵 grid 。岛屿 是由一些相邻的 1 (代表土地) 构成的组合,这里的「相邻」要求两个 1 必须在 水平或者竖直的四个方向上 相邻。你可以假设grid 的四个边缘都被 0(代表水)包围着。岛屿的面积是岛上值为 1 的单元格的数目。计算并返回 grid 中最大的岛屿面积。如果没有岛屿,则返回面积为 0 。

img
输入:grid = [[0,0,1,0,0,0,0,1,0,0,0,0,0],[0,0,0,0,0,0,0,1,1,1,0,0,0],
           [0,1,1,0,1,0,0,0,0,0,0,0,0],[0,1,0,0,1,1,0,0,1,0,1,0,0],
           [0,1,0,0,1,1,0,0,1,1,1,0,0],[0,0,0,0,0,0,0,0,0,0,1,0,0],
           [0,0,0,0,0,0,0,1,1,1,0,0,0],[0,0,0,0,0,0,0,1,1,0,0,0,0]]
输出:6
解释:答案不应该是 11 ,因为岛屿只能包含水平或垂直这四个方向上的 1

这题的大体思路和之前完全一样,只不过 dfs 函数淹没岛屿的同时,还应该想办法记录这个岛屿的面积。可以给 dfs 函数设置返回值,记录每次淹没的陆地的个数。

class Solution {
    
    
public:
    int maxAreaOfIsland(vector<vector<int>>& grid) {
    
    
        int m = grid.size(), n = grid[0].size();
        int count = 0;
        for (int i = 0; i < m; i++) {
    
    
            for (int j = 0; j < n; j++) {
    
    
                if (grid[i][j] == 1) {
    
    
                    // 淹没岛屿,并更新最大岛屿面积
                    count = max(count, dfs(grid, i, j));  
                }
            }
        }
        return count;
    }

    // 淹没与 (i, j) 相邻的陆地,并返回淹没的陆地面积
    int dfs(vector<vector<int>>& grid, int i, int j){
    
    
        int m = grid.size(), n = grid[0].size();
        if(i < 0 || j < 0 || i >= m || j >= n) return 0;
        if(grid[i][j] == 0) return 0;
        grid[i][j] = 0;
        return 
        dfs(grid, i + 1, j) +
        dfs(grid, i, j + 1) +
        dfs(grid, i - 1, j) +
        dfs(grid, i, j - 1) + 1;
    }
};

1905. 统计子岛屿

给你两个 m x n 的二进制矩阵 grid1 和 grid2 ,它们只包含 0 (表示水域)和 1 (表示陆地)。一个 岛屿 是由 四个方向 (水平或者竖直)上相邻的 1 组成的区域。任何矩阵以外的区域都视为水域。如果 grid2 的一个岛屿,被 grid1 的一个岛屿 完全 包含,也就是说 grid2 中该岛屿的每一个格子都被 grid1 中同一个岛屿完全包含,那么我们称 grid2 中的这个岛屿为 子岛屿 。请你返回 grid2 中子岛屿的数目。

输入:grid1 = [[1,1,1,0,0],[0,1,1,1,1],[0,0,0,0,0],[1,0,0,0,0],[1,1,0,1,1]], 
grid2 = [[1,1,1,0,0],[0,0,1,1,1],[0,1,0,0,0],[1,0,1,1,0],[0,1,0,1,0]]
输出:3
解释:如上图所示,左边为 grid1 ,右边为 grid2。grid2 中标红的 1 区域是子岛屿,总共有 3 个子岛屿。

什么情况下 grid2 中的一个岛屿 Bgrid1 中的一个岛屿 A 的子岛?当岛屿 B 中所有陆地在岛屿 A 中也是陆地的时候,岛屿 B 是岛屿 A 的子岛。反过来说,如果岛屿 B 中存在一片陆地,在岛屿 A 的对应位置是海水,那么岛屿 B 就不是岛屿 A 的子岛。那么,我们只要遍历 grid2 中的所有岛屿,把那些不可能是子岛的岛屿排除掉,剩下的就是子岛。

class Solution {
    
    
public:
    int countSubIslands(vector<vector<int>>& grid1, vector<vector<int>>& grid2) {
    
    
        int m = grid1.size(), n = grid1[0].size();
        for(int i = 0; i < m; i++){
    
    
            for(int j = 0; j < n; j++){
    
    
                if (grid1[i][j] == 0 && grid2[i][j] == 1) {
    
    
                    // 这个岛屿肯定不是子岛,淹掉
                    dfs(grid2, i, j);
                }
            }
        }
        // 现在 grid2 中剩下的岛屿都是子岛,计算岛屿数量
        int res = 0;
        for (int i = 0; i < m; i++) {
    
    
            for (int j = 0; j < n; j++) {
    
    
                if (grid2[i][j] == 1) {
    
    
                    res++;
                    dfs(grid2, i, j);
                }
            }
        }
        return res;
    }

    // 从 (i, j) 开始,将与之相邻的陆地都变成海水
    void dfs(vector<vector<int>>& grid, int i, int j){
    
    
        int m = grid.size(), n = grid[0].size();
        if(i < 0 || j < 0 || i >= m || j >= n) return;
        if(grid[i][j] == 0) return;
        grid[i][j] = 0;
        dfs(grid, i + 1, j);
        dfs(grid, i, j + 1);
        dfs(grid, i - 1, j);
        dfs(grid, i, j - 1);
    }
};

玩游戏

51. N 皇后

按照国际象棋的规则,皇后可以攻击与之处在同一行或同一列或同一斜线上的棋子。n 皇后问题研究的是如何将 n 个皇后放置在 n×n 的棋盘上,并且使皇后彼此之间不能相互攻击。给你一个整数 n ,返回所有不同的 n 皇后问题的解决方案。每一种解法包含一个不同的 n 皇后问题的棋子放置方案,该方案中 ‘Q’ 和 ‘.’ 分别代表了皇后和空位。

image-20220612152411212
输入:n = 4
输出:[[".Q..","...Q","Q...","..Q."],["..Q.","Q...","...Q",".Q.."]]
解释:如上图所示,4 皇后问题存在两个不同的解法。

皇后的约束条件:不能同行、不能同列、不能同斜线。

搜索皇后的位置,可以抽象为一棵树:

image-20220613113646889

那么我们用皇后们的约束条件,来回溯搜索这棵树,只要搜索到了树的叶子节点,说明就找到了皇后们的合理位置了

row来记录当前遍历到棋盘的第几层,递归深度就是row控制棋盘的行,每一层里for循环的col控制棋盘的列,一行一列,确定了放置皇后的位置。每次都是要从新的一行的起始位置开始搜,所以都是从0开始。

那如何验证棋盘是否合法呢?按照皇后的约束条件。没有在同行进行检查,因为在单层搜索的过程中,每一层递归,只会选for循环(也就是同一行)里的一个元素,所以不用去重了。

class Solution {
    
    
    public:
    vector<vector<string>> res;
    /* 输入棋盘边长 n,返回所有合法的放置 */
    vector<vector<string>> solveNQueens(int n) {
    
    
        // '.' 表示空,'Q' 表示皇后,初始化空棋盘。
        vector<string> board(n, string(n, '.'));
        backtrack(board, 0);
        return res;
    }
    
    // 路径:board 中小于 row 的那些行都已经成功放置了皇后
    // 选择列表:第 row 行的所有列都是放置皇后的选择
    // 结束条件:row 超过 board 的最后一行
    void backtrack(vector<string>& board, int row) {
    
    
        // 触发结束条件
        if (row == board.size()) {
    
    
            res.push_back(board);
            return;
        }
        int n = board[row].size();
        for (int col = 0; col < n; col++) {
    
    
            // 排除不合法选择
            if (!isValid(board, row, col)) continue;
            // 做选择
            board[row][col] = 'Q';
            // 进入下一行决策
            backtrack(board, row + 1);
            // 撤销选择
            board[row][col] = '.';
        }
    }
    
    /* 是否可以在 board[row][col] 放置皇后?*/
    bool isValid(vector<string>& board, int row, int col) {
    
    
        int n = board.size();
        // 检查列是否有皇后互相冲突
        for (int i = 0; i <= row; i++) {
    
    
            if(board[i][col] == 'Q') return false;
        }
        // 检查右上方是否有皇后互相冲突
        for (int i = row - 1, j = col + 1; i >= 0 && j < n; i--, j++) {
    
    
            if(board[i][j] == 'Q') return false;
        }
        // 检查左上方是否有皇后互相冲突
        for (int i = row - 1, j = col - 1; i >= 0 && j >= 0; i--, j--) {
    
    
            if(board[i][j] == 'Q') return false;
        }
        return true;
    }
};

37. 解数独

编写一个程序,通过填充空格来解决数独问题。数独的解法需遵循如下规则:

  1. 数字 1-9 在每一行只能出现一次。

  2. 数字 1-9 在每一列只能出现一次。

  3. 数字 1-9 在每一个以粗实线分隔的 3x3 宫内只能出现一次。

数独部分空格内已填入了数字,空白格用 ‘.’ 表示。

img

算法的核心思路非常非常的简单,就是对每一个空着的格子穷举 1 到 9,如果遇到不合法的数字(在同一行或同一列或同一个 3×3 的区域中存在相同的数字)则跳过,如果找到一个合法的数字,则继续穷举下一个空格子。

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

    bool backtrack(vector<vector<char>>& board, int i, int j){
    
    
        if(j == 9){
    
    
            // 穷举到最后一列的话就换到下一行重新开始。
            return backtrack(board, i + 1, 0);
        }
        // 找到一个可行解,触发 base case
        if(i == 9) return true;
        if (board[i][j] != '.') {
    
    
            // 如果有预设数字,不用我们穷举
            return backtrack(board, i, j + 1);
        }
        for(char ch = '1'; ch <= '9'; ch++){
    
    
            // 如果遇到不合法的数字,就跳过
            if (!isValid(board, i, j, ch)) continue;
            board[i][j] = ch;
            // 如果找到一个可行解,立即结束
            if (backtrack(board, i, j + 1)) return true;
            board[i][j] = '.';
        }
        // 穷举完 1~9,依然没有找到可行解,此路不通
        return false;
    }

    // 判断 board[i][j] 是否可以填入 ch
    bool isValid(vector<vector<char>>& board, int row, int col, char ch){
    
    
        for(int i = 0; i < 9; i++){
    
    
            // 判断行是否存在重复
            if (board[row][i] == ch) return false;
            // 判断列是否存在重复
            if (board[i][col] == ch) return false;
            // 判断 3 x 3 方框是否存在重复
            if (board[(row / 3) * 3 + i / 3][(col / 3) * 3 + i % 3] == ch){
    
    
                return false;
            }
        }
        return true;
    }
};

其他

22. 括号生成

数字 n 代表生成括号的对数,请你设计一个函数,用于能够生成所有可能的并且 有效的 括号组合。

输入:n = 3
输出:["((()))","(()())","(())()","()(())","()()()"]

一个「合法」括号组合的左括号数量一定等于右括号数量,并且对于一个「合法」的括号字符串组合 p,必然对于任何 0 <= i < len§ 都有:子串 p[0…i] 中左括号的数量都大于或等于右括号的数量。

比如这个括号组合 ))((,前几个子串都是右括号多于左括号,显然不是合法的括号组合。

题目相当于有 2n 个位置,每个位置可以放置字符 ( 或者 ),组成的所有括号组合中,有多少个是合法的?

left 记录还可以使用多少个左括号,用 right 记录还可以使用多少个右括号,就可以直接套用回溯算法套路框架了。

class Solution {
    
    
public:
    vector<string> res; // 记录所有合法的括号组合
    string track;       // 回溯过程中的路径
    vector<string> generateParenthesis(int n) {
    
    
        backtrack(n, n); // 可用的左括号和右括号数量初始化为 n
        return res;
    }

    // 可用的左括号数量为 left 个,可用的右括号数量为 right 个
    void backtrack(int left, int right) {
    
    
        // 若左括号剩下的多,说明不合法
        if (right < left) return;
        // 数量小于 0 肯定是不合法的
        if (left < 0 || right < 0) return;
        // 当所有括号都恰好用完时,得到一个合法的括号组合
        if (left == 0 && right == 0) {
    
    
            res.push_back(track);
            return;
        }
        // 尝试放一个左括号
        track.push_back('('); // 选择
        backtrack(left - 1, right);
        track.pop_back(); // 撤消选择
        // 尝试放一个右括号
        track.push_back(')'); // 选择
        backtrack(left, right - 1);
        track.pop_back(); // 撤消选择
    }
};

17. 电话号码的字母组合

给定一个仅包含数字 2-9 的字符串,返回所有它能表示的字母组合,答案可以按任意顺序返回。给出数字到字母的映射如下(与电话按键相同)。注意 1 不对应任何字母。

img
输入:digits = "23"
输出:["ad","ae","af","bd","be","bf","cd","ce","cf"]
class Solution {
    
    
private:
    const string letterMap[10] = {
    
    
        "",     // 0
        "",     // 1
        "abc",  // 2
        "def",  // 3
        "ghi",  // 4
        "jkl",  // 5
        "mno",  // 6
        "pqrs", // 7
        "tuv",  // 8
        "wxyz", // 9
    };
public:
    vector<string> result;
    string s;
    vector<string> letterCombinations(string digits) {
    
    
        if(digits.empty()) return result;
        backtracking(digits, 0);
        return result;
    }

    void backtracking(const string& digits, int index) {
    
    
        if (index == digits.size()) {
    
    
            result.push_back(s);
            return;
        }
        int digit = digits[index] - '0';        // 将index指向的数字转为int
        string letters = letterMap[digit];      // 取数字对应的字符集
        for (int i = 0; i < letters.size(); i++) {
    
    
            s.push_back(letters[i]);            // 处理
            backtracking(digits, index + 1);    // 递归,注意index + 1,下一层要处理下一个数字了
            s.pop_back();                       // 回溯
        }
    }
};

注意:index用来记录遍历第几个数字,就是用来遍历digits的。

79. 单词搜索

给定一个 m x n 二维字符网格 board 和一个字符串单词 word 。如果 word 存在于网格中,返回 true ;否则,返回 false 。单词必须按照字母顺序,通过相邻的单元格内的字母构成,其中“相邻”单元格是那些水平相邻或垂直相邻的单元格。同一个单元格内的字母不允许被重复使用。

img
输入:board = [["A","B","C","E"],["S","F","C","S"],["A","D","E","E"]], word = "ABCCED"
输出:true

在给定的表格中搜索每个位置作为起始位置的可能的路径,只要存在一条即可,使用lables数组去重。

class Solution
{
    
    
public:
    bool dfs(vector<vector<char>> &board, string &word, int row, int col, vector<vector<bool>> &lables, int begin){
    
    
        //终止的满足要求的条件
        if (begin == word.size()) return true;
        //错误的条件
        if (row < 0 || row == board.size() || 
            col < 0 || col == board[0].size() || 
            lables[row][col] || board[row][col] != word[begin]){
    
    
            return false;
        }
        //标识当前位置访问过
        lables[row][col] = true;
        if (dfs(board, word, row - 1, col, lables, begin + 1) || 
            dfs(board, word, row + 1, col, lables, begin + 1) || 
            dfs(board, word, row, col + 1, lables, begin + 1) || 
            dfs(board, word, row, col - 1, lables, begin + 1)){
    
    
            return true;
        }
        lables[row][col] = false; //复原
        return false;
    }

    bool exist(vector<vector<char>> &board, string word){
    
    
        //用于标识搜索过的路径
        vector<vector<bool>> lables(board.size(), vector<bool>(board[0].size(), false));
        int begin = 0;
        //搜索所有可能的起点
        for (int row = 0; row < board.size(); ++row){
    
    
            for (int col = 0; col < board[0].size(); ++col){
    
    
                if (dfs(board, word, row, col, lables, begin)){
    
    
                    return true;
                }
            }
        }
        return false;
    }
};

猜你喜欢

转载自blog.csdn.net/weixin_42461320/article/details/128694206