双向搜索-meet in middle


适用于输入数据较小,但还没小到能直接使用暴力搜索的情况。

在广度优先搜索中,如果结点数扩展增长过快,可以考虑双向广 搜。(若扩展快,但总状态量不大,也可直接用广搜)

⚫ 应用场合:有确定的起点和终点,并且能把从起点到终点的单个 搜索,变换为分别从起点出发和从终点出发的“相遇”问题。

⚫ 实现方法:(1)合用一个队列,交替进行。两个方向的搜索产生相 同的子状态,结束。适合正反方向扩展新节点数量差不多的情况; (2)分成两个队列,让子状态少的BFS先扩展,可以减少搜索的总 状态数,尽快相遇。

以一道例题进行讲解

题目背景

English Edition

题目描述

给出一张 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 1n35,1m595,1a,bn。保证没有重边和自环。
接下来就是对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;
}
  • 31
    点赞
  • 26
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

雙溪舴艋舟

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值