7. 递归和回溯法

一. 树形问题 Letter Combinations of a Phone Number

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

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

示例:

输入:"23"
输出:["ad", "ae", "af", "bd", "be", "bf", "cd", "ce", "cf"].
说明:
尽管上面的答案是按字典序排列的,但是你可以任意选择答案输出的顺序。

解题思路

该问题是一个 类树形结构问题

              2
          a/ b| c\
         /    |    \
        3     3     3
    d/ e| f\  ..     ..
   /    |    \
 ad    ae     af
 
 

假设
digits 是数字字符串
s(digits)是digits所能代表的字母字符串

s(digits[0...n-1])
=letter(digits[0] + s(digits[1...n-1])
=letter(digits[0] + letter(digits[1]) + s(digits[2...n-1])
...

代码

#include <iostream>
#include <vector>
#include <string>
#include <cassert>

using namespace std;

/// 17. Letter Combinations of a Phone Number
/// https://leetcode.com/problems/letter-combinations-of-a-phone-number/description/
/// 时间复杂度: O(2^len(s))
/// 空间复杂度: O(len(s))
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
    };

    vector<string> res;

    // s中保存了此时从digits[0...index-1]翻译得到的一个字母字符串
    // 寻找和digits[index]匹配的字母, 获得digits[0...index]翻译得到的解
    void findCombination(const string &digits, int index, const string &s){

        cout << index << " : " << s << endl;
        if(index == digits.size()){
            res.push_back(s);
            cout << "get " << s << " , return" << endl;
            return;
        }

        char c = digits[index];
        assert(c >= '0' && c <= '9' && c != '1');
        string letters = letterMap[c - '0'];
        for(int i = 0 ; i < letters.size() ; i ++){
            cout << "digits[" << index << "] = " << c << " , use " << letters[i] << endl;
            findCombination(digits, index+1, s + letters[i]);
        }

        cout << "digits[" << index << "] = " << c << " complete, return" << endl;

        return;
    }

public:
    vector<string> letterCombinations(string digits) {

        res.clear();
        if(digits == "")
            return res;

        findCombination(digits, 0, "");

        return res;
    }
};

int main() {

    vector<string> res = Solution().letterCombinations("234");
    for(int i = 0 ; i < res.size() ; i ++)
        cout << res[i] << endl;

    return 0;
}

时间复杂度

  • 使用回溯的方法解决的问题, 时间复杂度为: 3^n = O(2^n)
  • 回溯法是 暴力解法的一个主要实现手段
  • 不知道循环次数的情况下, 回溯法是个合适的方法

后面会探讨回溯法的应用


二. 排列问题 Permutations

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

示例:

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

解题思路

Perms( nums[0...n-1]) = {取出一个数字} + 
                        Perms(nums[{0...n-1} - 这个数字])

代码

#include <iostream>
#include <vector>
using namespace std;

/// 46. Permutations
/// https://leetcode.com/problems/permutations/description/
/// 时间复杂度: O(n^n)
/// 空间复杂度: O(n)
class Solution {

private:

    vector<vector<int>> res;   // 存放  每种 排列结果
    vector<bool> used;

    // p中保存了一个有index-1个元素的排列。
    // 向这个排列的末尾添加第index个元素, 获得一个有index个元素的排列
    void generatePermutation(const vector<int>& nums, int index, vector<int>& p){

        if(index == nums.size()){
            res.push_back(p);
            return;
        }

        for(int i = 0 ; i < nums.size() ; i ++)
            if(!used[i]){
                used[i] = true;
                p.push_back(nums[i]);
                generatePermutation(nums, index + 1, p );
                p.pop_back();   // 回溯   --后面会使用当前的nums[i], 只是排序位置不同了
                used[i] = false;
            }

        return;
    }

public:
    vector<vector<int>> permute(vector<int>& nums) {

        res.clear();
        if(nums.size() == 0)
            return res;

        used = vector<bool>(nums.size(), false);
        vector<int> p;
        generatePermutation(nums, 0, p);

        return res;
    }
};

如果用python, 有内置方法permutations可以使用

import itertools 



class Solution:
    def permute(self, nums):
        """
        :type nums: List[int]
        :rtype: List[List[int]]
        """
        return list(itertools.permutations(nums))

三. 组合问题 Combinations

给定两个整数 n 和 k,返回 1 ... n 中所有可能的 k 个数的组合。

示例:

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

解题思路

代码

#include <iostream>
#include <vector>

using namespace std;

/// 77. Combinations
/// https://leetcode.com/problems/combinations/description/
/// 时间复杂度: O(n^k)
/// 空间复杂度: O(k)
class Solution {
private:
    vector<vector<int>> res;  // 存放所有 组合结果

    // 求解C(n,k), 当前已经找到的组合存储在c中, 需要从start开始搜索新的元素
    void generateCombinations(int n, int k, int start, vector<int> &c){

        if(c.size() == k){
            res.push_back(c);
            return;
        }

        for(int i = start ; i <= n ; i ++){
            c.push_back( i );
            generateCombinations(n, k, i + 1, c);
            c.pop_back();  //回溯
        }

        return;
    }
public:
    vector<vector<int>> combine(int n, int k) {

        res.clear();
        if( n <= 0 || k <= 0 || k > n )
            return res;

        vector<int> c;
        generateCombinations(n, k, 0, c);

        return res;
    }
};

int main() {

    vector<vector<int>> res = Solution().combine(4,2);
    for( int i = 0 ; i < res.size() ; i ++ ){
        for( int j = 0 ; j < res[i].size() ; j ++ )
            cout<<res[i][j]<<" ";
        cout<<endl;
    }
    return 0;
}

python版

class Solution:
    def __init__(self):
        self.res = []
        self.oneCombin = []
    
    def generateCombinations(self, n ,k, start):
            if len(self.oneCombin) == k:
                self.res.append(self.oneCombin.copy())  # 不加copy 传入的是一个引用
                return

            for i in range(start, n+1):
                self.oneCombin.append(i)
                self.generateCombinations(n, k, i+1)
                self.oneCombin.pop()   # 回溯

            return
        
    def combine(self, n, k):
        """
        :type n: int
        :type k: int
        :rtype: List[List[int]]
        """
        self.res = []
        self.oneCombin = []
        
        if n <=0 or n <= 0 or k > n:
            return self.res
        
        
        self.generateCombinations(n, k, 1)
        
        return self.res
        
        

四. 回溯法解决组合问题的优化

  • 依然针对上一节 的 组合问题

剪枝思想

以在 [1,2,3,4] 中取 两个数作为组合,   为例子

1. 在遍历到 4的时候, 发现其实 4 根本不需要考虑, 因为 单独一个4 无法作为组合
2. 当 数据很大, k也很大, 这样的优化 会 有很好的优化效果

代码

c++

/// 77. Combinations
/// https://leetcode.com/problems/combinations/description/
/// 时间复杂度: O(n^k)
/// 空间复杂度: O(k)
class Solution {
private:
    vector<vector<int>> res;

    // 求解C(n,k), 当前已经找到的组合存储在c中, 需要从start开始搜索新的元素
    void generateCombinations(int n, int k, int start, vector<int> &c){

        if(c.size() == k){
            res.push_back(c);
            return;
        }

        // 还有k - c.size()个空位, 所以, [i...n] 中至少要有 k - c.size() 个元素
        // i最多为 n - (k - c.size()) + 1
        for(int i = start; i <= n - (k - c.size()) + 1 ; i ++){
            c.push_back(i);
            generateCombinations(n, k, i + 1 ,c);
            c.pop_back();
        }

        return;
    }
public:
    vector<vector<int>> combine(int n, int k) {

        res.clear();
        if(n <= 0 || k <= 0 || k > n)
            return res;

        vector<int> c;
        generateCombinations(n, k, 1, c);

        return res;
    }
};

python

class Solution:
    def __init__(self):
        self.res = []
        self.oneCombin = []
    
    def generateCombinations(self, n ,k, start):
            if len(self.oneCombin) == k:
                self.res.append(self.oneCombin.copy())  # 不加copy 传入的是一个引用
                return

            # 假设有 range(1, 10)  它是不包含10的, 这点要注意, 所以这里n+2
            for i in range(start, n+2 - (k-len(self.oneCombin))): # 剪枝 思想进行优化
                self.oneCombin.append(i)
                self.generateCombinations(n, k, i+1)
                self.oneCombin.pop()

            return
        
    def combine(self, n, k):
        """
        :type n: int
        :type k: int
        :rtype: List[List[int]]
        """
        self.res = []
        self.oneCombin = []
        
        if n <=0 or n <= 0 or k > n:
            return self.res
        
        
        self.generateCombinations(n, k, 1)
        
        return self.res
        

五. 二维平面上的回溯法 Word Search

给定一个二维网格和一个单词,找出该单词是否存在于网格中。

单词必须按照字母顺序,通过相邻的单元格内的字母构成,其中“相邻”单元格是那些水平相邻或垂直相邻的单元格。同一个单元格内的字母不允许被重复使用。

示例:

board =
[
 ['A','B','C','E'],
 ['S','F','C','S'],
 ['A','D','E','E']
]

给定 word = "ABCCED", 返回 true.
给定 word = "SEE", 返回 true.
给定 word = "ABCB", 返回 false.

回溯法解题

#include <iostream>
#include <vector>
#include <cassert>

using namespace std;

/// 79. Word Search
/// Source : https://leetcode.com/problems/word-search/description/
///
/// 回溯法
/// 时间复杂度: O(m*n*m*n)
/// 空间复杂度: O(m*n)
class Solution {

private:
    int d[4][2] = {{-1, 0}, {0,1}, {1, 0}, {0, -1}};  // 代表四个方向
    int m, n;
    vector<vector<bool>> visited;

    bool inArea(int x, int y){
        return x >= 0 && x < m && y >= 0 && y < n;
    }

    // 从board[startx][starty]开始, 寻找word[index...word.size())
    bool searchWord(const vector<vector<char>> &board, const string& word, int index,
                    int startx, int starty ){

        //assert(inArea(startx,starty));
        if(index == word.size() - 1)
            return board[startx][starty] == word[index];

        if(board[startx][starty] == word[index]){
            visited[startx][starty] = true;
            // 从startx, starty出发,向四个方向寻
            for(int i = 0 ; i < 4 ; i ++){
                int newx = startx + d[i][0];
                int newy = starty + d[i][1];
                if(inArea(newx, newy) && !visited[newx][newy] &&
                   searchWord(board, word, index + 1, newx, newy))
                    return true;
            }
            visited[startx][starty] = false;
        }
        return false;
    }

public:
    bool exist(vector<vector<char>>& board, string word) {

        m = board.size();
        assert(m > 0);
        n = board[0].size();
        assert(n > 0);

        visited.clear();
        for(int i = 0 ; i < m ; i ++)
            visited.push_back(vector<bool>(n, false));

        for(int i = 0 ; i < board.size() ; i ++)
            for(int j = 0 ; j < board[i].size() ; j ++)
                if(searchWord(board, word, 0, i, j))
                    return true;

        return false;
    }
};

int main() {

    char b1[3][4] = {{'A', 'B', 'C', 'E'},
                     {'S', 'F', 'C', 'S'},
                     {'A', 'D', 'E', 'E'}};
    vector<vector<char>> board1;
    for( int i = 0 ; i < 3 ; i ++ )
        board1.push_back(vector<char>(b1[i], b1[i] + sizeof(b1[i]) / sizeof(char)));

    int cases = 3;
    string words[3] = {"ABCCED", "SEE", "ABCB" };
    for(int i = 0 ; i < cases ; i ++)
        if(Solution().exist(board1,words[i]))
            cout << "found " << words[i] << endl;
        else
            cout << "can not found " << words[i] << endl;

    // ---

    char b2[1][1] = {{'A'}};
    vector<vector<char>> board2;
    for(int i = 0 ; i < 3 ; i ++)
        board2.push_back(vector<char>(b2[i], b2[i] + sizeof(b2[i])/sizeof(char)));

    if(Solution().exist(board2,"AB"))
        cout << "found AB" << endl;
    else
        cout << "can not found AB" << endl;

    return 0;
}

六. floodfill算法, 一类经典问题 Number of islands

给定一个由 '1'(陆地)和 '0'(水)组成的的二维网格,计算岛屿的数量。一个岛被水包围,并且它是通过水平方向或垂直方向上相邻的陆地连接而成的。你可以假设网格的四个边均被水包围。

示例 1:

输入:
11110
11010
11000
00000

输出: 1
示例 2:

输入:
11000
11000
00100
00011

输出: 3

解题

#include <iostream>
#include <vector>
#include <cassert>

using namespace std;

/// 200. Number of Islands
/// https://leetcode.com/problems/number-of-islands/description/
/// 时间复杂度: O(n*m)
/// 空间复杂度: O(n*m)
class Solution {

private:
    int d[4][2] = {{0, 1}, {1, 0}, {0, -1}, {-1, 0}};  // 方向
    int m, n;     // 范围
    vector<vector<bool>> visited;  // 是否被访问

    bool inArea(int x, int y){   // 是否在范围中
        return x >= 0 && x < m && y >= 0 && y < n;  
    }

    // 从grid[x][y]的位置开始,进行floodfill
    // 保证(x,y)合法,且grid[x][y]是没有被访问过的陆地
    void dfs(vector<vector<char>>& grid, int x, int y){

        //assert(inArea(x,y));
        visited[x][y] = true;
        for(int i = 0; i < 4; i ++){
            int newx = x + d[i][0];
            int newy = y + d[i][1];
            if(inArea(newx, newy) && !visited[newx][newy] && grid[newx][newy] == '1')
                dfs(grid, newx, newy);
        }

        return;
    }

public:
    int numIslands(vector<vector<char>>& grid) {

        m = grid.size();
        if(m == 0)
            return 0;
        n = grid[0].size();
        if(n == 0)
            return 0;

        for(int i = 0 ; i < m ; i ++)
            visited.push_back(vector<bool>(n, false));

        int res = 0;
        for(int i = 0 ; i < m ; i ++)
            for(int j = 0 ; j < n ; j ++)
                if(grid[i][j] == '1' && !visited[i][j]){  // if语句中  把第一次递归的  前提条件  都放入了
                    dfs(grid, i, j);  // 同属于 一个岛屿的 陆地  进行标记
                    res ++;
                }
        return res;
    }
};

七. 回溯法是经典人工智能的基础 N-Queens

n 皇后问题研究的是如何将 n 个皇后放置在 n×n 的棋盘上,并且使皇后彼此之间不能相互攻击。
(同一 水平线, 垂直线, 对角线上  都能攻击)

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

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

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

示例:

输入: 4
输出: [
 [".Q..",  // 解法 1
  "...Q",
  "Q...",
  "..Q."],

 ["..Q.",  // 解法 2
  "Q...",
  "...Q",
  ".Q.."]
]
解释: 4 皇后问题存在两个不同的解法。

解题思路

n x n的棋盘格(这里n=4):
00  01  02  03
10  11  12  13
20  21  22  23
30  31  32  33

除了上下左右的判断, 还有对角线的判断:


左下到右上 的 对角线 规律:  i + j (i横向位置, j纵向位置)

左上到右下 的 对角线 规律:  i - j (防止值<0, 我们在代码中 再加上n-1)   

两种对角线  个数都是  2n-1    

代码

#include <iostream>
#include <vector>
#include <cassert>

using namespace std;

/// 51. N-Queens
/// https://leetcode.com/problems/n-queens/description/
/// 时间复杂度: O(n^n)
/// 空间复杂度: O(n)
class Solution {
private:
    vector<bool> col, dia1, dia2;   // col 是否同一列, dia1 dia2  表示两中对角线
    vector<vector<string>> res;

    // 尝试在一个n皇后问题中, 摆放第index行的皇后位置
    void putQueen(int n, int index, vector<int> &row){  // row:每一行的皇后  放在第几列

        if(index == n){
            res.push_back(generateBoard(n, row));
            return;
        }

        for(int i = 0 ; i < n ; i ++)
            // 尝试将第index行的皇后摆放在第i列
            if(!col[i] && !dia1[index + i] && !dia2[index - i + n - 1]){
                row.push_back(i);
                col[i] = true;
                dia1[index + i] = true;
                dia2[index - i + n - 1] = true;
                putQueen(n, index + 1, row);  // 如果成功找到, 递归到最后 index会=n   并把结果放入 res
                col[i] = false;   // 回溯
                dia1[index + i] = false;
                dia2[index - i + n - 1] = false;
                row.pop_back();
            }

        return;
    }

    // 将vector形式的解  转化为   string 形式的解
    vector<string> generateBoard(int n, vector<int> &row){
        assert(row.size() == n);
        vector<string> board(n, string(n, '.'));
        for(int i = 0 ; i < n ; i ++)
            board[i][row[i]] = 'Q';
        return board;
    }

public:
    vector<vector<string>> solveNQueens(int n) {

        res.clear();

        col.clear();
        for(int i = 0 ; i < n ; i ++)
            col.push_back(false);

        dia1.clear();
        dia2.clear();
        for(int i = 0 ; i < 2 * n - 1 ; i ++){
            dia1.push_back(false);
            dia2.push_back(false);
        }

        vector<int> row;
        putQueen(n, 0, row);

        return res;
    }
};

猜你喜欢

转载自blog.csdn.net/weixin_41207499/article/details/84851500