1 重新安排行程
LeetCode:重新安排行程
一道Hard题,但做起来的感觉还是和普通的回溯题区别不大,唯一值得关注的是对于Hash的利用。
这里最值得关注的点在于排序性带来的剪枝,由于我们是利用map的有序性对终点站进行排序,所以在固定第一趟起点为JFK的情况下,所找到的第一条路线就是所需要的路线。
理论上可以利用回溯函数backTrack的bool返回值作为条件,但我这里采用了另一种判断方式。
class Solution {
public:
vector<string> result;
vector<string> path;
//<起点,<终点,航班次数>>,即[起点]:<终点:航班数>
//因为map是有序的,map代表着终点站的有序性,在必定JFK起飞的情况下
//找到的第一条路线就是我们所需要的,不需要搜索全部路线
unordered_map<string,map<string,int>> targets;
void backTrack(vector<vector<string>>& tickets)
{
if(result.size()==tickets.size()+1)
return;
if(path.size()==tickets.size()+1)
{
result=path;
return;
}
//result.back()是上一次的终点站==这一次的起点站
//target会遍历所有以path.back()为起点的机票,target为终点-次数的映射
for(pair<const string,int>& target:targets[path.back()])
{
//还有机票剩余
if(target.second>0)
{
--target.second;
path.push_back(target.first);
backTrack(tickets);
path.pop_back();
++target.second;
}
}
}
vector<string> findItinerary(vector<vector<string>>& tickets) {
for(const vector<string>& vec:tickets)
{
++targets[vec[0]][vec[1]];
}
result.push_back("JFK");
path.push_back("JFK");
backTrack(tickets);
return result;
}
};
2 N皇后
LeetCode:N皇后
大名鼎鼎的DFS问题,事实上DFS、递归与回溯本就是密不可分的思想。
这里针对N皇后问题采用了每次回溯在一行内选择的办法,即for控制列,递归控制行。
class Solution {
public:
vector<vector<string>> result;
bool isValid(vector<string>& chessboard,int i,int j)
{
int n=chessboard.size();
int k=0;
//同行和同列的不行
for(k=0;k<n;++k)
{
if(chessboard[i][k]=='Q' || chessboard[k][j]=='Q')
return false;
}
//对角线不行
//左上角
for(k=1;i-k>=0 && j-k>=0;k++)
{
if(chessboard[i-k][j-k]=='Q')
return false;
}
//左下角
for(k=1;i-k>=0 && j+k<n;k++)
{
if(chessboard[i-k][j+k]=='Q')
return false;
}
//右上角
for(k=1;i+k<n && j-k>=0;k++)
{
if(chessboard[i+k][j-k]=='Q')
return false;
}
//右下角
for(k=1;i+k<n && j+k<n;k++)
{
if(chessboard[i+k][j+k]=='Q')
return false;
}
return true;
}
void bactTrack(int numQueenLeft,vector<string>& chessboard)
{
if(numQueenLeft==0)
{
result.push_back(chessboard);
return;
}
int n=chessboard.size();
//n皇后是一个组合问题,若是有解,必然是每行一个
//因此规定每m层的回溯结果,必须将这一枚棋子放置于第m行
int i_index=n-numQueenLeft;
for(int j=0;j<n;j++)
{
if(isValid(chessboard,i_index,j))
{
chessboard[i_index][j]='Q';
bactTrack(numQueenLeft-1,chessboard);
chessboard[i_index][j]='.';
}
}
}
vector<vector<string>> solveNQueens(int n) {
result.clear();
vector<string> chessboard(n,string(n,'.'));
bactTrack(n,chessboard);
return result;
}
};
3 解数独
LeetCode:解数独
剪枝的重要性!!
树层剪枝的效率远高于树叶剪枝!应该在任何出问题的分支路口立刻回溯,来保证算法的效率!
最开始选择在所有棋盘能下的都下了再回溯,直接TimeOut,改成某个格子无有效填入就回溯,就能正常通过了。
class Solution {
public:
//因为题目保证仅有一个解,因此在board上进行操作即可
//使用bool判断,从而使得得到正确答案后不进行回溯
int countSpace(vector<vector<char>>& board)
{
int count=0;
for(int i=0;i<9;++i)
{
for(int j=0;j<9;++j)
{
if(board[i][j]=='.')
++count;
}
}
return count;
}
bool isValid(vector<vector<char>>& board,int i,int j,char num)
{
int k;
//同行与同列
for(k=0;k<9;k++)
{
if(board[k][j]==num || board[i][k]==num)
return false;
}
//九宫格的坐标
int block_i=i/3;
int block_j=j/3;
//九宫格内唯一
for(int m=0;m<=2;++m)
{
for(int n=0;n<=2;++n)
{
if(board[block_i*3+m][block_j*3+n]==num)
return false;
}
}
return true;
}
bool backTrack(int numSpaceLeft,vector<vector<char>>& board)
{
if(numSpaceLeft==0)
{
return true;
}
//行
for(int i=0;i<9;++i)
{
//列
for(int j=0;j<9;++j)
{
//有数字跳过
if(board[i][j]=='.')
{
//1-9逐个实验
for(int k=1;k<=9;k++)
{
char num=k+'0';
//有效才填入
if(isValid(board,i,j,num))
{
board[i][j]=num;
if(backTrack(numSpaceLeft-1,board))
return true;
board[i][j]='.';
}
}
//提前剪枝,这个地方9个数都不行就立刻返回false,回溯到上一刻
//如果到了i的循环之外,意味着你把整个棋盘都能填的都填了才意识到不对返回false
//太浪费了!一定要提前剪枝!
return false;
}
}
}
//这里return什么都无所谓,事实上一定不会走到这一步
return true;
}
void solveSudoku(vector<vector<char>>& board) {
int spaceCount=countSpace(board);
backTrack(spaceCount,board);
}
};
4 总结
这三道题都是Hard难度的题,但如果理清楚了思路,事实上依然是较为轻松可以解决的。
这三道题的重要性大致在于合理的数据结构选择、剪枝意识、排列与组合的选择、排列与全排列的区别以及递归和循环的内涵选择上。
感言一会儿在Hash里写。