【C++算法】dfs深度优先搜索(下) ——【高效剪枝策略+多场景实战解析】

张开发
2026/4/18 22:39:31 15 分钟阅读

分享文章

【C++算法】dfs深度优先搜索(下) ——【高效剪枝策略+多场景实战解析】
1. 为什么需要剪枝策略深度优先搜索就像是在迷宫里盲目地走遇到死胡同就回头。这种暴力搜索方式在简单场景下还能应付但当问题规模变大时计算量会呈指数级增长。我曾在一次编程比赛中遇到过这种情况用标准DFS解一个15×15的棋盘问题程序跑了10分钟还没出结果。这时候就需要剪枝技术了。简单来说剪枝就是在搜索过程中提前判断某些路径不可能得到解直接跳过这些无效分支。就像园丁修剪树枝一样把没用的枝条剪掉让树长得更好。在实际项目中合理的剪枝能让程序运行时间从几小时缩短到几秒钟。2. 常见剪枝策略详解2.1 可行性剪枝这是最基础的剪枝方法我在做迷宫问题时经常用。比如在八皇后问题中当某一行已经放置了皇后就跳过该行后续的位置检查。具体实现可以这样bool isValid(vectorint board, int row, int col) { for (int i 0; i row; i) { if (board[i] col || abs(board[i] - col) abs(i - row)) { return false; } } return true; } void dfs(vectorint board, int row) { if (row board.size()) { // 找到解 return; } for (int col 0; col board.size(); col) { if (!isValid(board, row, col)) continue; // 可行性剪枝 board[row] col; dfs(board, row 1); } }2.2 最优性剪枝解最短路径问题时如果当前路径长度已经超过已知的最短路径就没必要继续了。我在开发游戏AI时常用这个技巧void dfs(vectorvectorint graph, int pos, int path_len, int min_len) { if (path_len min_len) return; // 最优性剪枝 if (pos target) { min_len min(min_len, path_len); return; } for (auto next : graph[pos]) { dfs(graph, next, path_len 1, min_len); } }2.3 记忆化剪枝有些子问题会被重复计算这时候可以用缓存来存储中间结果。我在做数独求解器时深有体会unordered_mapstring, bool memo; bool dfs(string board) { if (memo.count(board)) return memo[board]; // 记忆化剪枝 // ...其他处理逻辑 memo[board] result; return result; }3. 棋盘类游戏实战3.1 数独求解优化原始DFS解9×9数独可能需要尝试上百万次填充。加入剪枝后基本能在毫秒级完成bool solveSudoku(vectorvectorchar board) { for (int i 0; i 9; i) { for (int j 0; j 9; j) { if (board[i][j] ! .) continue; for (char c 1; c 9; c) { if (!isValid(board, i, j, c)) continue; // 剪枝 board[i][j] c; if (solveSudoku(board)) return true; board[i][j] .; } return false; // 所有数字都尝试过都不行 } } return true; }3.2 五子棋AI设计给电脑设计AI时需要评估棋盘局势。通过alpha-beta剪枝可以大幅减少搜索深度int alphabeta(Board board, int depth, int alpha, int beta, bool isMax) { if (depth 0 || board.isGameOver()) { return board.evaluate(); } if (isMax) { int value INT_MIN; for (auto move : board.getPossibleMoves()) { board.makeMove(move); value max(value, alphabeta(board, depth-1, alpha, beta, false)); board.undoMove(move); alpha max(alpha, value); if (alpha beta) break; // alpha剪枝 } return value; } else { // 类似的最小值处理... } }4. 路径规划场景应用4.1 物流配送路线优化处理50个配送点的路线规划时原始DFS根本算不完。通过以下剪枝可以处理实际问题提前计算各点间最短路径维护当前最短路径上界预估剩余点的最小距离和void dfs(vectorbool visited, int current, int count, int cost, int min_cost, vectorint path) { if (cost min_cost) return; // 最优性剪枝 if (count visited.size()) { min_cost min(min_cost, cost); return; } for (int next 0; next visited.size(); next) { if (visited[next]) continue; int estimate cost dist[current][next]; if (estimate min_remaining[next] min_cost) continue; // 预估剪枝 visited[next] true; path.push_back(next); dfs(visited, next, count1, estimate, min_cost, path); path.pop_back(); visited[next] false; } }4.2 地铁换乘方案搜索处理地铁网络时可以结合广度优先和深度优先的优势先用BFS找出大致方向再用DFS在限定范围内搜索实时更新最优换乘次数void dfs(Station current, Station target, int transfers, int min_transfers, Line current_line) { if (transfers min_transfers) return; // 剪枝 if (current target) { min_transfers min(min_transfers, transfers); return; } for (auto neighbor : current.getNeighbors()) { int new_transfers transfers; if (neighbor.line ! current_line) { new_transfers; } dfs(neighbor.station, target, new_transfers, min_transfers, neighbor.line); } }5. 高级剪枝技巧5.1 对称性剪枝很多问题存在对称性比如棋盘旋转对称。在八数码问题中可以记录状态的最小表示string getCanonical(string state) { vectorstring symmetries; // 生成所有对称状态 return *min_element(symmetries.begin(), symmetries.end()); } void dfs(string state) { string canonical getCanonical(state); if (visited.count(canonical)) return; // 对称剪枝 visited.insert(canonical); // ...继续搜索 }5.2 启发式剪枝结合启发式函数引导搜索方向我在开发拼图游戏AI时发现特别有效int heuristic(State state) { // 计算当前状态到目标的预估距离 } void dfs(State state) { if (heuristic(state) max_depth) return; // 启发式剪枝 // ...继续搜索 }5.3 双端搜索对于知道起点和终点的问题可以两端同时搜索void bidirectional_dfs(Node start, Node end) { unordered_setNode visited_from_start, visited_from_end; stackNode stack_start, stack_end; stack_start.push(start); stack_end.push(end); while (!stack_start.empty() !stack_end.empty()) { // 从起点端搜索 Node current_start stack_start.top(); stack_start.pop(); if (visited_from_end.count(current_start)) { // 找到连接点 return; } // 处理邻居节点... // 从终点端搜索同理... } }6. 性能优化实践6.1 数据结构选择用对数据结构能让剪枝更高效。在解数独时我比较过几种方案二维数组检查O(n)时间位运算标记O(1)时间并查集管理适合连通性问题struct SudokuSolver { int rows[9] {0}; int cols[9] {0}; int boxes[9] {0}; bool dfs(vectorvectorchar board, int pos) { if (pos 81) return true; int i pos / 9, j pos % 9; if (board[i][j] ! .) return dfs(board, pos1); for (char c 1; c 9; c) { int mask 1 (c - 1); if (rows[i] mask || cols[j] mask || boxes[i/3*3j/3] mask) continue; // 设置标记 rows[i] | mask; cols[j] | mask; boxes[i/3*3j/3] | mask; board[i][j] c; if (dfs(board, pos1)) return true; // 回溯 board[i][j] .; rows[i] ^ mask; cols[j] ^ mask; boxes[i/3*3j/3] ^ mask; } return false; } };6.2 并行化搜索对于可以分割的搜索空间可以用多线程加速。我在开发拼图 solver 时这样实现void parallel_search(State state, int depth) { if (depth 0) { sequential_dfs(state); return; } vectorfuturevoid futures; for (auto next_state : state.expand()) { futures.push_back(async(launch::async, parallel_search, next_state, depth-1)); } for (auto f : futures) { f.get(); } }6.3 预处理技巧有些信息可以提前计算好。比如在单词接龙问题中先建立单词邻接表unordered_mapstring, vectorstring buildGraph(vectorstring wordList) { unordered_mapstring, vectorstring graph; unordered_setstring words(wordList.begin(), wordList.end()); for (auto word : wordList) { for (int i 0; i word.size(); i) { string temp word; for (char c a; c z; c) { temp[i] c; if (words.count(temp) temp ! word) { graph[word].push_back(temp); } } } } return graph; }

更多文章