适用于输入数据较小,但还没小到能直接使用暴力搜索的情况。
在广度优先搜索中,如果结点数扩展增长过快,可以考虑双向广 搜。(若扩展快,但总状态量不大,也可直接用广搜)
⚫ 应用场合:有确定的起点和终点,并且能把从起点到终点的单个 搜索,变换为分别从起点出发和从终点出发的“相遇”问题。
⚫ 实现方法:(1)合用一个队列,交替进行。两个方向的搜索产生相 同的子状态,结束。适合正反方向扩展新节点数量差不多的情况; (2)分成两个队列,让子状态少的BFS先扩展,可以减少搜索的总 状态数,尽快相遇。
以一道例题进行讲解
题目背景
题目描述
给出一张 n n n 个点 m m m 条边的无向图,每个点的初始状态都为 0 0 0。
你可以操作任意一个点,操作结束后该点以及所有与该点相邻的点的状态都会改变,由 0 0 0 变成 1 1 1 或由 1 1 1 变成 0 0 0。
你需要求出最少的操作次数,使得在所有操作完成之后所有 n n n 个点的状态都是 1 1 1。
输入格式
第一行两个整数 n , m n, m n,m。
之后 m m m 行,每行两个整数 a , b a, b a,b,表示在点 a , b a, b a,b 之间有一条边。
输出格式
一行一个整数,表示最少需要的操作次数。
本题保证有解。
样例 1
样例输入 1
5 6
1 2
1 3
4 2
3 4
2 5
5 3
样例输出 1
3
提示
对于 100 % 100\% 100% 的数据, 1 ≤ n ≤ 35 , 1 ≤ m ≤ 595 , 1 ≤ a , b ≤ n 1\le n\le35,1\le m\le595, 1\le a,b\le n 1≤n≤35,1≤m≤595,1≤a,b≤n。保证没有重边和自环。
接下来就是对oi wiki上的该题的题解以及代码进行解释和补充。
如果这道题暴力 DFS 找开关灯的状态,时间复杂度就是 O(2^{
n}), 显然超时。不过,如果我们用 meet
in middle 的话,时间复杂度可以优化至 O(n2^{
n/2})。meet in middle 就是让我们先找一半的状态,
也就是找出只使用编号为 1 到 \mathrm{
mid} 的开关能够到达的状态,再找出只使用另一半开关能到达
的状态。如果前半段和后半段开启的灯互补,将这两段合并起来就得到了一种将所有灯打开的方案。具
体实现时,可以把前半段的状态以及达到每种状态的最少按开关次数存储在 map 里面,搜索后半段时,
每搜出一种方案,就把它与互补的第一段方案合并来更新答案。
解题思路:
本题使用了大量的位运算。考虑到n最大为35,我们可以使用一个long long类型的数组a来表示每盏灯及其相连的灯。
先点亮自身那一位
for (int i = 1; i < n; ++i) a[i] = a[i - 1] * 2; // 进行预处理
a[0] = 1
->0001
(二进制)a[1] = 2
->0010
(二进制)a[2] = 4
->0100
(二进制)a[3] = 8
->1000
(二进制)然后处理与之连接的灯。读入u和v相连
a[u] |= ((long long)1 << v);a[v] |= ((long long)1 << u);
- 0和1:
a[0] = 0001 | 0010 = 0011
(二进制)a[1] = 0010 | 0001 = 0011
(二进制)- 2和3:
a[2] = 0100 | 1000 = 1100
(二进制)a[3] = 1000 | 0100 = 1100
(二进制)然后就是双向搜索处理:
对于前一半,二分之n个灯,则有 2 n / 2 2^{n/2} 2n/2种情况(每一个灯都有按了开关和没按开关两种情况)。所以我们需要一次遍历这 2 n / 2 2^{n/2} 2n/2种情况。并通过位运算记录每一种情况下最后哪些灯是亮着的,并记录该情况下开关按了几次(用map记录,灯亮情况作为key,按键次数为value)。然后在对后半部分进行遍历的时候,对后一半灯的所有可能状态进行枚举,并尝试与前一半灯的状态进行组合,以找到所有灯都打开的最小按键次数。
#include <algorithm>
#include <cstdio>
#include <iostream>
#include <map>
using namespace std;
int n,m,ans=0x7fffffff; //点的数目,边的条数
map<long long ,int>f;
long long a[36];
int main(){
ios::sync_with_stdio(false),cin.tie(0),cout.tie(0); //可以加快io速度
cin>>n>>m;
a[0]=1;
for(int i=1;i<n;++i){
a[i]=a[i-1]<<1;} //也可以乘以2,但位运算快一点
int u,v;
for(int j=0;j<m;++j){
//++j和j++效果一样,但++j速度更快
cin>>u>>v;
--u;
--v;
a[u] |= ((long long)1 << v); //不要写成a[u]|=a[v]了,注意区别.并注意long long类型
a[v] |= ((long long)1 << u);
}
//对前一半进行操作
for(int i=0;i<1<<(n/2);++i){
//二分之n最多20,用int就可以了。变量i二进制为1的位,就是按了开关的位
long long t=0; //记录按开关后灯的状态
int cnt=0;
for(int j=0;j<n/2;++j){
//看情况i有多少位是按了开关的,一位一位地排查
if((i>>j)&1){
//1的二进制数为00000001,进行按位与运算,前面都是0就只需要关注i>>j的最后一位就行了
t ^= a[j];
++cnt;
}
}
//写入或更新map
if(!f.count(t)){
f[t]=cnt;
}
else{
f[t] = min(f[t], cnt);
}
}
//对后一半进行操作
for (int i = 0; i < (1 << (n - n / 2)); ++i) {
long long t = 0;
int cnt = 0;
for (int j = 0; j < (n - n / 2); ++j) {
if ((i >> j) & 1) {
t ^= a[n / 2 + j]; //注意要加个二分之n
++cnt;
}
}
//现在将每一种处理了的情况尝试与前一半灯的状态进行组合,先判断存不存在互补的键
if(f.count((((long long)1<<n)-1)^t)){
//(((long long)1<<n)-1)得到一个位全是1的,再与t异或,得到互补的二进制数
ans = min(ans, cnt + f[(((long long)1 << n) - 1) ^ t]);
}
}
cout<<ans;
return 0;
}