1. 程式人生 > 實用技巧 >雙向(折半)搜尋

雙向(折半)搜尋

雙向(折半)搜尋

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\)