目录
LeetCode491.非递减子序列
给你一个整数数组
nums
,找出并返回所有该数组中不同的递增子序列,递增子序列中 至少有两个元素 。你可以按 任意顺序 返回答案。数组中可能含有重复元素,如出现两个整数相等,也可以视作递增序列的一种特殊情况。
思路:这里需要注意,需要去重,但是去重逻辑与之前的题目有差别。
之前去重的时候需要排序,但是这里不能排序,因为一排序,全部元素都有序了。
这里使用的是set集合,递归的每一层都会有一个set记录本层的使用过的元素,避免重复,同时也要小心一点,如何处理后面一个元素小于前面元素的情况。
vector<vector<int>> result;//记录最终结果
vector<int> path;//记录单次结果
void backtracking(vector<int>& nums, int startIndex){
if(path.size() >= 2) result.push_back(path);//当path的小至少为2时才能加入result中
unordered_set<int> st;//每层一个set集合避免重复元素的再次使用
for(int i = startIndex; i < nums.size(); i ++){
//这里是对同层元素的去重,同时跳过不满足递增的元素
if((!path.empty() && nums[i] < path.back()) || st.find(nums[i]) != st.end()) continue;
st.insert(nums[i]);//每次插入使用过的元素
path.push_back(nums[i]);
backtracking(nums, i + 1);
path.pop_back();
}
}
vector<vector<int>> findSubsequences(vector<int>& nums) {
result.clear();
path.clear();
backtracking(nums, 0);
return result;
}
但是其实使用set会造成空间和时间的一个消耗,因为插入和更新时会涉及到一些操作,并且这些操作每层都有,所以这里我们可以使用全局数组来代替set,这样能够减轻时间和空间的消耗。
vector<vector<int>> result;//记录最终结果
vector<int> path;//记录单次结果
void backtracking(vector<int>& nums, int startIndex){
if(path.size() >= 2) result.push_back(path);//当path的小至少为2时才能加入result中
int record[201] = {0};//每层一个record数组记录元素使用情况
for(int i = startIndex; i < nums.size(); i ++){
//这里是对同层元素的去重,同时跳过不满足递增的元素
if((!path.empty() && nums[i] < path.back()) || record[nums[i] + 100] == 1) continue;
record[nums[i] + 100] = 1; //每次将使用过的元素置为1
path.push_back(nums[i]);
backtracking(nums, i + 1);
path.pop_back();
}
}
vector<vector<int>> findSubsequences(vector<int>& nums) {
result.clear();
path.clear();
backtracking(nums, 0);
return result;
}
时间复杂度:O(n*2^n)
空间复杂度:O(n)
LeetCode46.全排列
给定一个不含重复数字的数组
nums
,返回其 所有可能的全排列 。你可以 按任意顺序 返回答案。
思路:排列和组合的最大区别就是排列的元素可以重复,只要顺序不一样,就是一种排列可能。
于是这道求全排列的题目中的for循环就需要每次从0开始,并且需要记录元素使用情况。
下面是使用的vector数组的find函数查找是否加入过元素,其实不太常见,所以不是很推荐。
vector<vector<int>> result;//记录最终结果
vector<int> path;//记录单次结果
void backtracking(vector<int>& nums){
if(path.size() == nums.size()){//path的大小等于nums的大小时,说明path包含了一个全排列解
result.push_back(path);
return;
}
for(int i = 0; i < nums.size(); i ++){//注意这里的下标是从0开始的
//当path不为空且path中出现了已经遍历过的元素后,直接跳过
if(!path.empty() && find(path.begin(), path.end(), nums[i]) != path.end()) continue;
path.push_back(nums[i]);
backtracking(nums);
path.pop_back();
}
}
vector<vector<int>> permute(vector<int>& nums) {
result.clear();
path.clear();
backtracking(nums);
return result;
}
这里推荐的是使用一个全局used数组,进入之前判断一下元素是否用过,用过就跳过,没用过就进入循环。
vector<vector<int>> result;//记录最终结果
vector<int> path;//记录单次结果
void backtracking(vector<int>& nums, vector<bool>& used){
if(path.size() == nums.size()){//path的大小等于nums的大小时,说明path包含了一个全排列解
result.push_back(path);
return;
}
for(int i = 0; i < nums.size(); i ++){//注意这里的下标是从0开始的
//used中记录了元素使用情况
if(used[i] == true) continue;
used[i] = true;
path.push_back(nums[i]);
backtracking(nums, used);
path.pop_back();//开始回溯
used[i] = false;
}
}
vector<vector<int>> permute(vector<int>& nums) {
result.clear();
path.clear();
vector<bool> used(nums.size(), false);//初始化used为nums大小,并且初始值为false
backtracking(nums, used);
return result;
}
时间复杂度:O(n!)
空间复杂度:O(n)
LeetCode47.全排列II
给定一个可包含重复数字的序列
nums
,按任意顺序 返回所有不重复的全排列。
思路:这道题相较于上面的题目就是多了重复的数字,其实跟之前的组合去重思路一样的,先排序,再使用used数组判断同层元素是否重复,重复就跳过,不重复就入循环。
vector<vector<int>> result;//记录最终结果
vector<int> path;//记录单次结果
void backtracking(vector<int>& nums, vector<bool>& used){
if(path.size() == nums.size()){//path的大小满足了一个全排列的大小
result.push_back(path);
return;
}
for(int i = 0; i < nums.size(); i ++){
//去除同层重复元素
if(i > 0 && nums[i] == nums[i - 1] && used[i - 1] == false) continue;
//避免一次全排列中重复使用相同元素
if(used[i] == true) continue;
used[i] = true;
path.push_back(nums[i]);
backtracking(nums, used);
path.pop_back();
used[i] = false;
}
}
vector<vector<int>> permuteUnique(vector<int>& nums) {
result.clear();
path.clear();
sort(nums.begin(), nums.end());//去重需要排序
vector<bool> used(nums.size(), false);
backtracking(nums, used);
return result;
}
时间复杂度:O(n! )
空间复杂度:O(n)
LeetCode332.重新安排行程
给你一份航线列表
tickets
,其中tickets[i] = [fromi, toi]
表示飞机出发和降落的机场地点。请你对该行程进行重新规划排序。所有这些机票都属于一个从
JFK
(肯尼迪国际机场)出发的先生,所以该行程必须从JFK
开始。如果存在多种有效的行程,请你按字典排序返回最小的行程组合。
- 例如,行程
["JFK", "LGA"]
与["JFK", "LGB"]
相比就更小,排序更靠前。假定所有机票至少存在一种合理的行程。且所有的机票 必须都用一次 且 只能用一次。
思路:这道题如果只是求所所有的可能行程的话,是比较简单的,跟前面的题目差不多。
但是这里需要对最终路径挑选,因为一旦最终路径不止一条,就需要按字典顺序找小的那条路径,而如果先统计所有可能路径,再来挑选,大概率会超时。
所以怎么办呢?
需要在处理之前就使得待处理元素有序!
我们需要找起始站,一个起始站可能对应多个终点站,并且稍有不慎就可能会死循环,比如两个站相互往来。
所以可以选择一个unordered_map存放起始站和终点站,unordered_map是无序的,终点站需要有序,这样才能在递归中找到的第一条就是解。所以加上为了避免死循环这个要求,终点站可以采用一个map数组,map是自动有序的,map中存放的是终点站,以及起点到终点的次数,这样当为0的时候也就是说走完了,不会再进入,从而避免了死循环。
所以前期准备的时候就需要将票的信息统计到unordered_map中,这样方便后续操作。
vector<string> result;//存放最终结果
unordered_map<string, map<string, int>> targets;//记录各航班的起始点以及终点和到达终点的次数
bool backtracking(int ticketNum, vector<string>& result){
if(result.size() == ticketNum + 1){//票有ticktNum张,那么当result的大小比ticketNum多1时则可以返回了
return true;//所有航班均已添加完成,因为本身就是有序的了,所以找到一条就可以返回了
}
for(pair<const string, int>& target: targets[result[result.size() - 1]]){
//取result的最后一个元素,在targets中找到它的下一个终点
if(target.second > 0){
result.push_back(target.first);
target.second --;
if(backtracking(ticketNum, result)) return true;
result.pop_back();
target.second ++;
}
}
return false;
}
vector<string> findItinerary(vector<vector<string>>& tickets) {
result.clear();
for(auto item : tickets){
targets[item[0]][item[1]] ++;//将tickets的情况记录在targets中
}
result.push_back("JFK");//加入起始站点
backtracking(tickets.size(), result);
return result;
}
LeetCode51.N皇后
按照国际象棋的规则,皇后可以攻击与之处在同一行或同一列或同一斜线上的棋子。
n 皇后问题 研究的是如何将
n
个皇后放置在n×n
的棋盘上,并且使皇后彼此之间不能相互攻击。给你一个整数
n
,返回所有不同的 n 皇后问题 的解决方案。每一种解法包含一个不同的 n 皇后问题 的棋子放置方案,该方案中
'Q'
和'.'
分别代表了皇后和空位。
思路:N皇后是比较经典的有关回溯的问题,同时也是一个棋盘问题。
关键就在于如何判断其放皇后的位置的合法性!
如果实在没头绪,建议先看代码,看着看着就理解了,会发现原来这么简单。
vector<vector<string>> result;//记录最终结果
void backtracking(int n, int row, vector<string>& chessboard){
if(row == n){//当path的大小和n相等时,说明已经将N个皇后放好位置了
result.push_back(chessboard);
return;
}
for(int col = 0; col < n; col ++){
if(isValid(row, col, chessboard, n)){
chessboard[row][col] = 'Q';
backtracking(n, row + 1, chessboard);
chessboard[row][col] = '.';
}
}
}
bool isValid(int row, int col, vector<string> chessboard, int n){
//检查行,避免同列
for(int i = 0; i < row; i ++){
if(chessboard[i][col] == 'Q'){
return false;
}
}
//检查斜线,左上角
for(int i = row - 1, j = col - 1; i >= 0 && j >= 0; i --, j --){
if(chessboard[i][j] == 'Q'){
return false;
}
}
//检查斜线,右上角
for(int i = row - 1, j = col + 1; i >= 0 && j < n; i --, j ++){
if(chessboard[i][j] == 'Q'){
return false;
}
}
return true;
//这里没有检查列,避免同行是因为本身在进入验证的时候,取的是某一行的一个位置来验证,尝试将Q放入,本身这一行也最多只有这一个元素,不会出现同行情况,所以没必要检查
}
vector<vector<string>> solveNQueens(int n) {
result.clear();
vector<string> chessboard(n, string(n, '.'));//初始化棋盘都是'.'
backtracking(n, 0, chessboard);
return result;
}
LeetCode37.解数独
编写一个程序,通过填充空格来解决数独问题。
数独的解法需 遵循如下规则:
- 数字
1-9
在每一行只能出现一次。- 数字
1-9
在每一列只能出现一次。- 数字
1-9
在每一个以粗实线分隔的3x3
宫内只能出现一次。(请参考示例图)数独部分空格内已填入了数字,空白格用
'.'
表示。
思路:这也是一道棋盘问题,比上面的N皇后还要难一点,N皇后是每次循环遍历一层,递归遍历一列,这样一层一列就能找到位置是否合法。
但是这里的数读问题是需要遍历每个位置,然后需要判断在该位置填上1~9的元素是否合法,也是涉及到合理性的判断。
当然,这里不需要另外写返回结束语句,因为当将每个空都填充完整,也就是找到了解后,会直接返回true;当然,如果说不存在解,也不会有无限递归的情况,因为如果1~9放在某个位置都不合适,会直接返回false,跳出一层递归,回溯,进行下面的操作。
bool backtracking(vector<vector<char>>& board){
//这里不需要写结束返回语句,当填充完成后能够自动返回true
for(int row = 0; row < 9 ; row ++){
for(int col = 0; col < 9; col ++){
if(board[row][col] == '.'){
for(char k = '1'; k <= '9'; k ++){//检查将1-9放入棋盘,是否合理
if(isValid(row, col, k, board)){
board[row][col] = k;
if(backtracking(board)) return true;
board[row][col] = '.';
}
}
return false;//如果说9个元素都试过一遍都没办法加入,那就直接返回false
}
}
}
return true;
}
bool isValid(int row, int col, char k, vector<vector<char>>& board){
//检查列,确保行不会重复
for(int i = 0; i < 9; i ++){
if(board[row][i] == k){
return false;
}
}
//检查行,确保列不会重复
for(int j = 0; j < 9; j ++){
if(board[j][col] == k){
return false;
}
}
//检查九宫格里面是否重复
int startRow = (row / 3) * 3;
int startCol = (col / 3) * 3;
for(int i = startRow; i < startRow + 3; i ++){
for(int j = startCol; j < startCol +3; j ++){
if(board[i][j] == k){
return false;
}
}
}
return true;
}
void solveSudoku(vector<vector<char>>& board) {
backtracking(board);
}
整体来说有关棋盘的问题还是比较复杂的,建议直接看代码,看着看着可能就豁然开朗了!
感谢你的阅读,希望我的文章能够给你帮助,如果有帮助,麻烦点赞加收藏,或者点点关注,非常感谢。
如果有什么问题欢迎评论区讨论!