浅谈双向搜索
前置知识#
搜索。
引入#
我们知道,暴力搜索的复杂度往往是指数级的,这在数据范围稍微大一点的情况下就难以承受。而双向搜索可以通过一些方法对搜索进行优化。本文介绍两种双向搜索——双向同时搜索和 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#
有一种显然的想法就是暴力枚举 a,b,c,d,e,f,然后判断是否符合题意。时间复杂度是 ,也就是 ,是难以承受的。
但是我们可以对式子进行变形。
至此,我们将 a,b,c
和 d,e,f
分成了两组,我们对这两组分别搜索,将所有可能的值各自统计,然后有双指针查找其中相同的值。时间复杂度直接降到了 ,这已经是可以接受的了。
从中可以看出,分组独立搜索是基于待搜索的每个元素间没有联系,从而保证分组的可行性。独立搜索后,再使用一些方法将每个组的答案合并。一般来说,我们会将元素分成两组,然后用双指针来合并答案。
#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#
如果直接搜索是 ,但是我们可以只搜一半,也就是搜到对角线()就停下不搜,在每个格子记录当前异或和。然后从 倒着搜,搜到搜过的格子,就用第二次搜去找到第一次搜的方案,用桶来实现即可。时间复杂度 。
#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
根据之前的做法,我们是将 个数分成两组,每一组中的每个数有三种选法:集合 A,集合 B 和不选。复杂度 。
但是交上去发现 WA
了。
注意到,题目要求的是先选出一个集合,再将集合分成两个集合。对于 3 3 3 3
来说,按照上面的做法,在四个都选的情况下,会有 4 种情况:
- 1A,2B
- 1A,2A
- 1B,2A
- 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 则强调待搜元素间没有联系,进而能够分组独立搜索。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 从 HTTP 原因短语缺失研究 HTTP/2 和 HTTP/3 的设计差异
· AI与.NET技术实操系列:向量存储与相似性搜索在 .NET 中的实现
· 基于Microsoft.Extensions.AI核心库实现RAG应用
· Linux系列:如何用heaptrack跟踪.NET程序的非托管内存泄露
· 开发者必知的日志记录最佳实践
· winform 绘制太阳,地球,月球 运作规律
· TypeScript + Deepseek 打造卜卦网站:技术与玄学的结合
· Manus的开源复刻OpenManus初探
· 写一个简单的SQL生成工具
· AI 智能体引爆开源社区「GitHub 热点速览」