第一题:Leetcode 491. 非递减子序列
题目描述
解题思路
- 由于 nums 中可能存在重复元素,因此在某一层访问时,需要记录某个数字是否已经被使用(这是在同层中剪枝)
- 一般来说:组合问题和排列问题是在树形结构的叶子节点上收集结果,而子集问题就是取树上所有节点的结果。
题解
class Solution {
public:
vector<vector<int>> ans;
vector<int> path;
vector<vector<int>> findSubsequences(vector<int>& nums) {
backTracking(nums, 0);
return ans;
}
void backTracking(const vector<int>& nums, int startIdx) {
if (path.size() >= 2)
ans.push_back(path);
if (startIdx >= nums.size())
return;
vector<int> uesed(201, 0); // 代表本层是否用过
for (int i = startIdx; i < nums.size(); i++) {
if ((path.size() > 0 && nums[i] < path[path.size() - 1]) ||
uesed[nums[i] + 100] == 1)
continue;
uesed[nums[i] + 100] = 1;
path.push_back(nums[i]);
backTracking(nums, i + 1);
path.pop_back();
}
}
};
- uesed是表示本层是否用过某个数,不用传递到下一层。此处使用 vector 是因为数字位于 [-100,100]。如果不限制范围,则需要使用 unordered_set ,使用 insert 和 find 函数来实现。
第二题:Leetcode46. 全排列
题目描述
给定一个不含重复数字的数组 nums
,返回其 所有可能的全排列 。你可以 按任意顺序 返回答案。
题解
class Solution {
public:
vector<vector<int>> ans;
vector<int> path;
vector<vector<int>> permute(vector<int>& nums) {
vector<bool> used(nums.size(), false);
backTracking(nums, used);
return ans;
}
void backTracking(const vector<int>& nums, vector<bool>& used) {
if (path.size() == nums.size()) {
ans.push_back(path);
return;
}
for (int i = 0; i < nums.size(); i++) {
if (used[i] == true)
continue;
path.push_back(nums[i]);
used[i] = true;
backTracking(nums, used);
path.pop_back();
used[i] = false;
}
}
};
- 排列组合问题,因此在叶子结点收集结果;
- nums 没有重复元素,因此不用进行同层剪枝。
第三题:Leetcode47. 全排列 II
题目描述
给定一个可包含重复数字的序列 nums
,按任意顺序 返回所有不重复的全排列。
题解
class Solution {
public:
vector<vector<int>> ans;
vector<int> path;
vector<vector<int>> permuteUnique(vector<int>& nums) {
sort(nums.begin(), nums.end());
vector<bool> used(nums.size(), false);
backTracking(nums, used);
return ans;
}
void backTracking(const vector<int>& nums, vector<bool>& used) {
if (path.size() == nums.size()) {
ans.push_back(path);
return;
}
for (int i = 0; i < nums.size(); i++) {
if (used[i] == true)
continue;
if (i > 0 && nums[i] == nums[i - 1] && used[i - 1] == false)
continue;
path.push_back(nums[i]);
used[i] = true;
backTracking(nums, used);
path.pop_back();
used[i] = false;
}
}
};
由于存在重复元素,因此首先进行排序,然后使用
if (i > 0 && nums[i] == nums[i - 1] && used[i - 1] == false)
continue;
进行同层剪枝。
第四题:Leetcode332. 重新安排行程
题目描述
解题思路
此题重点在于:需要返回字典序顺序最小的结果,从“JKF”机场出发。因此,可以以字典序从小到大的顺序依次乘坐以JKF为出发机场的机票。
那么现在问题是:用什么数据结果存储飞机票,可以指定出发机场 + 可以以字典序顺序 访问
答案一:
unordered_map<string, multiset<string>> tickit_times
答案二:
unordered_map<string, map<string, int>> tickit_times
答案二更好,原因是:坐过的机票需要“撕毁”,第二种只需要将 int -- 即可,而第一种需要删除键,回溯时需要再insert,会导致迭代器失效。
tickit_times[city1][city2]表示从 city1 到 city2 的直达航班数(在乘坐过后需要--,回溯后++)
题解
class Solution {
public:
unordered_map<string, map<string, int>> tickit_times;
vector<string> findItinerary(vector<vector<string>>& tickets) {
vector<string> ans;
tickit_times.clear();
// tickit_times记录从fly[0]-> fly[1] 的次数
for (auto fly : tickets)
tickit_times[fly[0]][fly[1]]++;
ans.push_back("JFK");
backTracking(tickets.size(), ans);
return ans;
}
bool backTracking(const size_t ticketNum, vector<string>& ans) {
// 四场票需要ans中有五个机场
if (ans.size() == ticketNum + 1)
return true;
// 访问按照字典升序进行,结果自然是字典序最小的结果——map性质
// 此处重点:flight需要作为引用,否则无法对航班数进行--,会导致重复飞行。
for (auto& flight : tickit_times[ans.back()) {
if (flight.second > 0) {
flight.second--;
ans.push_back(flight.first);
// 此处需要剪枝,其实有点贪心算法的意思
if (backTracking(ticketNum, ans))
return true;
flight.second++;
ans.pop_back();
}
}
return false;
}
};
这里的重点是,在遍历的时候,需要使用引用。
for (auto& flight : tickit_times[ans.back())
由于已经是字典序升序访问,因此在这一枝找到结果以后,就直接返回,无需回溯。
第五题:Leetcode 51.N皇后
解题思路
- 按行放置皇后——因此不存在同行冲突;
- 使用isAvailable[i]表示i列是否已经放置皇后——因此避免同列冲突;
-
isValidPosition(const vector<string> board, const int row, const int col)
函数用于判断在 (row,col)位置放置皇后是否会存在斜线冲突。
题解
class Solution {
public:
vector<vector<string>> solveNQueens(int n) {
vector<vector<string>> ans;
vector<string> board(n, string(n, '.'));
vector<bool> isAvailable(n, true);
backTrackingNQueen(board, isAvailable, 0, ans);
return ans;
}
// toPlaceRowIdx 代表即将放置的行数
// isAvailable[col]表示第i列已经放置过了
void backTrackingNQueen(vector<string>& board, vector<bool>& isAvailable,
int toPlaceRowIdx, vector<vector<string>>& ans) {
if (toPlaceRowIdx == board.size()) {
ans.push_back(board);
return;
}
for (int col = 0; col < board.size(); col++) {
// 如果 col 这一列还没有放置
// 且 toPlaceRowIdx,col 放置合法
if (isAvailable[col] &&
isValidPosition(board, toPlaceRowIdx, col)) {
board[toPlaceRowIdx][col] = 'Q';
isAvailable[col] = false;
backTrackingNQueen(board, isAvailable, toPlaceRowIdx + 1,ans);
board[toPlaceRowIdx][col] = '.';
isAvailable[col] = true;
}
}
}
bool isValidPosition(const vector<string> board, const int row,
const int col) {
const int n = board.size();
int step_r = -1, step_c = -1;
int r = row + step_r, c = col + step_c;
while (r >= 0 && r < n && c >= 0 && c < n) {
if (board[r][c] == 'Q')
return false;
r += step_r, c += step_c;
}
step_r = -1, step_c = 1;
r = row + step_r, c = col + step_c;
while (r >= 0 && r < n && c >= 0 && c < n) {
if (board[r][c] == 'Q')
return false;
r += step_r, c += step_c;
}
return true;
}
};
第六题:Leetcode37.解数独
解题思路
仿照人类思维,一个一个填。
使用 toFill 表示从何处填起,然后使用 toFill 计算 row和col。
题解
class Solution {
public:
void solveSudoku(vector<vector<char>>& board) { backTracking(board, 0); }
bool backTracking(vector<vector<char>>& board, int toFill) {
for (int n = toFill; n < 81; n++) {
int r = n / 9;
int c = n % 9;
if (board[r][c] == '.') {
// 从 1-9 挨个试
for (char i = '1'; i <= '9'; i++) {
if (isValid(board, r, c, i)) {
board[r][c] = i;
if (backTracking(board, n + 1))
return true;
board[r][c] = '.';
}
}
return false;
}
}
return true;
}
bool isValid(const vector<vector<char>>& board, const int row,
const int col, const char num) {
for (int i = 0; i < 9; i++) {
if (board[row][i] == num || board[i][col] == num)
return false;
}
const int startRow = (row / 3) * 3;
const int startCol = (col / 3) * 3;
for (int r = startRow; r < startRow + 3; r++) {
for (int c = startCol; c < startCol + 3; c++) {
if (board[r][c] == num)
return false;
}
}
return true;
}
};
要点
由于题目保证只有一个解,因此在这一枝已经找到返回合适值的时候,无需继续回溯。