1.1 Word Search 给定一个二维平板和一个单词,请找出这个单词是否在二维平板中出现。
单词可以由平板中的邻接单元组成,这里的“邻接”定义为上下左右四个方向。
同一个单元上的字母最多只能使用一次。
在深度优先搜索中,最重要的就是考虑好搜索顺序。
我们先枚举单词的起点,然后依次枚举单词的每个字母。
过程中需要将已经使用过的字母改成一个特殊字母,以避免重复使用字符。
时间复杂度分析:单词起点一共有 n2n2 个,单词的每个字母一共有上下左右四个方向可以选择,但由于不能走回头路,所以除了单词首字母外,仅有三种选择。所以总时间复杂度是 O(n2*3k)。
class Solution {
public:
bool exist(vector<vector<char>>& board, string str) {
for (int i = 0; i < board.size(); i ++ )
for (int j = 0; j < board[i].size(); j ++ )
if (dfs(board, str, 0, i, j))
return true;
return false;
}
bool dfs(vector<vector<char>> &board, string &str, int u, int x, int y) {
//不匹配直接返回false,进入下一步循环,匹配则继续执行
if (board[x][y] != str[u]) return false;
//字符串全部匹配成果则返回true
if (u == str.size() - 1) return true;
int dx[4] = {
-1, 0, 1, 0}, dy[4] = {
0, 1, 0, -1};
char t = board[x][y];
board[x][y] = '*';
for (int i = 0; i < 4; i ++ ) {
//每个字母都在四个方向去遍历
int a = x + dx[i], b = y + dy[i];
if (a >= 0 && a < board.size() && b >= 0 && b < board[a].size()) {
//不匹配则回溯回该字母的下一个方向
if (dfs(board, str, u + 1, a, b)) return true;
}
}
board[x][y] = t;//四个方向都不匹配,则回溯回上一个字母的下一个方向,并恢复现场
return false;
}
};
1.2 给定一个仅包含数字 2-9 的字符串,返回所有它能表示的字母组合。
给出数字到字母的映射如下(与电话按键相同)。注意 1 不对应任何字母。
(递归) O(4^l)
- 可以通过手工或者循环的方式预处理每个数字可以代表哪些字母。
- 通过递归尝试拼接一个新字母。 递归到目标长度,将当前字母串加入到答案中。
- 注意,有可能数字串是空串,需要特判。
class Solution {
public:
vector<char> digit[10];
vector<string> res;
void init() {
char cur = 'a';
for (int i = 2; i < 10; i++) {
for (int j = 0; j < 3; j++)
digit[i].push_back(cur++);
if (i == 7 || i == 9)
digit[i].push_back(cur++);
}
}
void solve(string digits, int d, string cur) {
if (d == digits.length()) {
res.push_back(cur);
return;
}
int cur_num = digits[d] - '0';
for (int i = 0; i < digit[cur_num].size(); i++)
solve(digits, d + 1, cur + digit[cur_num][i]);
}
vector<string> letterCombinations(string digits) {
if (digits == "")
return res;
init();
solve(digits, 0, "");
return res;
}
};
1.3 给定一个字符串,只包含数字。请解码出所有合法的IP地址。
(暴力搜索) O(C3n−1)
直接暴力搜索出所有合法方案。
合法的IP地址由四个0到255的整数组成。我们直接枚举四个整数的位数,然后判断每个数的范围是否在0到255。
时间复杂度分析:一共 n个数字,n−1个数字间隔,相当于从 n−1个数字间隔中挑3个断点,所以计算量是 O(C3n−1)
class Solution {
public:
vector<string> ans;
vector<int> path;
vector<string> restoreIpAddresses(string s) {
dfs(0, 0, s);
return ans;
}
// u表示枚举到的字符串下标,k表示当前截断的IP个数,s表示原字符串
void dfs(int u, int k, string &s)
{
if (u == s.size())
{
if (k == 4)
{
string ip = to_string(path[0]);
for (int i = 1; i < 4; i ++ )
ip += '.' + to_string(path[i]);
ans.push_back(ip);
}
return;
}
if (k > 4) return;
unsigned t = 0;
for (int i = u; i < s.size(); i ++ )
{
t = t * 10 + s[i] - '0';
if (t >= 0 && t < 256)
{
path.push_back(t);
dfs(i + 1, k + 1, s);
path.pop_back();
}
if (!t) break;
}
}
};
2 排列
2.1 给出一列互不相同的整数,返回其全排列。
(回溯) O(n×n!)
我们从前往后,一位一位枚举,每次选择一个没有被使用过的数。
选好之后,将该数的状态改成“已被使用”,同时将该数记录在相应位置上,然后递归。
递归返回时,不要忘记将该数的状态改成“未被使用”,并将该数从相应位置上删除。
时间复杂度分析:
搜索树中最后一层共 n! 个叶节点,在叶节点处记录方案的计算量是 O(n),所以叶节点处的计算量是 O(n×n!)
搜索树一共有 n!+n!2!+n!3!+…=n!(1+12!+13!+…)≤n!(1+12+14+18+…)=2n! 个内部节点,在每个内部节点内均会for循环 n次,因此内部节点的计算量也是 O(n×n!)。 所以总时间复杂度是 O(n×n!)
class Solution {
public:
vector<vector<int>> res;
vector<bool> st;
vector<int> path;
vector<vector<int> > permute(vector<int> &num) {
for(int i=0;i<num.size();i++)
st.push_back(false);
dfs(num,0);
return res;
}
void dfs(vector<int> &num,int u){
if(u==num.size()){
res.push_back(path);
return ;
}
for(int i=0;i<num.size();i++){
if(!st[i]){
st[i]=true;
path.push_back(num[i]);
dfs(num,u+1);
st[i]=false;
path.pop_back();
}
}
}
};
2.2 给出一组可能包含重复项的数字,返回该组数字的所有排列
(回溯) O(n!)
由于有重复元素的存在,这道题的枚举顺序和 Permutations 不同。
- 先将所有数从小到大排序,这样相同的数会排在一起;
- 从左到右依次枚举每个数,每次将它放在一个空位上;
- 对于相同数,我们人为定序,就可以避免重复计算:我们在dfs时记录一个额外的状态,记录上一个相同数存放的位置start,我们在枚举当前数时,只枚举 start+1,start+2,…,n这些位置。
不要忘记递归前和回溯时,对状态进行更新。
时间复杂度分析: 搜索树中最后一层共 n!个节点,前面所有层加一块的节点数量相比于最后一层节点数是无穷小量,可以忽略。且最后一层节点记录方案的计算量是 O(n),所以总时间复杂度是 O(n×n!)。
class Solution {
public:
vector<bool> st;
vector<int> path;
vector<vector<int>> ans;
vector<vector<int>> permuteUnique(vector<int>& nums) {
sort(nums.begin(), nums.end());
st = vector<bool>(nums.size(), false);
path = vector<int>(nums.size());
dfs(nums, 0, 0);
return ans;
}
void dfs(vector<int>& nums, int u, int start)
{
if (u == nums.size())
{
ans.push_back(path);
return;
}
for (int i = start; i < nums.size(); i ++ )
if (!st[i])
{
st[i] = true;
path[i] = nums[u];
if (u + 1 < nums.size() && nums[u + 1] != nums[u])//排序后相同的数在一起
dfs(nums, u + 1, 0);
else
dfs(nums, u + 1, i + 1);
st[i] = false;
}
}
};
如果需要按照字典序排列,则采用方法二
/*枚举每个位置上放哪个数
以[1,1,2]为例:
[ , , ]
/ |
/ |
[1, , ] [2 ,, ]
/ \ |
/ \ |
[1,1, ] [1,2, ] [2,1, ]
| | |
[1,1,2] [1,2,1] [2,1,1]
*/
class Solution {
public:
vector<bool> st;
vector<vector<int>> res;
vector<int> path;
vector<vector<int> > permuteUnique(vector<int> &num) {
sort(num.begin(),num.end());
int n=num.size();
st=vector<bool> (n,false);
path=vector<int> (n);
if(n==0)return res;
dfs(num,0);
return res;
}
void dfs(vector<int> &num,int u){
if(u==num.size()){
res.push_back(path);
return ;
}
for(int i=0;i<num.size();i++){
if(i>0&&st[i-1]&&num[i-1]==num[i])
continue;
if(!st[i]){
st[i]=true;
path[u]=num[i];
dfs(num,u+1);
st[i]=false;
}
}
}
};
2.3 排列序列
(计数) O(n2)
做法:
从高位到低位依次考虑每一位;
对于每一位,从小到大依次枚举未使用过的数,确定当前位是几;
为了便于理解,我们这里给出一个例子的具体操作:n=4,k=14。
首先我们将所有排列按首位分组:
1 + (2, 3, 4的全排列)
2 + (1, 3, 4的全排列)
3 + (1, 2, 4的全排列)
4 + (2, 3, 4的全排列)
接下来我们确定第 k=14个排列在哪一组中。每组的排列个数是 3!=6个,所以第14个排列在第3组中,所以首位已经可以确定,是3。
然后我们再将第3组的排列继续分组:
31 + (2, 4的全排列)
32 + (1, 4的全排列)
34 + (1, 2的全排列)
接下来我们判断第 k=14 个排列在哪个小组中。我们先求第 14个排列在第三组中排第几,由于前两组每组有6个排列,所以第14个排列在第3组排第 14−6∗2=2。
在第三组中每个小组的排列个数是 2!=2个,所以第 k个排列在第1个小组,所以可以确定它的第二位数字是1。
依次类推,可以推出第14个排列是 3142。
时间复杂度分析:两重循环,所以时间复杂度是 O(n2)。
class Solution {
public:
string getPermutation(int n, int k) {
string res;
vector<bool>