双向DFS
定义
双向深度优先搜索(Bi-directional Depth First Search, BD-DFS)是一种在图或树中寻找两点间路径的算法。与传统的单向DFS不同,BD-DFS同时从起始节点和目标节点出发进行搜索,使用两个DFS过程。一个向前探索从起点到中间状态的路径,另一个向后探索从终点到相同中间状态的路径。当两个搜索的前沿相遇时,即可找到一条从起点到终点的路径。
运用情况
- 优化搜索效率:当搜索空间非常大时,双向搜索可以显著减少搜索的总节点数,因为每个方向上的搜索都不需要遍历完整个空间。
- 明确初态和终态:适用于已知明确的起始状态和结束状态的问题,如某些迷宫问题、最短路径问题等,尤其是当两个状态之间的距离较远时。
- 求解最短路径:在某些情况下,如求解有界范围内的最短路径问题,双向DFS配合剪枝策略能有效降低时间复杂度。
- 状态空间对称性:当搜索空间存在某种对称性,且从两端开始搜索能更快相遇时,使用双向DFS更为高效。
注意事项
- 空间利用:需要额外的数据结构来存储两个方向的搜索状态,以及可能的中间状态匹配。
- 状态匹配:设计有效的数据结构(如哈希表)来快速判断两个搜索过程中的状态是否匹配,这是算法成功的关键。
- 剪枝:合理设置剪枝条件,避免重复搜索和无效扩展,尤其是在状态空间较大的情况下。
- 同步控制:两个方向的搜索需要适当同步,确保不会错过可能的交汇点,特别是在动态规划或状态压缩场景下。
解题思路
- 初始化:分别从起点和终点开始,进行DFS,并记录已访问的状态。
- 状态标记与存储:对每个访问的状态进行标记,防止重复访问,并将这些状态存储起来以便后续比较。
- 中间状态匹配:设计机制检查两个搜索过程中生成的状态是否有交集。这可以通过维护两个集合并在每次扩展状态时检查是否存在交集来实现。
- 收敛:一旦发现交集,即找到了从起点到终点的路径。此时可以通过回溯或保存的路径信息来重构完整的路径。
- 优化:根据问题特性,考虑使用启发式信息、剪枝策略等进一步优化搜索过程,尤其是在搜索空间较大时。
AcWing 171. 送礼物
题目描述
171. 送礼物 - AcWing题库
运行代码
#include <iostream>
#include <algorithm>
#include <vector>
using namespace std;
typedef long long LL;
const int N = 1 << 24;int n, m, k;
int g[50], weights[N];
int cnt = 0;
int ans;void dfs(int u, int s)
{if(u == k){weights[cnt ++ ] = s;return;}if((LL) s + g[u] <= m) dfs(u + 1, s + g[u]);dfs(u + 1, s);
}void dfs2(int u, int s)
{if (u == n){int l = 0, r = cnt - 1;while (l < r){int mid = l + r + 1 >> 1;if (weights[mid] + (LL)s <= m) l = mid;else r = mid - 1;}if (weights[l] + (LL)s <= m) ans = max(ans, weights[l] + s);return;}if ((LL)s + g[u] <= m) dfs2(u + 1, s + g[u]);dfs2(u + 1, s);
}int main()
{cin >> m >> n;for(int i = 0; i < n; i ++ ) cin >> g[i];sort(g, g + n, greater<int>());k = n / 2;dfs(0, 0);sort(weights, weights + cnt);int t = 1;for (int i = 1; i < cnt; i ++ )if (weights[i] != weights[i - 1])weights[t ++ ] = weights[i];cnt = t;dfs2(k, 0);cout << ans << endl;return 0;
}
代码思路
-
输入处理与预处理:读取背包最大承重
m
和物品数量n
,以及每个物品的重量g[]
。然后按重量从大到小排序物品,以利于贪心策略。 -
前向DFS (
dfs
函数):从物品集合的前半部分(n/2
个物品)开始,计算所有可能的子集重量并存储在weights[]
数组中。这个步骤利用了深度优先搜索遍历所有组合。 -
排序与去重:对
weights[]
数组进行排序,并去除重复的重量值,以便于二分查找。 -
后向DFS (
dfs2
函数):从物品集合的后半部分开始,对每个可能的子集重量加上当前累计重量s
,并使用二分查找在前半部分得到的子集重量中寻找合适的配对,更新最大总重量ans
。 -
输出结果:输出最终得到的最大总重量。
改进思路
-
记忆化搜索:虽然原始代码没有直接使用,但考虑到DFS的递归调用,如果遇到更复杂的约束或更大规模的问题,引入记忆化可以避免重复计算,提高效率。
-
迭代代替递归:DFS递归可能导致栈溢出,对于大规模问题,可以考虑使用迭代方式(如栈模拟)替代递归。
-
更高效的搜索算法:对于背包问题,动态规划(DP)通常比DFS更高效。此问题可以考虑使用二维DP数组,其中
dp[i][j]
表示前i
个物品中选取总重量不超过j
的最大价值(这里价值即重量)。但由于需要分两部分考虑,可以先DP计算前半部分所有可能的组合,再基于这些组合结果对后半部分做优化搜索。 -
剪枝优化:在DFS过程中,可以通过剪枝策略进一步减少不必要的搜索,比如在前向或后向DFS中,如果当前路径的重量已经不可能超过当前最优解,则停止该分支的搜索。