【小白学算法】双向搜索超详细解析+例题[cf_contest525_E_Anya and Cubes]

什么是双向搜索?

双向搜索,也称为Meet-in-the-Middle算法,是一种用于解决搜索空间过大问题的经典算法技巧。它的核心思想是分而治之:将原问题分解为两个规模较小的子问题,分别求解后再合并结果。

既然要从A点到B点,那我们为什么不同时从A点往B点搜索,从B点往A点搜索呢?

想象一下,你和朋友约定在一个很大的商场里见面。与其你一个人慢慢找过去,不如你们两个人同时出发,向对方走去,这样就能更快地碰面了!

算法优势

对于一个搜索空间为O(\(2^n\))的问题:

  1. 将n个元素分成两部分,每部分约n/2个元素

  2. 分别枚举两部分的所有可能状态

  3. 通过某种方式将两部分的结果进行匹配组合

时间复杂度优化:从O(\(2^n\))降低到O(\(2^{n/2}\))

当d=6,b=3时:

  • 单向搜索:\(3^6\) = 729 个节点

  • 双向搜索:2×\(3^3\) = 54 个节点

性能提升超过13倍!

双向搜索适用问题类型

  • 子集枚举问题(n通常在20-40之间)

  • 状态搜索问题,且状态可以分解

  • 优化问题,需要枚举所有可能的组合

  • 数论问题中的穷举搜索

伪代码

void solve() {
    // 1. 分割问题
    int mid = n / 2;
    
    // 2. 搜索左半部分
    vector<State> left_states;
    dfs_left(0, initial_state, left_states);
    
    // 3. 搜索右半部分  
    vector<State> right_states;
    dfs_right(mid, initial_state, right_states);
    
    // 4. 预处理(使用数组时可以排序)
    sort(right_states.begin(), right_states.end());
    
    // 5. 合并结果
    for (auto& left : left_states) {
        // 在right_states中查找匹配的状态
        // 通常使用数组二分查找或者map/unordered_map
    }
}
//当然,其实有的题可以再搜索出左半部分后,在搜索右半部分时直接进行匹配,这样时间复杂度会更低
//这里为了更清晰 所以我将步骤拆分,搜左——>搜右——>合并结果并判断

例题cf_contest525_E_Anya and Cubes

此题题解有很多,今天讲一个没有过多剪枝(相对优化很多的题解更暴力啦~ )的题解,保证大家能看懂嘟

题目转化

给定一个长度为n的数组a,我们有k个"!"操作,每个操作可以将数组中的一个数变为它的阶乘(同一个数最多变一次)。求选择任意个数使它们的和等于S的方案数。

约束条件

  • 1 ≤ n ≤ 25

  • 0 ≤ k ≤ n

  • 0 ≤ $a_i $≤ \(10^9\)

  • 1 ≤ S ≤ \(10^{16}\)

为什么使用双向搜索?

对于每个数 \(a_i\),我们有3种选择:

  1. 不选择

  2. 选择原值 \(a_i\)

  3. 选择阶乘值\(a_i\)!(需要消耗一个"!"操作)

如果直接枚举,时间复杂度为O(\(3^n\)),当n=25时约为8.4×\(10^{11}\),明显超时。

使用双向搜索后,复杂度降为O(\(3^{n/2}\)),当n=25时约为1.6×\(10^6\),完全可以接受

算法实现

  • 用 pair< int,int >来表示每一个状态,即每一种情况 { 当前sum值,当前选的阶乘数 } 。

  • 在计算阶乘时,虽然题中a_i的取值为0~10^9,但实际上当a_i ≥19时,就已经超过了S的最大值10^{16},所以这一步剪枝就是此题是否能ac的关键,我们直接将≥19的a_i在计算阶乘时返回s+1,这样省去了很多时间。

  • 虽然n最大25,但是还是有可能会有重复的a_i, 所以在计算阶乘时也可以顺便记忆化一下

下面就是代码样例啦(顺便给大家附上详细注释嘟)~

#include<iostream>
#include<unordered_map>
#include<vector>
using namespace std;
#define int long long
int a[30];
int ans=0;
int n,k,s;
bool visited[30];
int per[30];
int mid;
vector<pair<int,int> > L,R;
int getfac(int x){
 if(a[x]>19) return s+1; //不可能解 直接返回无解数剪枝 减少时间
 if(visited[x])
     return per[x];//判断是否计算过当前数的阶乘
 int sum=1;
 for(int i=1;i<=a[x];i++){
     sum*=i;
 }
 per[x]=sum;
 visited[x]=1;//未计算过则记忆化
//    cout<<a[x]<<" "<<sum<<endl;
 return sum;
}
void dfsl(int now_s,int depth,int p){
 if(depth>=mid){
     L.push_back({now_s,p});//前半部分所有数均已判断过,此时记录答案,存入动态数组中
     return ;
 }
 dfsl(now_s,depth+1,p);//当前数不加入sum

 dfsl(now_s+a[depth],depth+1,p);//当前数按照原数值加入sum

 if(p<k){
     int t= getfac(depth);
//        cout<<t<<endl;
     if(t<=s)//判断是否可加,提前剪枝
         dfsl(now_s+t,depth+1,p+1);//可加入情况下,计算其阶乘加入sum
 }
}
void dfsr(int now_s,int depth,int p){//该部分与dfsl完全相同
 if(depth>=n){
     R.push_back({now_s,p});
     return ;
 }
 dfsr(now_s,depth+1,p);

 dfsr(now_s+a[depth],depth+1,p);

 if(p<k){
     int t= getfac(depth);
     if(t<=s)//判断是否可加,提前剪枝
         dfsr(now_s+t,depth+1,p+1);//可加入情况下,计算其阶乘加入sum
 }
}
signed main(){

 cin>>n>>k>>s;

 mid=n/2;//去n的中值,将搜索过程分为两半

 for(int i=0;i<n;i++){
     cin>>a[i];
 }
 dfsl(0,0,0);
 dfsr(0,mid,0);

 sort(R.begin(),R.end());
 //由于我在算右半部分时没有直接对结果进行处理,所以我需要单独匹配合并一下答案
 //遍历左半部分的结果,找右半部分是否有符合解
 //这里的sort没有自定义排序,那么他会按照pair的默认排序,
 //首先按first排序(sum值)
 //first相同时按second排序
 for(auto &l :L){
     int ns=s-l.first;//计算出当前需要找到的sum,然后再R里二分查找
     int lb= lower_bound(R.begin(),R.end(), make_pair(ns,0))-R.begin();//找到sum且是0个阶乘数的位置
     int rb= upper_bound(R.begin(),R.end(), make_pair(ns,k-l.second))-R.begin();//计算出此时剩余可选最大阶乘数,那么其下一个地址一定不合题意
//        cout<<lb<<"   "<<rb<<endl;
     ans+=rb-lb;//此时lb~rb-1就是解区间 那么其区间长度则为rb-lb
 }
 cout<<ans<<endl;//全部遍历完后就可以输出结果啦~
 return 0;
}
posted @ 2025-08-20 16:03  芝士青瓜不拿铁  阅读(14)  评论(0)    收藏  举报