题目:重新安排行程
-
给你一份航线列表
tickets
,其中tickets[i] = [fromi, toi]
表示飞机出发和降落的机场地点。请你对该行程进行重新规划排序。所有这些机票都属于一个从JFK
(肯尼迪国际机场)出发的先生,所以该行程必须从JFK
开始。如果存在多种有效的行程,请你按字典排序返回最小的行程组合。所有这些机票都属于一个从JFK
(肯尼迪国际机场)出发的先生,所以该行程必须从JFK
开始。如果存在多种有效的行程,请你按字典排序返回最小的行程组合。 -
例如,行程
["JFK", "LGA"]
与["JFK", "LGB"]
相比就更小,排序更靠前。假定所有机票至少存在一种合理的行程。且所有的机票 必须都用一次 且 只能用一次。 -
这道题目有几个难点:
- 一个行程中,如果航班处理不好容易变成一个圈,成为死循环
- 有多种解法,字母序靠前排在前面,让很多同学望而退步,如何该记录映射关系呢 ?
- 使用回溯法(也可以说深搜) 的话,那么终止条件是什么呢?
- 搜索的过程中,如何遍历一个机场所对应的所有机场。
-
一个机场映射多个机场,机场之间要靠字母序排列,一个机场映射多个机场,可以使用std::unordered_map,如果让多个机场之间再有顺序的话,就是用std::map 或者std::multimap 或者 std::multiset。这样存放映射关系可以定义为
unordered_map<string, multiset<string>> targets
或者unordered_map<string, map<string, int>> targets
。- unordered_map<string, multiset> targets:unordered_map<出发机场, 到达机场的集合> targets;unordered_map<string, map<string, int>> targets:unordered_map<出发机场, map<到达机场, 航班次数>> targets
- 这两个结构,我选择了后者,因为如果使用
unordered_map<string, multiset<string>> targets
遍历multiset的时候,不能删除元素,一旦删除元素,迭代器就失效了。**出发机场和到达机场是会重复的,搜索的过程没及时删除目的机场就会死循环。**所以搜索的过程中就是要不断的删multiset里的元素,那么推荐使用unordered_map<string, map<string, int>> targets
。 - 在遍历
unordered_map<出发机场, map<到达机场, 航班次数>> targets
的过程中,**可以使用"航班次数"这个字段的数字做相应的增减,来标记到达机场是否使用过了。**如果“航班次数”大于零,说明目的地还可以飞,如果“航班次数”等于零说明目的地不能飞了,而不用对集合做删除元素或者增加元素的操作。
-
class Solution { public: // unordered_map<出发机场, map<到达机场, 航班次数>> targets unordered_map<string,map<string,int>> targets; bool track(int ticNUM,vector<string> &res){ if(res.size()==ticNUM+1){//参数里还需要ticketNum,表示有多少个航班(终止条件会用上)。 return true; } // 一定要加上引用即 & target,因为后面有对 target.second 做减减操作,如果没有引用,单纯复制,这个结果就没记录下来,那最后的结果就不对了。 for(pair<const string,int>& target:targets[res[res.size()-1]]){ if(target.second>0){ // 记录到达机场是否飞过了 res.push_back(target.first); target.second--; if(track(ticNUM,res)) return true; res.pop_back(); target.second++; } } return false; } vector<string> findItinerary(vector<vector<string>>& tickets) { targets.clear(); vector<string> res; for(const vector<string> & vec : tickets){ targets[vec[0]][vec[1]]++; } res.push_back("JFK"); track(tickets.size(),res); return res; } };
题目:N 皇后
-
按照国际象棋的规则,皇后可以攻击与之处在同一行或同一列或同一斜线上的棋子。n 皇后问题 研究的是如何将
n
个皇后放置在n×n
的棋盘上,并且使皇后彼此之间不能相互攻击。给你一个整数n
,返回所有不同的 n 皇后问题 的解决方案。每一种解法包含一个不同的 n 皇后问题 的棋子放置方案,该方案中'Q'
和'.'
分别代表了皇后和空位。 -
确定完约束条件,来看看究竟要怎么去搜索皇后们的位置,其实搜索皇后的位置,可以抽象为一棵树。
-
从图中,可以看出,二维矩阵中矩阵的高就是这棵树的高度,矩阵的宽就是树形结构中每一个节点的宽度。那么我们用皇后们的约束条件,来回溯搜索这棵树,只要搜索到了树的叶子节点,说明就找到了皇后们的合理位置了。
-
class Solution { public: vector<vector<string>> res; bool isV(int row,int col,vector<string>& board,int n){ for(int i=0;i<row;i++){//检查列 if(board[i][col]=='Q') return false; } for(int i=row-1,j=col-1;i>=0&&j>=0;i--,j--){ if(board[i][j]=='Q') return false; } for(int i=row-1,j=col+1;i>=0&&j<n;i--,j++) if(board[i][j]=='Q') return false; return true; } void track(int n,int start,vector<string>& board){ if(start==n){ res.push_back(board); return ; } for(int i=0;i<n;i++){ if(isV(start,i,board,n)){ board[start][i]='Q'; track(n,start+1,board); board[start][i]='.'; } } } vector<vector<string>> solveNQueens(int n) { res.clear(); vector<string> chessboard(n,string(n,'.')); track(n,0,chessboard); return res; } };
题目:解数独
-
编写一个程序,通过填充空格来解决数独问题。数独部分空格内已填入了数字,空白格用
'.'
表示。数独的解法需 遵循如下规则:- 数字
1-9
在每一行只能出现一次。 - 数字
1-9
在每一列只能出现一次。 - 数字
1-9
在每一个以粗实线分隔的3x3
宫内只能出现一次。(请参考示例图)
- 数字
-
本题中棋盘的每一个位置都要放一个数字(而N皇后是一行只放一个皇后),并检查数字是否合法,解数独的树形结构要比N皇后更宽更深。回溯三部曲
- 递归函数以及参数:**递归函数的返回值需要是bool类型,**因为解数独找到一个符合的条件(就在树的叶子节点上)立刻就返回,相当于找从根节点到叶子节点一条唯一路径,所以需要使用bool返回值。
- 递归终止条件:本题递归不用终止条件,解数独是要遍历整个树形结构寻找可能的叶子节点就立刻返回。递归的下一层的棋盘一定比上一层的棋盘多一个数,等数填满了棋盘自然就终止(填满当然好了,说明找到结果了),所以不需要终止条件!
- 递归单层搜索逻辑:在树形图中可以看出我们需要的是一个二维的递归(也就是两个for循环嵌套着递归);一个for循环遍历棋盘的行,一个for循环遍历棋盘的列,一行一列确定下来之后,递归遍历这个位置放9个数字的可能性!
-
class Solution { public: bool track(vector<vector<char>>& board){ for(int i=0;i<board.size();i++){ for(int j=0;j<board[0].size();j++){ if(board[i][j]=='.'){ for(char k='1';k<='9';k++){ if(isV(i,j,k,board)){ board[i][j]=k; if(track(board)) return true; board[i][j]='.'; } } return false; } } } return true; } bool isV(int row,int col,char k,vector<vector<char>>& chessboard){ for(int i=0;i<9;i++){ if(chessboard[row][i]==k || chessboard[i][col]==k) return false; } int startR = (row/3)*3; int startC = (col/3)*3; for(int i=startR;i<startR+3;i++){ for(int j=startC;j<startC+3;j++){ if(chessboard[i][j]==k) return false; } } return true; } void solveSudoku(vector<vector<char>>& board) { track(board); } };
-
回溯是递归的副产品,只要有递归就会有回溯,所以回溯法也经常和二叉树遍历,深度优先搜索混在一起,因为这两种方式都是用了递归。回溯法就是暴力搜索,并不是什么高效的算法,最多再剪枝一下。
-
回溯算法能解决如下问题:
- 组合问题:N个数里面按一定规则找出k个数的集合
- 排列问题:N个数按一定规则全排列,有几种排列方式
- 切割问题:一个字符串按一定规则有几种切割方式
- 子集问题:一个N个数的集合里有多少符合条件的子集
- 棋盘问题:N皇后,解数独等等
-
回溯法确实不好理解,所以需要把回溯法抽象为一个图形来理解就容易多了,在后面的每一道回溯法的题目我都将遍历过程抽象为树形结构方便大家的理解。
-
子集问题分析:
- 时间复杂度: O ( 2 n ) O(2^n ) O(2n),因为每一个元素的状态无外乎取与不取,收集树的节点
- 空间复杂度:O(n),递归深度为n,所以系统栈所用空间为O(n),每一层递归所用的空间都是常数级别,注意代码里的result和path都是全局变量,就算是放在参数里,传的也是引用,并不会新申请内存空间,最终空间复杂度为O(n)
-
排列问题分析:
- 时间复杂度:O(n!),这个可以从排列的树形图中很明显发现,每一层节点为n,第二层每一个分支都延伸了n-1个分支,再往下又是n-2个分支,所以一直到叶子节点一共就是 n * n-1 * n-2 * … 1 = n!。
- 空间复杂度:O(n),和子集问题同理。
-
组合问题分析:
- 时间复杂度:
O
(
2
n
)
O(2^n)
O(2n),组合问题其实就是一种子集的问题,所以组合问题最坏的情况,也不会超过子集问题的时间复杂度。
n-1个分支,再往下又是n-2个分支,所以一直到叶子节点一共就是 n * n-1 * n-2 * … 1 = n!。 - 空间复杂度:O(n),和子集问题同理。
- 时间复杂度:
O
(
2
n
)
O(2^n)
O(2n),组合问题其实就是一种子集的问题,所以组合问题最坏的情况,也不会超过子集问题的时间复杂度。