当前位置: 首页 > news >正文

深度优先搜索(DFS)剪枝技术详解与C++实现

        深度优先搜索(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];
}

六、总结

  • 可行性剪枝:提前终止不可能满足约束的路径。

  • 搜索顺序剪枝:优化搜索顺序以快速逼近解。

  • 最优性剪枝:维护已知最优解,剪掉更劣路径。

  • 排除等效冗余:避免重复状态,减少冗余计算。

  • 记忆化搜索:缓存中间结果,避免重复递归。

综合应用:在实际问题中,常需结合多种剪枝技术。例如:

  1. 预处理排序(可行性剪枝 + 排除等效冗余)。

  2. 状态记录(记忆化搜索 + 排除等效冗余)。

  3. 动态更新最优解(最优性剪枝 + 可行性剪枝)。


七、 小例题

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加1
        return;  // 返回,结束当前DFS分支
    }
    for (int i = x; sum + i * (k - u) <= n; i++) {  // 从x开始枚举下一个数,确保剩余数的和不超过n
        dfs(i, sum + i, u + 1);  // 递归调用DFS,继续划分下一个数
    }
}

int main() {
    cin >> n >> k;  // 输入n和k
    dfs(1, 0, 0);  // 从1开始划分,初始和为0,已划分的数的个数为0
    cout << cnt;  // 输出合法的划分方案数
    return 0;  // 程序正常结束
}

代码设计思路详解

  1. 问题分析

    • 题目要求将整数n划分为k份,且每份不能为空,且不考虑顺序。

    • 例如,n=7, k=3时,{1,1,5}、{1,5,1}、{5,1,1}被认为是相同的划分,只算一种。

  2. DFS模拟划分过程

    • 为了避免重复计算,DFS过程中强制要求划分的数从小到大排列。

    • 第一个数从1开始,第二个数必须大于或等于第一个数,以此类推。

    • 这样做的目的是确保划分的唯一性,避免重复计算。

  3. 剪枝优化

    • 在DFS过程中,通过sum + i * (k - u) <= n进行剪枝。

    • 这个条件的意思是:当前划分的数i,加上剩余未划分的数(每个数至少为i),总和不能超过n

    • 如果超过n,则当前分支不可能得到合法的划分,直接跳过。

  4. 递归终止条件

    • 当已经划分了k个数(u == k),并且这些数的和等于nsum == n),则说明找到了一个合法的划分方案,cnt加1。

  5. 时间复杂度

    • 由于DFS需要枚举所有可能的划分方案,当n=200k=6时,计算量非常大(代码中提到num=147123026)。

    • 但由于剪枝的存在,实际计算量会大大减少。

为什么这样设计?

  1. 从小到大划分

    • 为了避免重复计算,强制要求划分的数从小到大排列。

    • 例如,{1,2,4}和{1,4,2}是相同的划分,但通过从小到大排列,只计算{1,2,4}。

  2. 剪枝条件

    • sum + i * (k - u) <= n是核心剪枝条件。

    • 它确保了当前划分的数i加上剩余数的和不会超过n,从而避免了无效的DFS分支。

  3. 递归设计

    • 每次递归调用时,x表示当前划分的数,sum表示前u-1个数的和,u表示已经划分的数的个数。

    • 这种设计使得DFS能够逐步构建划分方案,并在每一步进行剪枝。

相关文章:

  • Python第五章02:函数的参数和练习
  • JavaWeb全链路学习:10、Mybatis
  • Spring(6)——Spring、Spring Boot 与 Spring MVC 的关系与区别
  • 前端组件、组件库使用指南
  • 【Linux】learning notes(4)cat、more、less、head、tail、vi、vim
  • Leetcode-2272. Substring With Largest Variance [C++][Java]
  • AI第一天 自我理解笔记--生成文本概率Top-k p 束搜索 贪心搜索温度
  • Python 鼠标轨迹算法 - 防止游戏检测
  • OSPF-4 2类LSA NetworkLSA
  • airtest用法
  • 欧税通:日本JCT标准申报的具体税率是多少?
  • N皇后问题——dfs解法(回溯+减枝+深搜)
  • Unreal Engine 5.4 (虚幻引擎5)中动画制作快捷键大全
  • 自动化测试脚本
  • 深入解析 Kubernetes Worker Node 组件
  • SpringBoot 第一课(Ⅲ) 配置类注解
  • 经历过的IDEA+Maven+JDK一些困惑
  • DNS服务器的简单搭建方法
  • Linux 上常见的解压缩命令
  • STM32F407——RTC实时时钟
  • 织梦网站环境搭建/北京网站seo优化推广
  • 网站模板中文/网络营销pdf
  • 面料做电商 哪个网站好/百度指数怎么做
  • 青岛做网站大公司有哪些/互联网广告销售好做吗
  • 网站制作公司起名/semir是什么意思
  • 网页与网站/seo课程培训