【回溯算法(二)】回溯算法与(深度优先/广度优先)搜索

回溯算法

代码方面,回溯算法的框架:

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();
    }
}

我们这里稍微做了些变通,没有显式记录「选择列表」,而是通过numstrack推导出当前的选择列表

不像动态规划存在重叠子问题可以优化,回溯算法就是纯暴力穷举,复杂度一般都很高

二、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函数可以将不符合条件的情况剪枝

img

有的时候,我们并不想得到所有合法的答案,只想要一个答案,怎么办呢?比如解数独的算法,找所有解法复杂度太高,只要找到一种解法就可以。

其实特别简单,只要稍微修改一下回溯算法的代码即可:

// 函数找到一个答案后就返回 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求排列组合

遍历数组中所有k个元素的组合,求出的最大值

#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 的预算和每场比赛的票价,试求:如果总票价不超过预算,他有多少种观赛方案。如果存在以其中一种方案观看某场比赛而另一种方案不观看,则认为这两种方案不同。
输入第一行,两个正整数 NM (),表示比赛的个数和 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;
}
posted @ 2020-10-14 00:07  satire  阅读(664)  评论(0编辑  收藏  举报