雙向(折半)搜尋
雙向(折半)搜尋
Part 1:雙向搜尋概念與樸素複雜度分析
雙向搜尋是對於深度優先搜尋的一種優化,它的基本思想是:把\(dfs\)從一端開始改為從兩端開始,從而有效減少搜尋狀態
如果上面的定義不懂的話,請看下面兩這張圖(設\(n\)是搜尋層數):
上面這個圖就是普通的\(dfs\)在每次拓展兩個節點的情況下所產生的的搜尋樹的形狀
這棵搜尋樹,每一個節點都代表了一次遞迴呼叫,我們很容易可以看出來它的複雜度是\(O(2^n)\)(滿二叉樹情況下)(其實減不減1無所謂了)
下面這個是雙向搜尋所產生的兩棵搜尋樹(一顆紅色,一顆藍色)
我們在這兩棵搜尋樹上分別得到了\(4\)和\(2\)種結果,現在組合一下(綠色部分),得到了\(4*2=8\)
那麼分析複雜度:我們一次搜尋的複雜度\(O(2^\frac{n+2}{2})\),兩次搜尋就是\(O(2^{\frac{n+4}{2}})\)
如果我們樸素的統計答案,那麼一次搜尋會產生\(2^{\frac{n}{2}}\)種結果,兩次則平方,變成了\(2^n\)次統計
把複雜度相加:總複雜度是\(O(2^{\frac{n+4}{2}}+2^n)\)
Q:怎麼還慢了呢?混蛋!
A:(沒錯就是慢了)
Q:那你講個P!(握緊小拳頭)
A:(先別打,先別打!我還沒講完呢!)
Part 2:真正的雙向搜尋複雜度分析
上面的方法慢了,主要原因是我們選擇了“樸素”的統計答案
考慮怎麼優化:我們統計答案的第一步就是判斷這個組合得到的最終答案合不合法
那麼,我們可以考慮對其中一個答案陣列排序,然後以另一個答案陣列為查詢元素進行二分查詢
假設找到一個元素,因為在它之前的元素一定小於它,所以在這個元素之前的元素也都合法,這樣我們就不用一個一個的統計答案了,提高了效率
分析這麼做的時間複雜度:
\(sort()\)快速排序一遍\(O(\frac{n}{2}logn)\)
進行\(\frac{n}{2}\)次二分查詢,\(O(\frac{n}{2}logn)\)
統計答案總複雜度:\(O(nlogn)\)
演算法總複雜度:\(O(2^{\frac{n+4}{2}}+nlogn)\)
這不就快了嗎
Part 3:例題
傳送門:https://www.luogu.com.cn/problem/P4799
題目中要求我們求出在不超過總預算的情況下,小B去看比賽的方案數
首先,資料範圍\(n\leq 40\),普通的\(dfs\)肯定會\(TLE\)的飛起,我們考慮雙向搜尋
令\(Mid=\frac{n}{2}\),我們第一次從\(1\)搜尋到\(Mid\),把答案記錄到序列\(a\)中,第二次從\(Mid+1\)搜尋到\(n\)答案記錄到序列\(b\)中
現在統計答案,從\(a\),\(b\)中任選一個序列進行快速排序,(這裡排哪個應該對時間有影響,但是不大),這裡假設我們對\(b\)排序
我們的滿足答案的狀態是:\(a_i+b_j<=m\),所以我們對\(a\)陣列\(for\)一遍,在\(b\)陣列中二分查詢第一個大於\(m-a_i\)的元素
假設第一個大於\(m-a_i\)的元素是\(b_j\),那麼在\(j\)之前的元素一定也符合條件(因為\(b\)序列單調不降),於是\(ans+=j\)
另外,因為我們搜尋時會進行一些特判和剪枝,所以我們搜尋和統計答案的複雜度比上面推出來的式子低,總複雜度也遠遠低於\(O(2^{\frac{n+4}{2}}+nlogn)\)
Code
#include<cstdio>
#include<utility>
#include<algorithm>
typedef long long int ll;
const ll maxn=(1<<20)+10;//產生的最大狀態數是2^20
ll n,m,price[45],ida,idb,a[maxn],b[maxn],ans;//不開long long見祖宗
//n場比賽,m預算,price[i]表示第i場收費,ida為a陣列的迭代器,idb是b陣列的迭代器,ans統計答案
inline ll read(){
ll x=0,fh=1;
char ch=getchar();
while(ch<'0'||ch>'9'){ if(ch=='-') fh=-1;ch=getchar(); }
while('0'<=ch&&ch<='9'){ x=(x<<3)+(x<<1)+ch-'0';ch=getchar(); }
return x*fh;
}//瞎寫的快讀
void dfs(ll num,ll u,ll end,ll turn){
//num表示列舉到第num場比賽,u表示已使用預算,end表示搜到第幾場比賽停止,turn表示這是第幾輪搜尋
if(num==end+1){//如果搜到了end+1場比賽,則已經完成搜尋,記錄答案
if(turn==1){ a[ida]=u;ida++; }//第一輪記錄到a中,迭代器++
else{ b[idb]=u;idb++; }//第二輪記錄到b中迭代器++
return;
}
dfs(num+1,u,end,turn);//第num場比賽不看
if(u+price[num]<=m)//剪枝,如果要看第num場比賽,那麼先判斷錢夠不夠,不夠的話看個寂寞,直接跳過搜尋
dfs(num+1,u+price[num],end,turn);
}
int main(){
n=read(),m=read();
for(ll i=1;i<=n;i++)
price[i]=read();//快速讀入
ll Mid=(n>>1);//取中點Mid
dfs(1,0,Mid,1);//第一次搜尋
dfs(Mid+1,0,n,2);//第二次搜尋
std::sort(b,b+idb);//對b序列排序
for(ll i=0;i<ida;i++){
ll qwq=std::upper_bound(b,b+idb,m-a[i])-b;
//在b中二分查詢第一個大於n-a[i]的元素
ans+=qwq;//答案加上該元素的下標,分析如上
}
printf("%lld\n",ans);//輸出答案
return 0;
}
感謝您的閱讀\(OvO\)