赞
踩
给你一个大小为 m x n 的二进制矩阵 grid 。
岛屿 是由一些相邻的 1 (代表土地) 构成的组合,这里的「相邻」要求两个 1 必须在 水平或者竖直的四个方向上 相邻。你可以假设 grid 的四个边缘都被 0(代表水)包围着。
岛屿的面积是岛上值为 1 的单元格的数目。
计算并返回 grid 中最大的岛屿面积。如果没有岛屿,则返回面积为 0 。
此题是十分标准的搜索题,我们可以拿来练手深度优先搜索。一般来说,深度优先搜索类型的题可以分为主函数和辅函数,主函数用于遍历所有的搜索位置,判断是否可以开始索,如果可以即在辅函数进行搜索。辅函数则负责深度优先搜索的递归调用。当然,我们也可以使用栈(stack )实现深度优先搜索,但因为栈与递归的调用原理相同,而递归相对便于实现,因此刷题时笔者推荐使用递归式写法,同时也方便进行回溯(见下节)。不过在实际工程上,直接使用栈可能才是最好的选择,一是因为便于理解,二是更不易出现递归栈满的情况。这里我们使用了一个小技巧,对于四个方向的遍历,可以创造一个数组 [-1, 0, 1, 0, -1],每相邻两位即为上下左右四个方向之一。在辅函数里,一个一定要注意的点是辅函数内递归搜索时,边界条件的判定。边界判定一般有两种写法,一种是先判定是否越界,只有在合法的情况下才进行下一步搜索(即判断放在调用递归函数前);另一种是不管三七二十一先进行下一步搜索,待下一步搜索开始时再判断是否合法(即判断放在辅函数第一行)。
- class Solution {
- public:
- vector<int> direction{-1, 0, 1, 0, -1};
- // 主函数
- int maxAreaOfIsland(vector<vector<int>>& grid) {
- if(grid.empty() || grid[0].empty())
- return 0;
- int max_area = 0;
- for(int i=0; i<grid.size(); ++i){
- for(int j=0; j<grid[0].size(); ++j){
- if(grid[i][j] == 1){
- max_area = max(max_area, dfs(grid, i, j));
- }
- }
- }
- return max_area;
- }
- // 辅函数
- int dfs(vector<vector<int>>& grid, int r, int c){
- if(grid[r][c] == 0)
- return 0;
- grid[r][c] = 0;
- int x, y, area = 1;
- for(int i=0; i<4; ++i){
- x = r + direction[i], y = c + direction[i + 1];
- if(x >= 0 && x < grid.size() && y >= 0 && y < grid[0].size()){
- area += dfs(grid, x, y);
- }
- }
- return area;
- }
- };
有 n 个城市,其中一些彼此相连,另一些没有相连。如果城市 a 与城市 b 直接相连,且城市 b 与城市 c 直接相连,那么城市 a 与城市 c 间接相连。
省份 是一组直接或间接相连的城市,组内不含其他没有相连的城市。
给你一个 n x n 的矩阵 isConnected ,其中 isConnected[i][j] = 1 表示第 i 个城市和第 j 个城市直接相连,而 isConnected[i][j] = 0 表示二者不直接相连。
返回矩阵中 省份 的数量。
对于题目 695 ,图的表示方法是,每个位置代表一个节点,每个节点与上下左右四个节点相邻。而在这一道题里面,每一行(列)表示一个节点,它的每列(行)表示是否存在一个相邻节点。因此题目 695 拥有 m × n 个节点,每个节点有 4 条边;而本题拥有 n 个节点,每个节点最多有 n 条边,表示和所有人都是朋友,最少可以有 1 条边,表示自己与自己相连。当清楚了图的表 示方法后,这道题与题目 695 本质上是同一道题:搜索省份 (岛屿)的个数(最大面积)。
- class Solution {
- public:
- // 主函数
- int findCircleNum(vector<vector<int>>& isConnected) {
- int n = isConnected.size(), count = 0;
- vector<bool> visited(n, false);
- for(int i=0; i<n; ++i){
- if(!visited[i]){
- dfs(isConnected, i, visited);
- ++count;
- }
- }
- return count;
- }
- // 辅函数
- void dfs(vector<vector<int>>& isConnected, int i, vector<bool>& visited){
- visited[i] = true;
- for(int k=0; k<isConnected.size(); ++k){
- if(isConnected[i][k] == 1 && !visited[k]){
- dfs(isConnected, k, visited);
- }
- }
- }
- };
417. Pacific Atlantic Water Flow
有一个 m × n 的矩形岛屿,与 太平洋 和 大西洋 相邻。 “太平洋” 处于大陆的左边界和上边界,而 “大西洋” 处于大陆的右边界和下边界。
这个岛被分割成一个由若干方形单元格组成的网格。给定一个 m x n 的整数矩阵 heights , heights[r][c] 表示坐标 (r, c) 上单元格 高于海平面的高度 。
岛上雨水较多,如果相邻单元格的高度 小于或等于 当前单元格的高度,雨水可以直接向北、南、东、西流向相邻单元格。水可以从海洋附近的任何单元格流入海洋。
返回网格坐标 result 的 2D 列表 ,其中 result[i] = [ri, ci] 表示雨水从单元格 (ri, ci) 流动 既可流向太平洋也可流向大西洋 。
虽然题目要求的是满足向下流能到达两个大洋的位置,如果我们对所有的位置进行搜索,那 么在不剪枝的情况下复杂度会很高。因此我们可以反过来想,从两个大洋开始向上流,这样我们 只需要对矩形四条边进行搜索。搜索完成后,只需遍历一遍矩阵,满足条件的位置即为两个大洋 向上流都能到达的位置。
- class Solution {
- public:
- vector<int> direction{-1, 0, 1, 0, -1};
- vector<vector<int>> pacificAtlantic(vector<vector<int>>& heights) {
- if(heights.empty() || heights[0].empty()){
- return {};
- }
- vector<vector<int>> ans;
- int m = heights.size(), n = heights[0].size();
- vector<vector<bool>> can_reach_p(m, vector<bool>(n, false)); // 太平洋
- vector<vector<bool>> can_reach_a(m, vector<bool>(n, false)); // 大西洋
- // 左右两边开始往里流水
- for(int i=0; i<m; ++i){
- dfs(heights, can_reach_p, i, 0);
- dfs(heights, can_reach_a, i, n - 1);
- }
- // 上下两边开始往里流水
- for(int i=0; i<n; ++i){
- dfs(heights, can_reach_p, 0, i);
- dfs(heights, can_reach_a, m - 1, i);
- }
- for(int i=0; i<m; ++i){
- for(int j=0; j<n; ++j){
- if(can_reach_p[i][j] && can_reach_a[i][j]){
- ans.push_back(vector<int>{i, j});
- }
- }
- }
- return ans;
- }
- void dfs(const vector<vector<int>>& heights, vector<vector<bool>>& can_reach, int r, int c){
- if(can_reach[r][c]){ //能反流到,则退出这次dfs
- return;
- }
- can_reach[r][c] = true; //标记为能反流到
- int x, y;
- for(int i=0; i<4; ++i){
- x = r + direction[i], y = c + direction[i + 1];
- if(x >= 0 && x <= heights.size() - 1 && y >= 0 && y <= heights[0].size() - 1 && heights[r][c] <= heights[x][y]){
- dfs(heights, can_reach, x, y);
- }
- }
- }
- };
给定一个不含重复数字的数组 nums
,返回其 所有可能的全排列 。你可以 按任意顺序 返回答案。
怎样输出所有的排列方式呢?对于每一个当前位置 i,我们可以将其于之后的任意位置交换,然后继续处理位置 i+1,直到处理到最后一位。为了防止我们每此遍历时都要新建一个子数组储存位置 i 之前已经交换好的数字,我们可以利用回溯法,只对原数组进行修改,在递归完成后再修改回来。
我们以样例[1,2,3]为例,按照这种方法,我们输出的数组顺序为[[1,2,3], [1,3,2], [2,1,3], [2,3,1], [3,2,1], [3,1,2]],可以看到所有的排列在这个算法中都被考虑到了。
- class Solution {
- public:
- // 主函数
- vector<vector<int>> permute(vector<int>& nums) {
- vector<vector<int>> ans;
- backtracking(nums, 0, ans);
- return ans;
- }
- // 辅函数
- void backtracking(vector<int> &nums, int level, vector<vector<int>> &ans){
- if(level == nums.size() - 1){
- ans.push_back(nums);
- return;
- }
- for(int i=level; i<nums.size(); ++i){
- swap(nums[i], nums[level]); // 修改当前节点状态
- backtracking(nums, level + 1, ans); // 递归子节点
- swap(nums[i], nums[level]); // 回退到当前节点
- }
- }
- };
给定两个整数 n
和 k
,返回范围 [1, n]
中所有可能的 k
个数的组合。
你可以按 任何顺序 返回答案。
类似于排列问题,我们也可以进行回溯。排列回溯的是交换的位置,而组合回溯的是是否把当前的数字加入结果中。
- class Solution {
- public:
- // 主函数
- vector<vector<int>> combine(int n, int k) {
- vector<vector<int>> ans;
- vector<int> comb(k, 0);
- int count = 0;
- backtracking(ans, comb, count, 1, n, k);
- return ans;
- }
- // 辅函数
- void backtracking(vector<vector<int>>& ans, vector<int>& comb, int& count, int pos, int n, int k){
- if(count == k){
- ans.push_back(comb);
- return;
- }
- for(int i=pos; i<=n; ++i){
- comb[count++] = i; // 修改当前节点状态
- backtracking(ans, comb, count, i + 1, n, k); // 递归子节点
- --count; // 回退到当前节点状态
- }
- }
- };
给定一个 m x n 二维字符网格 board 和一个字符串单词 word 。如果 word 存在于网格中,返回 true ;否则,返回 false 。
单词必须按照字母顺序,通过相邻的单元格内的字母构成,其中“相邻”单元格是那些水平相邻或垂直相邻的单元格。同一个单元格内的字母不允许被重复使用。
不同于排列组合问题,本题采用的并不是修改输出方式,而是修改访问标记。在我们对任意位置进行深度优先搜索时,我们先标记当前位置为已访问,以避免重复遍历(如防止向右搜索后又向左返回);在所有的可能都搜索完成后,再回改当前位置为未访问,防止干扰其它位置搜索到当前位置。使用回溯法,我们可以只对一个二维的访问矩阵进行修改,而不用把每次的搜索状态作为一个新对象传入递归函数中。
- class Solution {
- public:
- // 主函数
- bool exist(vector<vector<char>>& board, string word) {
- if(board.empty())
- return false;
- int m = board.size(), n = board[0].size();
- vector<vector<bool>> visited(m, vector<bool>(n, false));
- bool find = false;
- for(int i=0; i<m; ++i){
- for(int j=0; j<n;++j){
- backtracking(i, j, board, word, find, visited, 0);
- }
- }
- return find;
- }
- // 辅函数
- void backtracking(int i, int j, vector<vector<char>>& board, string word, bool& find, vector<vector<bool>>& visited, int pos){
- if(i < 0 || i >= board.size() || j < 0 || j >= board[0].size()){
- return;
- }
- if(visited[i][j] || find || board[i][j] != word[pos]){
- return;
- }
- if(pos == word.size() - 1){
- find = true;
- return;
- }
- // 修改当前节点状态
- visited[i][j] = true;
- // 递归子节点
- backtracking(i + 1, j, board, word, find, visited, pos + 1);
- backtracking(i - 1, j, board, word, find, visited, pos + 1);
- backtracking(i, j + 1, board, word, find, visited, pos + 1);
- backtracking(i, j - 1, board, word, find, visited, pos + 1);
- // 回退当前节点状态
- visited[i][j] = false;
- }
- };
按照国际象棋的规则,皇后可以攻击与之处在同一行或同一列或同一斜线上的棋子。
n 皇后问题 研究的是如何将 n 个皇后放置在 n×n 的棋盘上,并且使皇后彼此之间不能相互攻击。
给你一个整数 n ,返回所有不同的 n 皇后问题 的解决方案。
每一种解法包含一个不同的 n 皇后问题 的棋子放置方案,该方案中 'Q' 和 '.' 分别代表了皇后和空位。
类似于在矩阵中寻找字符串,本题也是通过修改状态矩阵来进行回溯。不同的是,我们需要对每一行、列、左斜、右斜建立访问数组,来记录它们是否存在皇后。本题有一个隐藏的条件,即满足条件的结果中每一行或列有且仅有一个皇后。这是因为我们一共只有 n 行和 n 列。所以如果我们通过对每一行遍历来插入皇后,我们就不需要对行建立访问 数组了。
- class Solution {
- public:
- // 主函数
- vector<vector<string>> solveNQueens(int n) {
- vector<vector<string>> ans;
- if(n == 0)
- return ans;
- vector<string> board(n, string(n, '.'));
- vector<bool> column(n, false), ldiag(2 * n - 1, false), rdiag(2 * n - 1, false);
- backtracking(ans, board, column, ldiag, rdiag, 0, n);
- return ans;
- }
- // 辅函数
- void backtracking(vector<vector<string>>& ans, vector<string>& board, vector<bool>& column, vector<bool>& ldiag, vector<bool>& rdiag, int row, int n){
- if(row == n){
- ans.push_back(board);
- return;
- }
- // 在row行寻找合适位置放置皇后
- for(int i=0; i<n; ++i){
- if(column[i] || ldiag[n - row + i - 1] || rdiag[row + i]){
- continue;
- }
- // 修改当前节点状态
- board[row][i] = 'Q';
- column[i] = ldiag[n - row + i - 1] = rdiag[row + i] = true;
- // 递归子节点
- backtracking(ans, board, column, ldiag, rdiag, row + 1, n);
- //回退当前节点状态
- board[row][i] = '.';
- column[i] = ldiag[n - row + i - 1] = rdiag[row + i] = false;
- }
- }
- };
这里要注意,深度优先搜索和广度优先搜索都可以处理可达性问题,即从一个节点开始是否能达到另一个节点。因为深度优先搜索可以利用递归快速实现,很多人会习惯使用深度优先搜索刷此类题目。实际软件工程中,笔者很少见到递归的写法,因为一方面难以理解,另一方面可能产生栈溢出的情况;而用栈实现的深度优先搜索和用队列实现的广度优先搜索在写法上并没有太大差异,因此使用哪一种搜索方式需要根据实际的功能需求来判断。
给你一个大小为 n x n 的二元矩阵 grid ,其中 1 表示陆地,0 表示水域。
岛 是由四面相连的 1 形成的一个最大组,即不会与非组内的任何其他 1 相连。grid 中 恰好存在两座岛 。
你可以将任意数量的 0 变为 1 ,以使两座岛连接起来,变成 一座岛 。
返回必须翻转的 0 的最小数目。
本题实际上是求两个岛屿间的最短距离,因此我们可以先通过任意搜索方法找到其中一个岛屿,然后利用广度优先搜索,查找其与另一个岛屿的最短距离。
- class Solution {
- public:
- vector<int> direction{-1, 0, 1, 0, -1};
- // 主函数
- int shortestBridge(vector<vector<int>>& grid) {
- int m = grid.size(), n = grid[0].size();
- queue<pair<int, int>> points;
- // dfs寻找第一个岛屿,并把1全部赋值为2
- bool flipped = false;
- for(int i=0; i<m; ++i){
- if(flipped)
- break;
- for(int j=0; j<n; ++j){
- if(grid[i][j] == 1){
- dfs(points, grid, m, n, i, j);
- flipped = true;
- break;
- }
- }
- }
- // bfs寻找第二个岛屿,并把过程中经过的0赋值为2
- int x, y;
- int level = 0;
- while(!points.empty()){
- ++level;
- int n_points = points.size();
- while(n_points--){
- auto [r, c] = points.front();
- grid[r][c] = 2;
- points.pop();
- for(int k=0; k<4; ++k){
- x = r + direction[k], y = c + direction[k + 1];
- if(x >= 0 && y >= 0 && x < m && y < n){
- if(grid[x][y] == 2){
- continue;
- }
- if(grid[x][y] == 1){
- return level;
- }
- points.push({x, y});
- grid[x][y] = 2;
- }
- }
- }
- }
- return 0;
- }
- // 辅函数
- void dfs(queue<pair<int, int>>& points, vector<vector<int>>& grid, int m, int n, int i, int j){
- if(i < 0 || j < 0 || i >= m || j >= n || grid[i][j] == 2){
- return;
- }
- if(grid[i][j] == 0){
- points.push({i, j});
- return;
- }
- grid[i][j] = 2;
- dfs(points, grid, m, n, i - 1, j);
- dfs(points, grid, m, n, i + 1, j);
- dfs(points, grid, m, n, i, j - 1);
- dfs(points, grid, m, n, i, j + 1);
- }
- };
按字典 wordList 完成从单词 beginWord 到单词 endWord 转化,一个表示此过程的 转换序列 是形式上像 beginWord -> s1 -> s2 -> ... -> sk 这样的单词序列,并满足:
每对相邻的单词之间仅有单个字母不同。
转换过程中的每个单词 si(1 <= i <= k)必须是字典 wordList 中的单词。注意,beginWord 不必是字典 wordList 中的单词。
sk == endWord
给你两个单词 beginWord 和 endWord ,以及一个字典 wordList 。请你找出并返回所有从 beginWord 到 endWord 的 最短转换序列 ,如果不存在这样的转换序列,返回一个空列表。每个序列都应该以单词列表 [beginWord, s1, s2, ..., sk] 的形式返回。
我们可以把起始字符串、终止字符串、以及单词表里所有的字符串想象成节点。若两个字符串只有一个字符不同,那么它们相连。因为题目需要输出修改次数最少的所有修改方式,因此我 们可以使用广度优先搜索,求得起始节点到终止节点的最短距离。我们同时还使用了一个小技巧:我们并不是直接从起始节点进行广度优先搜索,直到找到终止节点为止;而是从起始节点和终止节点分别进行广度优先搜索,每次只延展当前层节点数最少的那一端,这样我们可以减少搜索的总结点数。举例来说,假设最短距离为 4 ,如果我们只从一 端搜索 4 层,总遍历节点数最多是 1 + 2 + 4 + 8 + 16 = 31 ;而如果我们从两端各搜索两层,总遍 历节点数最多只有 2 × ( 1 + 2 + 4 ) = 14 。在搜索结束后,我们还需要通过回溯法来重建所有可能的路径。
- class Solution {
- public:
- vector<vector<string>> findLadders(string beginWord, string endWord, vector<string>& wordList) {
- vector<vector<string>> ans;
- unordered_set<string> dict;
- for(const auto &w: wordList){
- dict.insert(w);
- }
- if(!dict.count(endWord)) {
- return ans;
- }
- dict.erase(beginWord);
- dict.erase(endWord);
- unordered_set<string> q1{beginWord}, q2{endWord};
- unordered_map<string, vector<string>> next;
- bool reversed = false, found = false;
- while(!q1.empty()){
- unordered_set<string> q;
- for(const auto &w: q1){
- string s = w;
- for(size_t i=0; i<s.size(); i++){
- char ch = s[i];
- for(int j=0; j<26; j++){
- s[i] = j + 'a';
- if(q2.count(s)){
- reversed? next[s].push_back(w): next[w].push_back(s);
- found = true;
- }
- if(dict.count(s)){
- reversed? next[s].push_back(w): next[w].push_back(s);
- q.insert(s);
- }
- }
- s[i] = ch;
- }
- }
- if(found)
- break;
- for(const auto &w:q)
- dict.erase(w);
- if(q.size() <= q2.size()){
- q1 = q;
- }else{
- reversed = !reversed;
- q1 = q2;
- q2 = q;
- }
- }
- if(found){
- vector<string> path = {beginWord};
- backtracking(beginWord, endWord, next, path, ans);
- }
- return ans;
- }
-
- void backtracking(const string &src, const string &dst, unordered_map<string,
- vector<string>> &next, vector<string> &path, vector<vector<string>> &ans) {
- if (src == dst) {
- ans.push_back(path);
- return;
- }
- for (const auto &s: next[src]) {
- path.push_back(s);
- backtracking(s, dst, next, path, ans);
- path.pop_back();
- }
- }
- };
欢迎大家共同学习和纠正指教
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。