Loading

浅谈双向搜索

前置知识

搜索。

引入

我们知道,暴力搜索的复杂度往往是指数级的,这在数据范围稍微大一点的情况下就难以承受。而双向搜索可以通过一些方法对搜索进行优化。本文介绍两种双向搜索——双向同时搜索和 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,然后判断是否符合题意。时间复杂度是 \(\mathcal O(V^6)\),也就是 \(\mathcal O(10^{12})\),是难以承受的。

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

\[\begin{aligned}\frac{a*b+c}{d}-e&=f\\\frac{a*b+c}{d}&=f+e\\a*b+c&=(f+e)*d\end{aligned} \]

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

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

#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

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

#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 和不选。复杂度 \(\mathcal O(3^{\frac{n}{2}})\)

但是交上去发现 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 @ 2023-04-14 17:26  Thunder_S  阅读(131)  评论(0编辑  收藏  举报