雙向(折半)搜索

雙向(折半)搜索

Part 1:雙向搜索概念與樸素複雜度分析

雙向搜索是對於深度優先搜索的一種優化,它的基本思想是:把\(dfs\)從一端開始改成從兩端開始,從而有效減小搜索狀態算法

若是上面的定義不懂的話,請看下面兩這張圖(設\(n\)是搜索層數):數組

圖片爆炸了OvO

上面這個圖就是普通的\(dfs\)在每次拓展兩個節點的狀況下所產生的的搜索樹的形狀優化

這棵搜索樹,每個節點都表明了一次遞歸調用,咱們很容易能夠看出來它的複雜度是\(O(2^n)\)(滿二叉樹狀況下)(其實減不減1無所謂了)spa

圖片爆炸了OvO

下面這個是雙向搜索所產生的兩棵搜索樹(一顆紅色,一顆藍色)code

咱們在這兩棵搜索樹上分別獲得了\(4\)\(2\)種結果,如今組合一下(綠色部分),獲得了\(4*2=8\)種結果,和原來的搜索結果同樣blog

那麼分析複雜度:咱們一次搜索的複雜度\(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:怎麼還慢了呢?混蛋!get

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

相關文章
相關標籤/搜索