淺談折半搜索


折半搜索(又稱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\)
問這個人有多少種看比賽的方案(一場不看也算做一種,兩種方案不同當且僅當兩種方案所看的比賽中有至少一場不同)
數據范圍:\(n\leqslant 40,\ m\leqslant 10^{18}\)

m的范圍過大,考慮搜索。但是枚舉每場比賽看或者不看的方案數高達\(2^{40}=1099511627776\approx 10^{12}\),顯然不能通過本題。
於是我們就要使用折半搜索的思想。將比賽分為\(l\)\(r\)兩部分,分別算出兩部分的所有可能的花費的錢數。
這時情況總數只不過有\(2*2^{20}=2097152\approx 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\n",ans);
    return 0;
}

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

簡明題意:求從\(n\)個數(\(m_i\))任意選出一些數,使這些數可以分為和相等的兩部分的方案數。
數據范圍:\(2\leqslant n\leqslant 20,\ 1\leqslant m_i\leqslant 10^8\)

這道題比剛剛的題目更加復雜了一些,因為現在每個數有三種狀態,可以不選,可以放到左邊集合,也可以放到右邊集合。
\(3**20=3486784401\approx 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\n",ans-1);//別忘了減全是0的情況
    return 0;
}


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM