浅谈折半搜索

折半搜索(又称meet in the middle),顾名思义,就是将原有的数据分成两部分分别进行搜索,最后在中间合并的算法。
设对(n)的大小进行搜索所需要的时间复杂度为(O(f(n))),合并时间复杂度为(O(g(n))),那么折半搜索所需要的时间复杂度就为(O(2f(n/2)+g(n)))
一般来说搜索的时间复杂度是指数级别的,而合并的时间复杂度通常不会太高,因此进行折半搜索基本上能让我们通过比暴力算法将近大一倍的数据范围。
下面通过两道经典的题目来对折半搜索做一个简单的讲解。

1. [luoguP4799][CEOI2015 Day2]世界冰球锦标赛

简明题意:
一个人有(m)元钱。有(n)场比赛,每场比赛的门票价格为(c_i)
问这个人有多少种看比赛的方案(一场不看也算做一种,两种方案不同当且仅当两种方案所看的比赛中有至少一场不同)
数据范围:(nleqslant 40, mleqslant 10^{18})

m的范围过大,考虑搜索。但是枚举每场比赛看或者不看的方案数高达(2^{40}=1099511627776approx 10^{12}),显然不能通过本题。
于是我们就要使用折半搜索的思想。将比赛分为(l)(r)两部分,分别算出两部分的所有可能的花费的钱数。
这时情况总数只不过有(2*2^{20}=2097152approx 2*10^6)种,存储起来简直是绰绰有余。
然后将(l)部分的比赛进行排序,然后每次取出(r)部分的一个比赛,进行二分查找统计可行的方案即可。

code:

#include <iostream>
#include <cstdio>
#include <cstring>
#include <algorithm>
#include <cmath>
#define int long long//不开long long见祖宗
using namespace std;
int n,m,ans,price[100];//ans是总的方案数
int ansl,ansr,l[1<<21],r[1<<21];//ansl是l部分的方案总数,ansr是r部分的方案总数,l和r分别存储两部分的所有方案
void ldfs(int ll,int rr,int now)
{
    if(now>m)return;
    if(ll>rr)//注意判定方法
    {
        l[++ansl]=now;//增添一种新方案
        return;
    }
    ldfs(ll+1,rr,now+price[ll]);//看ll这场比赛
    ldfs(ll+1,rr,now);//不看
}
void rdfs(int ll,int rr,int now)//同上
{
    if(now>m)return;
    if(ll>rr)
    {
        r[++ansr]=now;
        return;
    }
    rdfs(ll+1,rr,now+price[ll]);
    rdfs(ll+1,rr,now);
}
signed main()
{
    scanf("%lld%lld",&n,&m);
    for(int i=1;i<=n;i++)scanf("%lld",&price[i]);
    ldfs(1,n/2,0);rdfs(n/2+1,n,0);//对两部分分别进行搜索
    sort(l+1,l+ansl+1);//对l数组进行排序,方便后续的合并操作
    for(int i=1;i<=ansr;i++)ans+=upper_bound(l+1,l+ansl+1,m-r[i])-l-1;//显然,如果两部分价钱的和不超过m,那就有了一种总的方案
    printf("%lld
",ans);
    return 0;
}

2. [luoguP3067][USACO12OPEN]Balanced Cow Subsets G

简明题意:求从(n)个数((m_i))任意选出一些数,使这些数可以分为和相等的两部分的方案数。
数据范围:(2leqslant nleqslant 20, 1leqslant m_ileqslant 10^8)

这道题比刚刚的题目更加复杂了一些,因为现在每个数有三种状态,可以不选,可以放到左边集合,也可以放到右边集合。
(3**20=3486784401approx 3.5*10^9),依然爆炸。于是同样考虑折半搜索。

看上去可以直接用上一道题的方法进行搜索,使用0,1,-1分别代表不选,选到当前集合,选到对面集合。
但是,这道题毒瘤的地方在于,可能会有重复的情况。
出现这种重复情况的原因在于这道题选出的数是没有顺序的,比如下面的数据:

4
3 3 3 3

你在寻找{3,3,3,3}这一组数据的时候,会被重复统计高达6次。
具体地,我们用(l)来表示将元素放到左集合(S_l),用(r)来表示将元素放到右集合(S_r)
(S_l: ll, lr, rl, rr)
(S_r: ll, lr, rl, rr)
于是有(ll.rr, rr.ll, lr.lr ,lr.rl, rl.lr, rl.rl)这六种情况,显然不是除以一个2就能解决的了的。

于是我们考虑保存每一种情况的具体选法,具体操作时要进行状态压缩,以最大限度节省空间。
合并直接暴力比对就行。接下来我们以样例为例手动模拟一下。
搜索:(为了方便理解,状态用二进制表示,sum给出具体计算)

l.state: 0000 0010 0010 0001 0011 0011 0001 0011 0011
l.sum:  0 2 -2 1 1+2 1-2 -1 -1+2 -1-2
r.state: 0000 1000 1000 0100 1100 1100 0100 1100 1100
r.sum: 0 4 -4 3 3+4 3-4 -3 -3+4 -3-4

排序:

l.state: 0011 0010 0011 0001 0000 0001 0011 0010 0011
l.sum: -1-2 -2 1-2 -1 0 1 -1+2 2 1+2
r.state: 1100 1000 0100 1100 0000 1100 0100 1000 1100
r.sum: 3+4 4 3 -3+4 0 3-4 -3 -4 -3-4

这里注意左集合要从小到大,而右集合要从大到小。这是为了接下来的合并。

合并时我们只需要对左集合情况中的每一项找到对应的右集合中可行的方案即可。
我们用中括号[]表示左指针(lp)和右指针(rp)的位置。

初始:

l.state: [0011] 0010 0011 0001 0000 0001 0011 0010 0011
l.sum: [-1-2] -2 1-2 -1 0 1 -1+2 2 1+2
r.state: [1100] 1000 0100 1100 0000 1100 0100 1000 1100
r.sum: [3+4] 4 3 -3+4 0 3-4 -3 -4 -3-4

移动(rp)直到(l.sum[lp]+r.sum[rp]==0)
此时在(book)中记录下方案(l.state[lp]|r.state[rp]),中间是位或运算。

l.state: [0011] 0010 0011 0001 0000 0001 0011 0010 0011
l.sum: [-1-2] -2 1-2 -1 0 1 -1+2 2 1+2
r.state: 1100 1000 [0100] 1100 0000 1100 0100 1000 1100
r.sum: 3+4 4 [3] -3+4 0 3-4 -3 -4 -3-4
book: 0111
ans: 1

此时(lp)增加,因为我们给两个数组排过序,所以显然不需要往回移动(rp)
同样移动(rp),但这次没有匹配到。

l.state: 0011 [0010] 0011 0001 0000 0001 0011 0010 0011
l.sum: -1-2 [-2] 1-2 -1 0 1 -1+2 2 1+2
r.state: 1100 1000 0100 [1100] 0000 1100 0100 1000 1100
r.sum: 3+4 4 3 [-3+4] 0 3-4 -3 -4 -3-4
book: 0111
ans: 1

继续让(lp)自增,成功匹配上。以下同理。

l.state: 0011 0010 [0011] 0001 0000 0001 0011 0010 0011
l.sum: -1-2 -2 [1-2] -1 0 1 -1+2 2 1+2
r.state: 1100 1000 0100 [1100] 0000 1100 0100 1000 1100
r.sum: 3+4 4 3 [-3+4] 0 3-4 -3 -4 -3-4
book: 0111 1111
ans: 2
l.state: 0011 0010 0011 [0001] 0000 0001 0011 0010 0011
l.sum: -1-2 -2 1-2 [-1] 0 1 -1+2 2 1+2
r.state: 1100 1000 0100 [1100] 0000 1100 0100 1000 1100
r.sum: 3+4 4 3 [-3+4] 0 3-4 -3 -4 -3-4
book: 0111 1111 1101
ans: 3

这一部比较特别,出现了0的情况。不过问题不大,最后把这种情况减掉就行了。

l.state: 0011 0010 0011 0001 [0000] 0001 0011 0010 0011
l.sum: -1-2 -2 1-2 -1 [0] 1 -1+2 2 1+2
r.state: 1100 1000 0100 1100 [0000] 1100 0100 1000 1100
r.sum: 3+4 4 3 -3+4 [0] 3-4 -3 -4 -3-4
book: 0111 1111 1101 0000
ans: 4

方案1101重复,不计入答案。

l.state: 0011 0010 0011 0001 0000 [0001] 0011 0010 0011
l.sum: -1-2 -2 1-2 -1 0 [1] -1+2 2 1+2
r.state: 1100 1000 0100 1100 0000 [1100] 0100 1000 1100
r.sum: 3+4 4 3 -3+4 0 [3-4] -3 -4 -3-4
book: 0111 1111 1101 0000
ans: 4
l.state: 0011 0010 0011 0001 0000 0001 [0011] 0010 0011
l.sum: -1-2 -2 1-2 -1 0 1 [-1+2] 2 1+2
r.state: 1100 1000 0100 1100 0000 1100 [0100] 1000 1100
r.sum: 3+4 4 3 -3+4 0 3-4 [-3] -4 -3-4
book: 0111 1111 1101 0000
ans: 4
l.state: 0011 0010 0011 0001 0000 0001 0011 [0010] 0011
l.sum: -1-2 -2 1-2 -1 0 1 -1+2 [2] 1+2
r.state: 1100 1000 0100 1100 0000 1100 [0100] 1000 1100
r.sum: 3+4 4 3 -3+4 0 3-4 [-3] -4 -3-4
book: 0111 1111 1101 0000
ans: 4

方案0111重复。

l.state: 0011 0010 0011 0001 0000 0001 0011 0010 [0011]
l.sum: -1-2 -2 1-2 -1 0 1 -1+2 2 [1+2]
r.state: 1100 1000 0100 1100 0000 1100 [0100] 1000 1100
r.sum: 3+4 4 3 -3+4 0 3-4 [-3] -4 -3-4
book: 0111 1111 1101 0000
ans: 4

模拟完成。输出的时候别忘了减去1。
当然这次模拟中还有一些小细节没有涉及到,在接下来的代码中将会进行讲解。

code:

#include <iostream>
#include <cstdio>
#include <cstring>
#include <algorithm>
#include <cmath>
#define int long long//不开long long见祖宗
using namespace std;
int n,ans,lastpos,m[30],lcnt,rcnt,lp,rp;//lastpos的意义在下面会有解释
bool book[1<<21];
struct node{int now,sum;}l[1<<21],r[1<<21];//为方便排序,把这两个放到结构体里
bool lcmp(node xx,node yy){return xx.sum<yy.sum;}//左集合升序
bool rcmp(node xx,node yy){return xx.sum>yy.sum;}//右集合降序
void ldfs(int ll,int rr,int now,int sum)
{
    if(ll>rr)
    {
        lcnt++;
        l[lcnt].now=now;
        l[lcnt].sum=sum;//记录答案,没什么好说的
        return;
    }
    ldfs(ll+1,rr,now,sum);//不选
    ldfs(ll+1,rr,now+(1<<(ll-1)),sum+m[ll]);//放到左集合里,注意状态压缩中的位运算操作
    ldfs(ll+1,rr,now+(1<<(ll-1)),sum-m[ll]);//放到右集合里
}
void rdfs(int ll,int rr,int now,int sum)//同上
{
    if(ll>rr)
    {
        rcnt++;
        r[rcnt].now=now;
        r[rcnt].sum=sum;
        return;
    }
    rdfs(ll+1,rr,now,sum);
    rdfs(ll+1,rr,now+(1<<(ll-1)),sum+m[ll]);
    rdfs(ll+1,rr,now+(1<<(ll-1)),sum-m[ll]);
}
signed main()
{
    scanf("%lld",&n);
    for(int i=1;i<=n;i++)scanf("%lld",&m[i]);
    ldfs(1,n/2,0,0);rdfs(n/2+1,n,0,0);
    sort(l+1,l+lcnt+1,lcmp);sort(r+1,r+rcnt+1,rcmp);
    lp=rp=1;//一定要做好初始化!
    while(lp<=lcnt&&rp<=rcnt)
    {
        while(l[lp].sum+r[rp].sum>0&&rp<=rcnt)rp++;//向右移动rp进行匹配
        lastpos=rp;//这里就是上述模拟中没有提到的细节
                       //实际上可能右集合中可能会有不同的state对应相同的sum,因此我们需要记录一下第一个可以匹配的
                       //如果不记录的话,此时左集合再来一个不同的state对应相同的sum,你就会漏情况
        while(l[lp].sum+r[rp].sum==0&&rp<=rcnt)//左右的sum匹配上了
        {
            if(book[l[lp].now|r[rp].now]==0)//去重,情况相同的不能记第二遍
            {
                book[l[lp].now|r[rp].now]=1;
                ans++;
            }
            rp++;//看看还有没有不同的state对应相同的sum
        }
        if(l[lp].sum==l[lp+1].sum)rp=lastpos;//左集合也有不同的state对应相同的sum,这时候需要将右指针挪回lastpos,重新进行统计以免漏情况
        lp++;//这个lp匹配完了,换下一个
    }
    printf("%lld
",ans-1);//别忘了减全是0的情况
    return 0;
}
原文地址:https://www.cnblogs.com/pjykk/p/15369832.html