一. 树形问题 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;
}
};