浅谈双向搜索

前置知识#

搜索。

引入#

我们知道,暴力搜索的复杂度往往是指数级的,这在数据范围稍微大一点的情况下就难以承受。而双向搜索可以通过一些方法对搜索进行优化。本文介绍两种双向搜索——双向同时搜索和 Meet in the Middle。

双向同时搜索#

双向同时搜索一般可以应用在起点和终点是确定的,操作可逆的情况,例如马走日八数码问题

双向同时搜索,顾名思义,就是从起点和终点同时搜索,一旦两条搜索的路径有了交点,就表示找到了一条从起点通往终点的路径。

在八数码问题(Luogu P1379)中,一种简单的搜索就是使用 BFS,将起点加入到队列中,然后依次向下扩展。这样做是可以 AC 的,但是很慢,因为状态数很多。

int bfs()//单向搜索,用时 10.15s,内存 16.90MB。
{
    if (st==ed) return 0;
    q.push(st);
    bj[st]=1;
    f[st]=0;
    while (!q.empty())
    {
        int now=q.front(),x,y;q.pop();
        for (int i=3;i;--i)
            for (int j=3;j;--j)
            {
                a[i][j]=now%10,now/=10;
                if (a[i][j]==0) x=i,y=j;
            }
        for (int i=1;i<=3;++i)
                for (int j=1;j<=3;++j)
                    now=now*10+a[i][j];
        if (now==ed) return f[now];
        for (int k=1;k<=4;++k)
        {
            int xx=x+fx[k],yy=y+fy[k];
            if (xx<1||xx>3||yy<1||yy>3) continue;
            swap(a[x][y],a[xx][yy]);
            int nxt=0;
            for (int i=1;i<=3;++i)
                for (int j=1;j<=3;++j)
                    nxt=nxt*10+a[i][j];
            if (bj[nxt]) {swap(a[x][y],a[xx][yy]);continue;}
            f[nxt]=f[now]+1;
            bj[nxt]=bj[now];
            q.push(nxt);
            swap(a[x][y],a[xx][yy]);
        }
    }
    return -1;
}

那么如何加速呢。很显然可以利用双向同时搜索。我们将起点和终点同时加入到队列中,打上不同的标记。那么在搜索的过程中,一旦搜索到与当前点不同的标记,就说明两条路径相交了,那么意味着一条从起点到终点的路径被找到了,也就得到答案了。

int bfs()//双向同时搜索,用时 281ms,内存 1.07MB。
{
    if (st==ed) return 0;
    q.push(st);q.push(ed);//起点终点都加入到队列中
    bj[st]=1;bj[ed]=2;
    f[st]=0;f[ed]=0;
    while (!q.empty())
    {
        int now=q.front(),x,y;q.pop();
        for (int i=3;i;--i)
            for (int j=3;j;--j)
            {
                a[i][j]=now%10,now/=10;
                if (a[i][j]==0) x=i,y=j;
            }
        for (int i=1;i<=3;++i)
                for (int j=1;j<=3;++j)
                    now=now*10+a[i][j];
        for (int k=1;k<=4;++k)
        {
            int xx=x+fx[k],yy=y+fy[k];
            if (xx<1||xx>3||yy<1||yy>3) continue;
            swap(a[x][y],a[xx][yy]);
            int nxt=0;
            for (int i=1;i<=3;++i)
                for (int j=1;j<=3;++j)
                    nxt=nxt*10+a[i][j];
            if (bj[nxt])
            {
                if (bj[nxt]==bj[now]) {swap(a[x][y],a[xx][yy]);continue;}
                else return f[now]+f[nxt]+1;//搜到不一样的标记就得到了答案。
            }
            f[nxt]=f[now]+1;
            bj[nxt]=bj[now];
            q.push(nxt);
            swap(a[x][y],a[xx][yy]);
        }
    }
    return -1;
}

可以看到,双向搜索的用时和内存都远远小于单向搜索,这是因为减小了无用的状态数。这也就是双向同时搜索带来的优化。

Meet in the Middle#

我一般将 Meet in the Middle 翻译为折半搜索,这和二分搜索是不一样。这里的折半指的是将数据分割,各自独立搜索,然后计算答案。

下面以几道题来详细的到底什么是分组独立搜索。

SP4580 ABCDEF - ABCDEF#

SP4580 ABCDEF - ABCDEF

有一种显然的想法就是暴力枚举 a,b,c,d,e,f,然后判断是否符合题意。时间复杂度是 O(V6),也就是 O(1012),是难以承受的。

但是我们可以对式子进行变形。

ab+cde=fab+cd=f+eab+c=(f+e)d

至此,我们将 a,b,cd,e,f 分成了两组,我们对这两组分别搜索,将所有可能的值各自统计,然后有双指针查找其中相同的值。时间复杂度直接降到了 O(2V3),这已经是可以接受的了。

从中可以看出,分组独立搜索是基于待搜索的每个元素间没有联系,从而保证分组的可行性。独立搜索后,再使用一些方法将每个组的答案合并。一般来说,我们会将元素分成两组,然后用双指针来合并答案

#include<cstdio>
#include<algorithm>
#define N 1000005
using namespace std;
int n,num1,num2,ans,a[N],a1[N],a2[N],cnt[N];
int main()
{
    scanf("%d",&n);
    for (int i=1;i<=n;++i)
        scanf("%d",&a[i]);
    for (int i=1;i<=n;++i)
        for (int j=1;j<=n;++j)
            for (int k=1;k<=n;++k)
                a1[++num1]=a[i]*a[j]+a[k];
    for (int i=1;i<=n;++i)
        for (int j=1;j<=n;++j)
            for (int k=1;k<=n;++k)
                if (a[i]!=0)//注意 d≠0
                    a2[++num2]=(a[j]+a[k])*a[i];
    sort(a1+1,a1+num1+1);sort(a2+1,a2+num2+1);
    for (int i=1,j=1;i<=num2;++i)
    {
        if (i!=1&&a2[i]==a2[i-1]) {cnt[i]=cnt[i-1];continue;}
        while (j<=num1&&a2[i]>=a1[j]) 
        {
            if (a2[i]==a1[j]) cnt[i]++;
            ++j;
        }
    }
    for (int i=1;i<=num2;++i)
        ans+=cnt[i];
    printf("%d\n",ans);
    return 0;
}

CF1006F Xor-Paths#

CF1006F Xor-Paths

如果直接搜索是 O(2n+m),但是我们可以只搜一半,也就是搜到对角线(x+y=n+m2+1)就停下不搜,在每个格子记录当前异或和。然后从 (n,m) 倒着搜,搜到搜过的格子,就用第二次搜去找到第一次搜的方案,用桶来实现即可。时间复杂度 O(2n)

#include<map>
#include<cstdio>
#define N 25
#define ll long long
using namespace std;
int n,m;
ll k,ans,a[N][N];
map<ll,ll> t[N][N];
void dg1(int x,int y,ll sum)
{
    if (x>n||y>m) return;
    if (x+y==(n+m)/2+1)
    {
        ++t[x][y][sum];
        return;
    }
    dg1(x+1,y,sum^a[x+1][y]);
    dg1(x,y+1,sum^a[x][y+1]);
}
void dg2(int x,int y,ll sum)
{
    if (x<1||y<1) return;
    if (x+y==(n+m)/2+1)
    {
        ans+=t[x][y][k^sum^a[x][y]];
        return;
    }
    dg2(x,y-1,sum^a[x][y-1]);
    dg2(x-1,y,sum^a[x-1][y]);
}
int main()
{
    scanf("%d%d%lld",&n,&m,&k);
    for (int i=1;i<=n;++i)
        for (int j=1;j<=m;++j)
            scanf("%lld",&a[i][j]);
    dg1(1,1,a[1][1]);
    dg2(n,m,a[n][m]);
    printf("%lld\n",ans);
    return 0;
}

SP11469 SUBSET - Balanced Cow Subsets#

SP11469 SUBSET - Balanced Cow Subsets

根据之前的做法,我们是将 n 个数分成两组,每一组中的每个数有三种选法:集合 A,集合 B 和不选。复杂度 O(3n2)

但是交上去发现 WA 了。

注意到,题目要求的是先选出一个集合,再将集合分成两个集合。对于 3 3 3 3 来说,按照上面的做法,在四个都选的情况下,会有 4 种情况:

  1. 1A,2B
  2. 1A,2A
  3. 1B,2A
  4. 1B,2B

但是,实际上选 4 个数的集合只有一种。这启发我们再记录一个值,表示当前这个数选了没有。显然对于一种选数的方法,怎么分是我们不关心的。因此我们最后只用统计每种选数的情况是否存在方案即可。

#include<map>
#include<cstdio>
#include<vector>
#include<algorithm>
#define N 15
using namespace std;
int n,cnt,ans,a[N<<1];
bool bj[1<<21];
map<int,int> t;
vector<int> s[1<<21];
void dg1(int x,int mx,int sum,int snow)//snow 表示选人情况
{
    if (x>mx)
    {
        if (t.find(sum)==t.end()) t[sum]=++cnt;//开桶记录
        s[t[sum]].push_back(snow);//对于每个值记录其选人情况
        return;
    }
    dg1(x+1,mx,sum,snow);
    dg1(x+1,mx,sum+a[x],snow|(1<<(x-1)));
    dg1(x+1,mx,sum-a[x],snow|(1<<(x-1)));
}
void dg2(int x,int mx,int sum,int snow)//snow 表示选人情况
{
    if (x>mx)
    {
        if (t.find(sum)==t.end()) return;
        int id=t[sum];//找到相对应的另一半
        for (int i=0;i<s[id].size();++i)
            bj[s[id][i]|snow]=true;//记录是否有方案
        return;
    }
    dg2(x+1,mx,sum,snow);
    dg2(x+1,mx,sum+a[x],snow|(1<<(x-1)));
    dg2(x+1,mx,sum-a[x],snow|(1<<(x-1)));
}
int main()
{
    scanf("%d",&n);
    for (int i=1;i<=n;++i)
        scanf("%d",&a[i]);
    dg1(1,n/2,0,0);
    dg2(n/2+1,n,0,0);
    for (int i=1;i<=(1<<n);++i)
        ans+=bj[i];
    printf("%d\n",ans);
    return 0;
}

总结#

双向搜索是搜索的一种优化,旨在将搜索数量减少从而达到降低时间复杂度的效果。

但注意,双向搜索有其相应的使用范围,如双向同时搜索就要保证起点和终点确定,且步骤可逆。Meet in the Middle 则强调待搜元素间没有联系,进而能够分组独立搜索。

posted @   Thunder_S  阅读(205)  评论(0编辑  收藏  举报
编辑推荐:
· 从 HTTP 原因短语缺失研究 HTTP/2 和 HTTP/3 的设计差异
· AI与.NET技术实操系列:向量存储与相似性搜索在 .NET 中的实现
· 基于Microsoft.Extensions.AI核心库实现RAG应用
· Linux系列:如何用heaptrack跟踪.NET程序的非托管内存泄露
· 开发者必知的日志记录最佳实践
阅读排行:
· winform 绘制太阳,地球,月球 运作规律
· TypeScript + Deepseek 打造卜卦网站:技术与玄学的结合
· Manus的开源复刻OpenManus初探
· 写一个简单的SQL生成工具
· AI 智能体引爆开源社区「GitHub 热点速览」
点击右上角即可分享
微信分享提示
主题色彩