for 选择 in 选择列表
#做选择
将该选择从选择列表中移除
路径.add(选择)
backtrack(路径, 选择列表)
#撤销选择
路径.remove(选择)
将该选择再加入选择列表
List<List<Integer>> res = new LinkedList<>();
/* 主函数,输入一组不重复的数字,返回它们的全排列 */
List<List<Integer>> permute(int[] nums){
// 记录「路径」
LinkedList<Integer> track = new LinkedList<>();
backtrack(nums, track);
return res;
}
// 路径:记录在 track 中
// 选择列表:nums 中不存在于 track 的那些元素
// 结束条件:nums 中的元素全都在 track 中出现
void backtrack(int[] nums, LinkedList<Integer> track) {
// 触发结束条件
if (track.size() == nums.length) {
res.add(new LinkedList(track));
return;
}
for (int i = 0; i < nums.length; i++) {
// 排除不合法的选择
if (track.contains(nums[i]))
continue;
// 做选择
track.add(nums[i]);
// 进入下一层决策树
backtrack(nums, track);
// 取消选择
track.removeLast();
}
}
vector<vector<string>> res;
/* 输入棋盘边长 n,返回所有合法的放置 */
vector<vector<string>> solveNQueens(int n) {
// '.' 表示空,'Q' 表示皇后,初始化空棋盘。
vector<string> board(n, string(n, '.'));
backtrack(board, 0);
return res;
}
// 路径:board 中小于 row 的那些行都已经成功放置了皇后
// 选择列表:第 row 行的所有列都是放置皇后的选择
// 结束条件:row 超过 board 的最后一行
void backtrack(vector<string>& board, int row) {
// 触发结束条件
if (row == board.size()) {
res.push_back(board);
return;
}
int n = board[row].size();
for (int col = 0; col < n; col++) {
// 排除不合法选择
if (!isValid(board, row, col))
continue;
// 做选择
board[row][col] = 'Q';
// 进入下一行决策
backtrack(board, row + 1);
// 撤销选择
board[row][col] = '.';
}
}
/* 是否可以在 board[row][col] 放置皇后? */
bool isValid(vector<string>& board, int row, int col) {
int n = board.size();
// 检查列是否有皇后互相冲突
for (int i = 0; i < n; i++) {
if (board[i][col] == 'Q')
return false;
}
// 检查右上方是否有皇后互相冲突
for (int i = row - 1, j = col + 1;
i >= 0 && j < n; i--, j++) {
if (board[i][j] == 'Q')
return false;
}
// 检查左上方是否有皇后互相冲突
for (int i = row - 1, j = col - 1;
i >= 0 && j >= 0; i--, j--) {
if (board[i][j] == 'Q')
return false;
}
return true;
}
// 函数找到一个答案后就返回 true
bool backtrack(vector<string>& board, int row) {
// 触发结束条件
if (row == board.size()) {
res.push_back(board);
return true;
}
...
for (int col = 0; col < n; col++) {
...
board[row][col] = 'Q';
if (backtrack(board, row + 1))
return true;
board[row][col] = '.';
}
return false;
}
def backtrack(...):
for 选择 in 选择列表:
做选择
backtrack(...)
撤销选择
不要想着极端的复习的状态,因为那样你可就太牛逼了,一线互联网公司可能都不敢收你的,因为他们会觉得你留不住。所以,没有最好只有更好,复习不到最佳状态不可怕,只要比跟你竞争的人强就好了。
八皇后问题,数学大佬高斯穷尽一生都没有数清楚八皇后问题到底有几种可能的放置方法
所以,想从整体上在脑袋中建立八皇后问题的搜索树,是基本不可能的,因为搞数学的大佬高斯都做不到,我们怎么可能做到?
但是,这并不妨碍我们找到解决这种问题的规律,设计出算法,进而交给计算机去解决这个问题。
不管是BFS,还是DFS,这些回溯算法所能解决的问题,都是有着类似的结构的。每一步的状态转换都是有限的,每一步的处理方式跟上一步骤是类似的,或者说是相同的。比如P1434滑雪问题,每个点可讨论的情况就是上下左右四个方向。所有点都是这样,无非就是在边界着重讨论一下可行的方向而已。
对于最优解问题,如果有最优子结构,那就可以转换成DP算法进行求解。如果局部最优的思路可以得到全局最优解,那么就可以转换成贪心的思路去解。
回溯算法设计的重点是如何由上一个选择转移到下一个选择,并设计好出口。
分治算法设计的重点是如何把大问题分割成子问题,然后子问题的解如何一步一步合并成原问题的解。有些问题在合并的时候非常困难(平面最近点对)。
DP算法设计的重点和难点是如何根据题意选择恰当的DP思路,因为很多题目的DP思路都不是跟题目有直接的对应关系的,这一部分有待做题后进一步讨论。
回溯算法设计,往往跟状态空间这个名词有很大联系。指数级的状态空间,剪枝操作。
回溯法对应DFS,分支限界法对应BFS。
为什么要划分成子问题呢?编程中,对子问题的定义是,仅规模、参数不同,其他的都相同。因为子问题和父问题有相同的结构。这样处理起来就可以用相同的代码段来解决。
循环和递归是用于处理子问题的两种程序结构。
不要以为循环就好懂,有很多循环就很难搞,比如二分的算法。
不要以为递归就难懂,树的前序、中序、后序遍历还是挺好理解的。
子问题和子问题之间的联系,以及子问题扩展到父问题,可以对应于数学归纳法。
做算法题,能够不去注重全局理解,而是去推导子问题之间的关系,应该是算法入门的重要表现。
我自己之前做题,总是想在整体上有一个把握,我总想着,对任何题目都能达到像,对数组赋值那样的理解(如下)。
int tmp;
for(int i=0;i<n;i++){
cin>>tmp;
array[i] = tmp;
}
然后,意料之中地,对复杂情况无法做到“理解”。就对算法题产生了厌烦的情绪。
其实,相比“理解”,正确的写出算法似乎更重要。