赞
踩
给定一个不含重复数字的数组 nums ,返回其 所有可能的全排列 。你可以 按任意顺序 返回答案。
示例:
输入:nums = [1,2,3]
输出:[[1,2,3],[1,3,2],[2,1,3],[2,3,1],[3,1,2],[3,2,1]]
本题为无重复数字的全排列问题,方法二使用路过改值的方法,每次选择不会选择已选择过的元素。
回溯的经典解法,本质上是个多叉树的遍历问题,关键就是在前序遍历位置和后序遍历位置做一些操作。本质上还是纯暴力穷举。
class Solution {
public:
vector<vector<int>> res;
vector<int> track;
vector<vector<int>> permute(vector<int>& nums) {
backtrack(nums);
return res;
}
void backtrack(vector<int>& nums){
// 结束条件
if (track.size() == nums.size()){
res.push_back(track);
return;
}
// 循环遍历
for (int i : nums){
// 排除不合法的选择
if (find(track.begin(), track.end(), i) != track.end())
continue;
// 做选择
track.push_back(i);
// 进入下一层决策树
backtrack(nums);
// 取消选择
track.pop_back();
}
}
};
上述代码在排除不合法的选择时使用了find方法,带入了O(n)的时间复杂度,可以通过路过改值来降低时间复杂度,在取消选择时把值改回来即可。
class Solution {
public:
vector<vector<int>> res;
vector<int> track;
vector<vector<int>> permute(vector<int>& nums) {
backtrack(nums);
return res;
}
void backtrack(vector<int>& nums){
// 结束条件
if (track.size() == nums.size()){
res.push_back(track);
return;
}
// 循环遍历
for (int i = 0; i < nums.size(); i++){
// 排除不合法的选择
if (nums[i] == 999) continue;
int temp = nums[i];
// 做选择
nums[i] = 999;
track.push_back(temp);
// 进入下一层决策树
backtrack(nums);
// 取消选择
track.pop_back();
nums[i] = temp;
}
}
};
给定一个可包含重复数字的序列 nums ,按任意顺序 返回所有不重复的全排列。
示例:
输入:nums = [1,1,2]
输出:
[[1,1,2],
[1,2,1],
[2,1,1]]
本题为上题的进阶版本,为含重复数字的不重复全排列问题,使用路过改值的方法,每次选择不会选择已选择过的元素。且某数退出选择后,其之后相同的数还有可能被选择,使用相邻数比较的方法,与路过改值同时使用,可以实现完美横向剪枝。
首先排序保证相同的数在一起,再对相邻的数大小进行判断,如果相同则进行剪枝(此处需要深刻体会,非常精妙)。
class Solution {
public:
vector<vector<int>> res;
vector<int> track;
vector<vector<int>> permuteUnique(vector<int>& nums) {
// 先排序,保证相同的数在一起
sort(nums.begin(), nums.end());
backtrack(nums);
return res;
}
void backtrack(vector<int>& nums) {
int len = nums.size();
for (int i = 0; i < len; i++) {
if (track.size() == len) {
res.push_back(track);
return;
}
// 排除不合法的选择
if (nums[i] == 999) continue; // 纵向剪枝,不走回头路
if (i >= 1 && nums[i] == nums[i - 1]) continue; // 横向剪枝,非常精妙
int temp = nums[i];
// 做选择
nums[i] = 999;
track.push_back(temp);
// 进入下一层决策树
backtrack(nums);
// 取消选择
track.pop_back();
nums[i] = temp;
}
}
};
给你一个 无重复元素 的整数数组 candidates 和一个目标整数 target ,找出 candidates 中可以使数字和为目标数 target 的 所有 不同组合 ,并以列表形式返回。你可以按 任意顺序 返回这些组合。
candidates 中的 同一个 数字可以 无限制重复被选取 。如果至少一个数字的被选数量不同,则两种组合是不同的。
对于给定的输入,保证和为 target 的不同组合数少于 150 个。
示例:
输入:candidates = [2,3,6,7], target = 7
输出:[[2,2,3],[7]]
解释:
2 和 3 可以形成一组候选,2 + 2 + 3 = 7 。注意 2 可以使用多次。
7 也是一个候选, 7 = 7 。
仅有这两种组合。
本题为无重复数字的可重复选取回溯问题,且元素均为正数。
在选取过程中,不可能走回头路,故使用index记录遍历的开始位置,且若当前sum + candidates[i] > target
,需及时break退出循环,会比下一次判断sum > target
更优,避免了接下来所有无效的遍历。
class Solution {
public:
vector<vector<int>> res;
vector<int> comb;
vector<vector<int>> combinationSum(vector<int>& candidates, int target) {
sort(candidates.begin(), candidates.end());
backtrack(candidates, target, 0, 0);
return res;
}
void backtrack(vector<int> candidates, int target, int sum, int index) {
if (sum == target) {
res.push_back(comb);
return;
}
for (int i = index; i < candidates.size(); i++) {
if (sum + candidates[i] > target) break;
// 做选择
comb.push_back(candidates[i]);
backtrack(candidates, target, sum + candidates[i], i);
// 取消选择
comb.pop_back();
}
}
};
给定一个候选人编号的集合 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]
]
本题为有重复数字的不可重复选取回溯问题,且元素均为正数,与全排列 II非常相似。
在选取过程中,为避免某数退出选择后,其之后相同的数还有可能被选择的问题,依然采用路过改值+相邻数比较的方法,实现横向剪枝,同时搭配index记录遍历的开始位置。
class Solution {
public:
vector<vector<int>> res;
vector<int> track;
vector<vector<int>> combinationSum2(vector<int>& candidates, int target) {
sort(candidates.begin(), candidates.end());
backtrack(candidates, target, 0, 0);
return res;
}
void backtrack(vector<int>& candidates, int target, int sum, int index) {
if (sum == target) {
res.push_back(track);
return;
}
for (int i = index; i < candidates.size(); i++) {
if (sum + candidates[i] > target) break;
if (candidates[i] == 9999) continue;
// 相邻数比较
if (i >= 1 && candidates[i] == candidates[i - 1]) continue;
// 路过改值
int t = candidates[i];
candidates[i] = 9999;
// 做选择
track.push_back(t);
backtrack(candidates, target, sum + t, i + 1);
// 取消选择
track.pop_back();
candidates[i] = t;
}
}
};
但其实还有更漂亮的只相邻数比较的方法,只需在判断中加入i > index
即可,避免了路过改值的麻烦。
class Solution {
public:
vector<vector<int>> res;
vector<int> track;
vector<vector<int>> combinationSum2(vector<int>& candidates, int target) {
sort(candidates.begin(), candidates.end());
backtrack(candidates, target, 0, 0);
return res;
}
void backtrack(vector<int>& candidates, int target, int sum, int index) {
if (sum == target) {
res.push_back(track);
return;
}
for (int i = index; i < candidates.size(); i++) {
if (sum + candidates[i] > target) break;
// 剪枝
if (i > index && candidates[i] == candidates[i - 1]) continue;
// 做选择
track.push_back(candidates[i]);
backtrack(candidates, target, sum + candidates[i], i + 1);
track.pop_back();
}
}
};
给定一个字符串 s ,通过将字符串 s 中的每个字母转变大小写,我们可以获得一个新的字符串。
返回 所有可能得到的字符串集合 。以 任意顺序 返回输出。
示例:
输入:s = “a1b2”
输出:[“a1b2”, “a1B2”, “A1b2”, “A1B2”]
本题为只更改当前字符回溯问题,在backtrack
回溯方法中不需要使用for
循环,只需直接把当前元素变更大小写并向下带索引递归即可。
首先想到的想法是使用str记录回溯时的字符串,并记录当前index,每次进回溯前拼接字符,回溯后删掉末尾字符,如果字符是字母,则拼接其对应大小写再回溯一次。
将字母转为对应大/小写使用了与32异或的做法,非常简洁精妙。
class Solution {
public:
vector<string> res;
string str;
vector<string> letterCasePermutation(string s) {
dfs(s, 0);
return res;
}
void dfs(string s, int index) {
// 终止条件
if (index == s.size()) {
res.push_back(str);
return;
}
// 做选择
str += s[index];
// 回溯
dfs(s, index + 1);
// 取消选择
str.erase(str.end() - 1);
if (isalpha(s[index])) {
// 做选择
str += s[index] ^ 32;
// 回溯
dfs(s, index + 1);
// 取消选择
str.erase(str.end() - 1);
}
}
};
由于本题可以直接对s进行操作并存入s的值,可以省去str变量,非常巧妙!这里注意s传入的不能是引用。
class Solution {
public:
vector<string> res;
vector<string> letterCasePermutation(string s) {
dfs(s, 0);
return res;
}
void dfs(string s, int index) {
// 终止条件
if (index == s.size()) {
res.push_back(s);
return;
}
// 直接递归回溯
dfs(s, index + 1);
// 如果是字母,转为大/小写并向后dfs
if (isalpha(s[index])) {
s[index] ^= 32; // // s[i] == islower(s[i]) ? toupper(s[i]) : tolower(s[i]);
dfs(s, index + 1);
}
}
};
假设有从 1 到 n 的 n 个整数。用这些整数构造一个数组 perm(下标从 1 开始),只要满足下述条件 之一 ,该数组就是一个 优美的排列 :
perm[i] 能够被 i 整除
i 能够被 perm[i] 整除
给你一个整数 n ,返回可以构造的 优美排列 的 数量 。
示例:
输入:n = 2
输出:2
解释:
第 1 个优美的排列是 [1,2]:
- perm[1] = 1 能被 i = 1 整除
- perm[2] = 2 能被 i = 2 整除
第 2 个优美的排列是 [2,1]:
- perm[1] = 2 能被 i = 1 整除
- i = 2 能被 perm[2] = 1 整除
经典回溯暴力遍历,使用bool数组记忆化回溯,回溯时num表示当前是为num下标找一个数组值。
class Solution {
public:
int res = 0;
int countArrangement(int n) {
vector<bool> b(n + 1, false);
dfs(n, 1, b);
return res;
}
// 为num下标找一个数组值
void dfs(int n, int num, vector<bool> b) {
// 终止条件
if (num > n) {
res++;
return;
}
// 做选择
for (int i = 1; i <= n; i++) {
// 排除不合法的选择
if (b[i] == true) continue;
if (num % i != 0 && i % num != 0) continue;
b[i] = true;
dfs(n, num + 1, b);
b[i] = false;
}
}
};
给定一个仅包含数字 2-9 的字符串,返回所有它能表示的字母组合。答案可以按 任意顺序 返回。
给出数字到字母的映射如下(与电话按键相同)。注意 1 不对应任何字母。
示例:
输入:digits = “23”
输出:[“ad”,“ae”,“af”,“bd”,“be”,“bf”,“cd”,“ce”,“cf”]
本题多一层问题抽象,将2-9的字符串对应到一个数组中,接着对数字的每一位找到其数组对应的string进行回溯遍历,回溯时使用num记录遍历到哪一位了。
class Solution {
public:
vector<string> res;
string str;
vector<string> letterCombinations(string digits) {
if (digits.empty()) return res;
vector<string> s{"abc", "def", "ghi", "jkl", "mno", "pqrs", "tuv", "wxyz"};
backtrack(digits, s, 0);
return res;
}
void backtrack(string& digits, vector<string>& s, int num) {
if (str.size() == digits.size()) {
res.push_back(str);
return;
}
for (char c: (s[digits[num] - '2'])) {
// 做选择
str += c;
backtrack(digits, s, num + 1);
// 取消选择
str.erase(str.end() - 1);
}
}
};
数字 n 代表生成括号的对数,请你设计一个函数,用于能够生成所有可能的并且 有效的 括号组合。
示例:
输入:n = 3
输出:[“((()))”,“(()())”,“(())()”,“()(())”,“()()()”]
本题难点也在于问题抽象,回溯时使用left和right记录已经使用的左括号和右括号数,并判断接下来该加入哪个括号。
class Solution {
public:
vector<string> res;
string s;
vector<string> generateParenthesis(int n) {
backtrack(n, 0, 0);
return res;
}
void backtrack(int n, int left, int right) {
if (s.size() == 2 * n) {
res.push_back(s);
return;
}
// 加入左括号的条件
if (left < n) {
s += '(';
backtrack(n, left + 1, right);
s.erase(s.end() - 1);
}
// 加入右括号的条件
if (left > right) {
s += ')';
backtrack(n, left, right + 1);
s.erase(s.end() - 1);
}
}
};
给定一个 m x n 二维字符网格 board 和一个字符串单词 word 。如果 word 存在于网格中,返回 true ;否则,返回 false 。
单词必须按照字母顺序,通过相邻的单元格内的字母构成,其中“相邻”单元格是那些水平相邻或垂直相邻的单元格。同一个单元格内的字母不允许被重复使用。
示例:
输入:board = [[“A”,“B”,“C”,“E”],[“S”,“F”,“C”,“S”],[“A”,“D”,“E”,“E”]], word = “ABCCED”
输出:true
本题与常规回溯相比,更加抽象,首先使用一个两层for循环寻找单词起点,在每次的起点使用dfs进行递归,递归的参数除了word
和board
之外,还有当前匹配的字符下标start
,当前的位置i, j
, 最关键的是记录当前位置有没有被来过,需要创建一个bool数组visited
,只有当当前位置合法、当前字符正确、当前位置没来过的情况下,向四周走动,并在合适的位置改变visited
的值。
class Solution {
public:
bool res = false;
bool exist(vector<vector<char>>& board, string word) {
int rows = board.size();
int cols = board[0].size();
vector<vector<bool>> visited(rows, vector<bool>(cols, false));
for (int i = 0; i <= rows; i++) {
for (int j = 0; j <= cols; j++) {
// 找起点
dfs(board, word, 0, i, j, visited);
if (res == true) return res;
}
}
return res;
}
void dfs(vector<vector<char>>& board, string word, int start, int i, int j, vector<vector<bool>>& visited) {
int rows = board.size();
int cols = board[0].size();
// 成功找到
if (start == word.size()) {
res = true;
return;
}
// 跑到地图外边了
if (i < 0 || i >= rows || j < 0 || j >= cols) return;
// 字符不对
if (board[i][j] != word[start]) return;
// 该区域来过
if (b[i][j] == true) return;
// 做选择
b[i][j] = true;
dfs(board, word, start + 1, i, j + 1, visited); // 向右走
dfs(board, word, start + 1, i, j - 1, visited); // 向左走
dfs(board, word, start + 1, i + 1, j, visited); // 向下走
dfs(board, word, start + 1, i - 1, j, visited); // 向上走
// 取消选择
b[i][j] = false;
}
};
有效 IP 地址 正好由四个整数(每个整数位于 0 到 255 之间组成,且不能含有前导 0),整数之间用 ‘.’ 分隔。
例如:“0.1.2.201” 和 “192.168.1.1” 是 有效 IP 地址,但是 “0.011.255.245”、“192.168.1.312” 和 “192.168@1.1” 是 无效 IP 地址。
给定一个只包含数字的字符串 s ,用以表示一个 IP 地址,返回所有可能的有效 IP 地址,这些地址可以通过在 s 中插入 ‘.’ 来形成。你 不能 重新排序或删除 s 中的任何数字。你可以按 任何 顺序返回答案。
示例:
输入:s = “25525511135”
输出:[“255.255.11.135”,“255.255.111.35”]
记忆化回溯,number记录已包含的整数,index记录遍历的下标位置。
需要注意的:
indexSize
为0)。class Solution {
public:
vector<string> res;
string track;
vector<string> restoreIpAddresses(string s) {
backtrack(s, 4, 0);
// 删除末尾的.
for (string &i : res) {
i.erase(i.end() - 1);
}
return res;
}
void backtrack(string s, int number, int index) {
if (number == 0) {
if (index == s.size())
res.push_back(track);
return;
}
int indexSize = 2;
if (s[index] == '0') indexSize = 0;
// 用窗口去包数[index, index + indexSize]
for (int i = index; i <= index + indexSize; i++) {
if (i >= s.size()) break;
string ip = s.substr(index, i - index + 1);
if (atoi(ip.c_str()) > 255) break;
track += ip;
track += '.';
backtrack(s, number - 1, i + 1);
track.erase(track.size() - ip.size() - 1, ip.size() + 1);
}
}
};
二进制手表顶部有 4 个 LED 代表 小时(0-11),底部的 6 个 LED 代表 分钟(0-59)。每个 LED 代表一个 0 或 1,最低位在右侧。
给你一个整数 turnedOn ,表示当前亮着的 LED 的数量,返回二进制手表可以表示的所有可能时间。你可以 按任意顺序 返回答案。
小时不会以零开头:
例如,“01:00” 是无效的时间,正确的写法应该是 “1:00” 。
分钟必须由两位数组成,可能会以零开头:
例如,“10:2” 是无效的时间,正确的写法应该是 “10:02”
常规的回溯无法处理这种 小时+分钟 分别回溯的问题,需要分开回溯,非常麻烦。
可以把这十个灯想象成交替排列的 小时和分钟 灯,即。
小时灯
1
,
2
,
4
,
8
,
0
,
0
,
0
,
0
,
0
,
0
1, 2, 4, 8, 0, 0, 0, 0, 0, 0
1,2,4,8,0,0,0,0,0,0
分钟灯
0
,
0
,
0
,
0
,
1
,
2
,
4
,
8
,
16
,
32
0, 0, 0, 0, 1, 2, 4, 8, 16, 32
0,0,0,0,1,2,4,8,16,32
每次只点亮其中一盏灯,并可轻松实现回溯,其中backtrack
函数的参数为:待点亮的灯个数,从哪个灯开始试点,当前小时数,当前分钟数。
本题的回溯没有 选择和取消选择过程,因为是直接向下递归的。
class Solution {
public:
vector<int> hours = {1, 2, 4, 8, 0, 0, 0, 0, 0, 0};
vector<int> mins = {0, 0, 0, 0, 1, 2, 4, 8, 16, 32};
vector<string> res;
vector<string> readBinaryWatch(int turnedOn) {
backtrack(turnedOn, 0, 0, 0);
return res;
}
void backtrack(int turnedOn, int index, int hour, int min) {
// 无效时间
if (hour >= 12 || min >= 60) return;
// 点完灯了
if (turnedOn == 0) {
string s = to_string(hour) + ":";
if (min < 10) s += "0";
s += to_string(min);
res.push_back(s);
}
// 从index开始,挨个试点亮灯
for (int i = index; i < 10; i++)
backtrack(turnedOn - 1, i + 1, hour + hours[i], min + mins[i]);
}
};
还有一种思路也非常巧妙,利用了这些灯表示的数值都是二进制的性质,不可能有不同的灯表示相同的时间。
使用暴力穷举所有时间,并计算当前时间所需要的灯数量是否等于亮灯数量。
当前时间的灯数恰好为当前小时/分钟二进制含1的个数。
class Solution {
public:
vector<string> readBinaryWatch(int turnedOn) {
vector<string> res;
for (int i = 0; i < 12; i++) {
for (int j = 0; j < 60; j++) {
if (count1(i) + count1(j) == turnedOn) {
res.push_back(to_string(i) + ":" + (j < 10 ? "0" + to_string(j) : to_string(j)));
}
}
}
return res;
}
int count1(int n) {
int res = 0;
while (n != 0) {
res += (n & 1);
n >>= 1;
}
return res;
}
};
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。