【回溯算法(二)】回溯算法与(深度优先/广度优先)搜索
回溯算法
代码方面,回溯算法的框架:
result = []
def backtrack(路径, 选择列表):
if 满足结束条件:
result.add(路径)
return
for 选择 in 选择列表:
# 做选择
将该选择从选择列表移除
路径.add(选择)
backtrack(路径, 选择列表)
# 撤销选择
路径.remove(选择)
将该选择再加入选择列表
其核心就是 for 循环里面的递归,在递归调用之前「做选择」,在递归调用之后「撤销选择」,特别简单。
一、全排列问题
我们只要在递归之前做出选择,在递归之后撤销刚才的选择,就能正确得到每个节点的选择列表和路径。
下面,直接看全排列代码:
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();
}
}
我们这里稍微做了些变通,没有显式记录「选择列表」,而是通过nums
和track
推导出当前的选择列表
不像动态规划存在重叠子问题可以优化,回溯算法就是纯暴力穷举,复杂度一般都很高。
二、N 皇后问题
直接套用框架:
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] = '.';
}
}
这部分主要代码,跟全排列问题差不多。isValid
函数的实现也很简单:
/* 是否可以在 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;
}
函数backtrack
依然像个在决策树上游走的指针,每个节点就表示在board[row][col]
上放置皇后,通过isValid
函数可以将不符合条件的情况剪枝:
有的时候,我们并不想得到所有合法的答案,只想要一个答案,怎么办呢?比如解数独的算法,找所有解法复杂度太高,只要找到一种解法就可以。
其实特别简单,只要稍微修改一下回溯算法的代码即可:
// 函数找到一个答案后就返回 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;
}
这样修改后,只要找到一个答案,for 循环的后续递归穷举都会被阻断。
深度优先与广度优先搜索
回溯算法实际上就是搜索,利用回溯算法框架可以进行深度优先于广度优先搜索。
深度优先搜索
例如:利用DFS求排列组合
#include <numeric>
class Solution {
public:
vector<vector<int>> combine(int n, int k, int m, vector<int> num) {//n个数存在num里面, 找k个组合
Int res;
vector<int> out;
helper(n, k, 1, num,m, out, res);
return res;
}
void helper(int n, int k, int level, vector<int> num, int m, vector<int>& out, int& res) {
if (out.size() == k)
{
int sum = accumulate(out.begin(),out.end(),0);//把out这个数组求和成sum
sum = sum % m;
if( sum > res )
res = sum; //找最大值
return;
}
for (int i = level; i <= n; ++i) {
out.push_back(num[i]);
helper(n, k, i + 1, out, res);
out.pop_back();
}
}
};
广度优先搜索(BFS)
例如: BFS遍历二叉树
void levelorder(T* node, int level, vector<vector<int>>&res)
{
if(!node) return;
vector<int> cur{};
if(res.size()==level) res.push_back(cur);//push new vector
res[level].push_back(node->val);
if(node->left) leveloder(node->left,level+1,res);
if(node->right) levelorder(nobde->right,level+1,res);
}
非递归:
class Solution {
public:
vector<vector<int> > levelOrderBottom(TreeNode* root) {
if (!root) return {};
vector<vector<int>> res;
queue<TreeNode*> q{{root}};
while (!q.empty()) {
vector<int> oneLevel;
for (int i = q.size(); i > 0; --i) {
TreeNode *t = q.front(); q.pop();
oneLevel.push_back(t->val);
if (t->left) q.push(t->left);
if (t->right) q.push(t->right);
}
res.insert(res.begin(), oneLevel);
}
return res;
}
};
深度优先搜索(DFS)状态压缩
位压缩
在上文N Queens (N<=10)中,可以用到位运算来表示状态,代码十分优美。
这道题目,首先确定搜什么。因为每一行仅且必须放一个皇后,所以我们可以按照顺序一行一行放置皇后,那么就搜每一行得皇后放在哪个位置。
接下来是状态表示,我们判断一个位置是否可以防止皇后,要考虑它的上方,左上 45°,右上 45° 是否放置了皇后,这些就是我们需要在深搜过程中维护的当前状态。
至此一个 DFS 的程序已经可以写出来了,不过我们可以通过位运算让它更加优美。因为数据量很小,所以我们可以把每一列有没有放置皇后压缩进一个 int 中存储。同理左上 45°,右上 45° 信息也是可以压缩进 int 中表示的,表示经过左上 45° / 右上 45° 的棋子影响后,当前行哪些位置不可以防止皇后,从这一行到下一行仅仅是一个左移 / 右移就可以变化的。
int dfs(int cur, int n, int lb, //左上角
int rb, //右上角
int cb) //列
{
if(cur>n) return 1;
int ban = lb |rb | cb, num = 0; //当前行每一位的情况都存在ban里面,该位为1表示不能放
for(int i = 0;i<n; i++) //当前行每一位都尝试
{
if( (ban >>i)& 1 ) continue; //当前位不能放
num +=dfs(cur+1,n, (lb|1<<i)<<1, (rb|1<<i)>>1, cb|(1<<i));
}
return num;
}
int main(){
int n, ans[15];
for (int i = 1; i <= 10; i++) ans[i] = dfs(1, i, 0, 0, 0);
while(scanf("%d", &n) != EOF && n > 0)
printf("%d\n", ans[n]);
return 0;
}
二进制基础运算
- 求一个数所有位共有几个1:
for(int res = 0; ; x-=x&-x; ++res)
if(x== 0) return res;
注:x&-x表示取x最低位为1的那个数。即m = x&-x,则x%m = 0且$ m = 2^k$, 如果x为奇数, m =1。例如:
x = 10000110
-x = 01111001 + 1 = 01111010
x&-x = 00000010
-
判断:j位为1:(x>>j)&1
-
set j 位为1:x |(1<<j)
-
set j位为0: x&~(1<<j)
Meet in the Middle
洛谷 P4799 世界冰球锦标赛 https://www.luogu.com.cn/problem/P4799
今年的世界冰球锦标赛在捷克举行。Bobek 已经抵达布拉格,他不是任何团队的粉丝,也没有时间观念。他只是单纯的想去看几场比赛。如果他有足够的钱,他会去看所有的比赛。不幸的是,他的财产十分有限,他决定把所有财产都用来买门票。
给出 Bobek 的预算和每场比赛的票价,试求:如果总票价不超过预算,他有多少种观赛方案。如果存在以其中一种方案观看某场比赛而另一种方案不观看,则认为这两种方案不同。
输入第一行,两个正整数 N 和 M (),表示比赛的个数和 Bobek 那家徒四壁的财产。
输入第二行,N 个以空格分隔的正整数,均不超过,代表每场比赛门票的价格。
输出一行,表示方案的个数。由于 N 十分大,注意:答案
这道题目的 M 太大了,没有什么好的做法,只能通过 DFS 进行枚举,求得最终合法的方案数。
但是这里的 N 也有点大,达到了 40,直接枚举的话有 中情况,一定会超时,这时候可以考虑一个 DFS 的技巧叫做 Meet in the Middle,也就是我们从起点开始搜索 2020 个,从终点往前搜索 2020 个,然后在中间合并搜索的结果,这样搜索空间的大小就是 ,就可以接受了。合并两侧搜索结果的时候,我们通常将两个 DFS 的结果存在数组里,然后通过二分将两个数组里的状态关联起来。
#include <cstdio>
#include <algorithm>
using namespace std;
#define LL long long
const int MAXN = 45;
const int MAXM = 1<<21;
LL M, price[55], pre[MAXM], suf[MAXM];
void dfs(int l, int r, LL v, LL sum[], int &cnt){
if (l > r) { sum[cnt++] = v; return; }
dfs(l + 1, r, v, sum, cnt);
dfs(l + 1, r, v + price[l], sum, cnt);
}
int main(){
int n, a = 0, b = 0;
scanf("%d%lld", &n, &M);
for (int i = 1; i <= n; i++) scanf("%lld", &price[i]);
dfs(1, n / 2, 0, pre, a);
dfs(n / 2 + 1, n, 0, suf, b);
sort(suf, suf + b);
LL ans = 0;
for (int i = 0; i < a; i++)
ans += upper_bound(suf, suf + b, M - pre[i]) - suf;
printf("%lld\n", ans);
return 0;
}