回溯算法小结


一. 回溯算法介绍

1.1 定义

回溯算法是指:用深度优先搜索对可行解进行暴力枚举,并在枚举后撤销当前操作,从而反复递归进行的搜索算法。

1.2 特征

经过总结,回溯算法主要有如下特征:

  • 尝试【新的变化】
  • 以该【新的变化】作为新的迭代入口进行迭代
  • 撤销【新的变化】
  • 重复以上过程

二. 应用举例

2.1 Leetcode 46 全排列

代码如下:

vector<vector<int>> permute(vector<int>& nums) {
    vector<vector<int>> ans;
    backtracking(nums, ans, 0);
    return ans;
}
void backtracking(vector<int>& nums, vector<vector<int>> ans, int pos) {
	// 递归出口(回溯法是递归,也要遵循递归的写法)
	if (pos = nums.size() - 1) {
		ans.push(nums); 
		return ;
	}
	// 递归条件
	backtrcking(nums, ans, pos + 1); // 不交换顺序,它本身也是一个解
	for (int i = pos + 1; i < nums.size(); ++i) { // 重复以下过程
		swap(nums[pos], nums[i]); // 尝试【新的变化】
		backtracking(nums, ans, i); // 以该【新的变化】作为新的迭代入口进行迭代
		swap(nus[pos], nums[i); // 撤销【新的变化】
	}
}

2.2 Leetcode 39 组合总和

代码如下:

vector<vector<int>> combinationSum(vector<int>& candidates, int target) {
    vector<vector<int>> ans; // 若尝试结果正确,则添加到ans中
    vector<int> path; // 保存每次尝试结果
    backtracking(candidates, target, path, ans, 0);
    return ans;
}
void backtracking(vector<int>& candidates, int target, vector<int>& path, vector<vector<int>>& ans, int pos) {
	// 不需要规定退出条件,因为下面的target > candidates[i]提前做了限制
	for (int i = pos; i < candidates.size(); ++i) { // 重复以下过程
		path.push_back(candidates[i]); // 尝试【新的变化】
		if (target == candidates[i]) {
			ans.push_back(path); // 以该【新的变化】作为新的迭代入口进入循环
		}
		else if (target > candidates[i]) {
			backtracking(candidates, target - candidates[i], path, ans, i);
		}
		path.pop_back(); // 撤销【新的变化】
	}
}

2.3 Leetcode 40 组和总和2

代码如下:

class Solution {
private:
    vector<pair<int, int>> freq;
    vector<vector<int>> ans;
    vector<int> tmp;
public:
    vector<vector<int>> combinationSum2(vector<int>& candidates, int target) {
        sort(candidates.begin(), candidates.end());
        for (int n: candidates) {
            if (freq.empty() || freq.back().first != n) {
                freq.push_back(make_pair(n, 1));
            } else {
                freq.back().second++;
            }
        }
        dfs(0, target);
        return ans;
    }

    void dfs(int idx, int target) {
        if (target == 0) {
            ans.push_back(tmp);
            return;
        }
        if (idx == freq.size() || freq[idx].first > target) { // idx超过了数组 或 数组当前位置的数比所需要的大
            return;
        }
        // 不选idx对应的数
        dfs(idx + 1, target);
        // 选idx对应的数,但是它可能有多个
        int more = min(target / freq[idx].first, freq[idx].second);
        for (int i = 1; i <= more; ++i) {
            tmp.push_back(freq[idx].first);
            dfs(idx + 1, target - i * freq[idx].first);
            // 这里的idx + 1是表示在freq这个数组下的下一个数字,即下一个不同的数字
        }
        for (int i = 1; i <= more; ++i) {
            tmp.pop_back();
        }
    }
};

2.3 Leetcode 77 组和

代码如下:

vector<vector<int>> combine(int n, int k) {
        vector<vector<int>> ans; // 若尝试结果正确,则添加到ans中
        vector<int> path; // 保存每次尝试结果
        backtracking(n, k, 1, path, ans);
        return ans;

    }
    void backtracking(int n, int k, int pos, vector<int>& path, vector<vector<int>>& ans) {
        // 递归出口(回溯法是递归,也要遵循递归的写法)
        if (k == 0) {
            ans.push_back(path);
            return;
        }
        if (n < k) return;
        // 调用循环 
        for (int i = pos; i <= n; ++i) { // 重复以下过程
            path.push_back(i); // 尝试【新的变化】
            backtracking(n, k - 1, i + 1, path, ans); // 以该【新的变化】作为新的迭代入口进入循环
            path.pop_back(); // 撤销【新的变化】
        }
    }

也可以将ans和path存储为成员变量。

class Solution {
public:
    vector<vector<int>> ans;
    vector<int> tmp;
    vector<vector<int>> combine(int n, int k) {
        com(n, k, 1);
        return ans;
    }

    void com(int n, int k, int idx) {
        if (k == 0) {
            ans.push_back(tmp);
            return;
        }
        for (int i = idx; i <= n; ++i) {
            tmp.push_back(i);
            com(n, k - 1, i + 1);
            tmp.pop_back();
        }
    }
};

2.3 Leetcode 79 单词搜索

代码如下:

vector<int> direction{-1,0,1,0,-1};
    bool exist(vector<vector<char>>& board, string word) {
        bool ans = false;
        int m = board.size(), n = board[0].size();
        vector<vector<bool>> visited(m, vector<bool>(n, false));
        for (int i = 0; i < m; ++i) {
            for (int j = 0; j < n; ++j) {
                if (board[i][j] == word[0]) {
                    visited[i][j] = true;
                    ans = ans || backtracking(board, word, m, n, i, j, 0, visited);
                    visited[i][j] = false;
                    if (ans) { // 减少搜索次数
                        flag = true;
                        break;
                    }
                }
            }
        }
        return ans;

    }
    bool backtracking(vector<vector<char>>& board, string word, int m, int n, int r, int c, int pos, vector<vector<bool>>& visited) {
        // 递归出口(回溯法是递归,也要遵循递归的写法)
        if (pos == word.length() - 1) return true;
        int i = 0, j = 0;
        bool ans = false;
        for (int k = 0; k < 4; ++k) { // 重复以下过程
            i = r + direction[k], j = c + direction[k + 1];
            if (i >= 0 && i < m && j >= 0 && j < n && board[i][j] == word[pos + 1] && !visited[i][j]) {
                visited[i][j] = true; // 尝试【新的变化】
                ans = ans || backtracking(board, word, m, n, i, j, pos + 1, visited); // 以该【新的变化】作为新的迭代入口进入循环
                visited[i][j] = false; // 撤销【新的变化】
                if (ans) {
                        break; // 减少搜索次数
                    }
            }
        }
        return ans;
    }

2.3 Leetcode 51 N皇后

N皇后的难点在于比起其他题目,需要更多的代码量,用于处理皇后放置在某位置后更新可行域(put_queen函数),代码如下:

vector<vector<string>> solveNQueens(int n) {
        vector<vector<string>> ans;
        vector<vector<int>> attack(n, vector<int>(n, 0));
        vector<string> queen(n, string(n, '.'));
        backtracking(0, n, queen, attack, ans);
        return ans;

    }
    void backtracking(int k, int n, vector<string>& queen, 
    vector<vector<int>>& attack, vector<vector<string>>& ans) {
        // 递归出口(回溯法是递归,也要遵循递归的写法)
        if (k == n) {
            ans.push_back(queen);
            return;
        }
        // 遍历第k行所有选择
        for (int i = 0; i < n; ++i) { // 重复以下过程
            if (!attack[k][i]) {
                vector<vector<int>> temp = attack;
                queen[k][i] = 'Q'; // 尝试【新的变化】
                put_queen(k, i, attack, n); // 皇后放置在某位置后更新可行域
                backtracking(k + 1, n, queen, attack, ans); // 以该【新的变化】作为新的迭代入口进入循环
                attack = temp; // 撤销【新的变化】
                queen[k][i] = '.'; // 撤销【新的变化】
            }
        }


    }
    // 皇后放置在某位置后更新可行域
    void put_queen(int a, int b, vector<vector<int>>& attack, int n) {
        vector<int> dx{-1, -1, -1, 0, 0, 1, 1, 1};
        vector<int> dy{-1, 0, 1, -1, 1, -1, 0, 1};
        int r = 0, c = 0;
        for (int i = 1; i < n; ++i) {
            for (int j = 0; j < 8; ++j) {
                r = a + i * dx[j], c = b + i * dy[j];
                if (r >= 0 && r < n && c >= 0 && c < n) {
                    attack[r][c] = 1;
                } 
            }
        }
    }

2.4 华为机考 67 24点

24点有且仅有四种代数运算,分别是加减乘除,我们对目标值进行一次运算,以新的目标值作为下一个迭代的起点。

#include<iostream>
#include<vector>
bool dfs(vector<int> nums, vector<bool>& visited, int target) {
	if (target == 0) return true;
	bool ans = false;
	for (int i = 0; i < 4; ++i) {
		if (visited[i]) continue;
		visited[i] = true;
		ans = ans || dfs(nums, visited, target - nums[i]);
		ans = ans || dfs(nums, visited, target + nums[i]);
		ans = ans || dfs(nums, visited, target * nums[i]);
		if (target % nums[i] == 0) ans = ans || dfs(nums, visited, target / nums[i]);
		if (ans) return true;
		visited[i] = false;
	}
	return false;
}
int main() {
    vector<int> nums(4, 0);
    vector<bool> visited(4, false);
    for (int i = 0; i < 4; ++i) cin >> nums[i];
    if (dfs(nums, visited, 24)) cout << "true";
    else cout << "false";
    return 0;
}

2.5 Leetcode 78 子集

class Solution {
public:
    vector<vector<int>> ans;
    vector<int> tmp;
    vector<vector<int>> subsets(vector<int>& nums) {
        int n = nums.size();
        backtracking(nums, n, 0);
        return ans;
    }

    void backtracking(vector<int>& nums, int n, int idx) {
        if (idx == n) {
            ans.push_back(tmp);
            return ;
        }
        backtracking(nums, n, idx + 1); // 不考虑该点
        tmp.push_back(nums[idx]); // 考虑该点
        backtracking(nums, n, idx + 1);
        if (tmp.back() == nums[idx]) tmp.pop_back();
    }
};

我自己也准备了第二种解法,如下所示:

class Solution {
public:
    vector<vector<int>> ans;
    vector<int> tmp;
    vector<vector<int>> subsets(vector<int>& nums) {
        int n = nums.size();
        subsets(nums, 0, n);
        return ans;
    }

    void subsets(vector<int>& nums, int idx, int n) {
        if (idx == n) {
            ans.push_back(tmp);
            return;
        }
        // 不做添加,直接加入ans
        ans.push_back(tmp);
        // 按顺序遍历加入ans
        for (int i = idx; i < n; ++i) {
            tmp.push_back(nums[i]);
            subsets(nums, i + 1, n);
            tmp.pop_back();
        }
    }
};

2.6 Leetcode 129 求根节点到叶节点数字之和

class Solution {
public:
    int ans = 0, tmp = 0;
    int sumNumbers(TreeNode* root) {
        if (!root) {
            return 0;
        }
        tmp = 10 * tmp + root->val;
        if (!root->left && !root->right) {
            ans += tmp;
        }
        if (root->left) sumNumbers(root->left);
        if (root->right) sumNumbers(root->right);
        tmp /= 10;
        return ans;
    }
};

也可以写一个backtracking函数做记录。

class Solution {
public:
    int ans = 0, tmp = 0;
    int sumNumbers(TreeNode* root) {
        if (!root) return 0;
        bactracking(root);
        return ans;
    }
    void bactracking(TreeNode* root) {
        tmp = 10 * tmp + root->val;
        // 如果是末叶节点
        if (!root->left && !root->right) {
            ans += tmp;
        }
        if (root->left) bactracking(root->left);
        if (root->right) bactracking(root->right);
        tmp /= 10;
    }
};
  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值