深度优先搜索(DFS)剪枝技术通过提前终止无效路径的搜索,大幅提升算法效率。以下是五种核心剪枝技术的详细解析及C++代码示例:
目录
一、可行性剪枝
C++实现示例
二、搜索顺序剪枝
伪代码逻辑
三、最优性剪枝
C++实现示例
四、排除等效冗余
C++实现示例
五、记忆化搜索(Memoization)
C++实现示例
六、总结
七、 小例题
P1025 [NOIP 2001 提高组] 数的划分 - 洛谷
算法代码:
代码设计思路详解
为什么这样设计?
一、可行性剪枝
核心思想:若当前路径明显无法满足问题的约束条件,则提前回溯。
典型应用:组合总和问题。
C++实现示例
class Solution {
public:vector<vector<int>> combinationSum(vector<int>& candidates, int target) {vector<vector<int>> res;vector<int> path;sort(candidates.begin(), candidates.end()); // 排序便于剪枝dfs(candidates, target, 0, path, res);return res;}void dfs(vector<int>& nums, int target, int start, vector<int>& path, vector<vector<int>>& res) {if (target < 0) return; // 可行性剪枝:路径和已超目标if (target == 0) {res.push_back(path);return;}for (int i = start; i < nums.size(); ++i) {if (nums[i] > target) break; // 剪枝:后续元素更大,无需尝试path.push_back(nums[i]);dfs(nums, target - nums[i], i, path, res); // 允许重复path.pop_back();}}
};
二、搜索顺序剪枝
核心思想:调整搜索顺序,优先处理更可能找到解的分支,减少无效搜索。
典型应用:数独问题中优先填充空格较少的行或列。
伪代码逻辑
void solveSudoku(vector<vector<char>>& board) {vector<pair<int, int>> emptyCells = findEmptyCells(board);sort(emptyCells.begin(), emptyCells.end(), [&](auto& a, auto& b) {return getCandidates(a).size() < getCandidates(b).size(); // 优先候选少的格子});dfs(board, emptyCells, 0);
}
三、最优性剪枝
核心思想:若当前路径的代价已超过已知最优解,则停止搜索。
典型应用:旅行商问题(TSP)的简化版本。
C++实现示例
int minCost = INT_MAX;void dfs(int current, int cost, int visited, vector<vector<int>>& graph) {if (cost >= minCost) return; // 最优性剪枝:当前路径已不如已知最优if (visited所有节点) {minCost = min(minCost, cost);return;}for (int next = 0; next < graph.size(); ++next) {if (!visited[next]) {dfs(next, cost + graph[current][next], visited | (1 << next), graph);}}
}
四、排除等效冗余
核心思想:跳过重复或等效的状态,避免生成相同结果的多个分支。
典型应用:含重复元素的全排列问题。
C++实现示例
class Solution {
public:vector<vector<int>> permuteUnique(vector<int>& nums) {vector<vector<int>> res;vector<int> path;vector<bool> used(nums.size(), false);sort(nums.begin(), nums.end()); // 排序便于去重dfs(nums, used, path, res);return res;}void dfs(vector<int>& nums, vector<bool>& used, vector<int>& path, vector<vector<int>>& res) {if (path.size() == nums.size()) {res.push_back(path);return;}for (int i = 0; i < nums.size(); ++i) {if (used[i] || (i > 0 && nums[i] == nums[i-1] && !used[i-1])) continue; // 剪枝:避免重复排列used[i] = true;path.push_back(nums[i]);dfs(nums, used, path, res);path.pop_back();used[i] = false;}}
};
五、记忆化搜索(Memoization)
核心思想:缓存已计算的状态结果,避免重复递归调用。
典型应用:斐波那契数列、爬楼梯问题。
C++实现示例
unordered_map<int, int> memo;int fib(int n) {if (n <= 1) return n;if (memo.find(n) != memo.end()) return memo[n]; // 直接返回缓存结果memo[n] = fib(n-1) + fib(n-2);return memo[n];
}
六、总结
-
可行性剪枝:提前终止不可能满足约束的路径。
-
搜索顺序剪枝:优化搜索顺序以快速逼近解。
-
最优性剪枝:维护已知最优解,剪掉更劣路径。
-
排除等效冗余:避免重复状态,减少冗余计算。
-
记忆化搜索:缓存中间结果,避免重复递归。
综合应用:在实际问题中,常需结合多种剪枝技术。例如:
-
预处理排序(可行性剪枝 + 排除等效冗余)。
-
状态记录(记忆化搜索 + 排除等效冗余)。
-
动态更新最优解(最优性剪枝 + 可行性剪枝)。
七、 小例题
P1025 [NOIP 2001 提高组] 数的划分 - 洛谷
整数划分是经典问题,标准解法是动态规划、母函数,计算复杂度为O(n^2)。当n较小时,也可以用DFS暴搜出答案。
算法代码:
#include <bits/stdc++.h> // 包含标准库的所有头文件
using namespace std; // 使用标准命名空间int n, k, cnt; // n: 要划分的整数;k: 划分的份数;cnt: 记录合法的划分方案数void dfs(int x, int sum, int u) { // DFS函数,参数为:x: 上次划分的数;sum: 前u-1个数的和;u: 已经划分的数的个数if (u == k) { // 如果已经划分了k个数if (sum == n) cnt++; // 如果这些数的和等于n,则cnt加1return; // 返回,结束当前DFS分支}for (int i = x; sum + i * (k - u) <= n; i++) { // 从x开始枚举下一个数,确保剩余数的和不超过ndfs(i, sum + i, u + 1); // 递归调用DFS,继续划分下一个数}
}int main() {cin >> n >> k; // 输入n和kdfs(1, 0, 0); // 从1开始划分,初始和为0,已划分的数的个数为0cout << cnt; // 输出合法的划分方案数return 0; // 程序正常结束
}
代码设计思路详解
-
问题分析:
-
题目要求将整数n划分为k份,且每份不能为空,且不考虑顺序。
-
例如,n=7, k=3时,{1,1,5}、{1,5,1}、{5,1,1}被认为是相同的划分,只算一种。
-
-
DFS模拟划分过程:
-
为了避免重复计算,DFS过程中强制要求划分的数从小到大排列。
-
第一个数从1开始,第二个数必须大于或等于第一个数,以此类推。
-
这样做的目的是确保划分的唯一性,避免重复计算。
-
-
剪枝优化:
-
在DFS过程中,通过
sum + i * (k - u) <= n
进行剪枝。 -
这个条件的意思是:当前划分的数
i
,加上剩余未划分的数(每个数至少为i
),总和不能超过n
。 -
如果超过
n
,则当前分支不可能得到合法的划分,直接跳过。
-
-
递归终止条件:
-
当已经划分了
k
个数(u == k
),并且这些数的和等于n
(sum == n
),则说明找到了一个合法的划分方案,cnt
加1。
-
-
时间复杂度:
-
由于DFS需要枚举所有可能的划分方案,当
n=200
,k=6
时,计算量非常大(代码中提到num=147123026
)。 -
但由于剪枝的存在,实际计算量会大大减少。
-
为什么这样设计?
-
从小到大划分:
-
为了避免重复计算,强制要求划分的数从小到大排列。
-
例如,{1,2,4}和{1,4,2}是相同的划分,但通过从小到大排列,只计算{1,2,4}。
-
-
剪枝条件:
-
sum + i * (k - u) <= n
是核心剪枝条件。 -
它确保了当前划分的数
i
加上剩余数的和不会超过n
,从而避免了无效的DFS分支。
-
-
递归设计:
-
每次递归调用时,
x
表示当前划分的数,sum
表示前u-1
个数的和,u
表示已经划分的数的个数。 -
这种设计使得DFS能够逐步构建划分方案,并在每一步进行剪枝。
-