先來看乙個經典的二分查詢例子。
int binarysearch(vector& nums, int target)
return -1;
}
時間複雜度是\(o(logn)\)。
我們看到,二分查詢貫徹了分治的思想。當我們要解決乙個輸入規模較大(不妨設為\(n\))的問題時,可以將這個問題分解成\(k\)個不同的子集,如果能得到\(k\)個不同的、可以獨立求解的子問題,而且在求出解之後還可以使用適當的方法將他們的解合併成整個問題的解,這樣原問題得以解決,這種將整個問題分解成若干小問題處理的方法稱為分治法。
一般來說:
分解出的子問題應與原問題有相同的結構(便於使用遞迴實現)。
如果分解出的子問題仍然很大,可以對子問題繼續使用分治法,直到子問題不用分解就可以直接求解。
一般情況下,\(k=2\)是乙個常見的思路。
下面是分治法一般的偽**:
dico(p, q)
}
本節中我們主要分析一些使用分治演算法的例子。
問題描述:求乙個n元陣列中的最大值和最小值
輸入:[2,4,1,6,4,3,2,-1]
輸出:[-1,6]
乙個最基本的思路是這樣的:
vectormaxmin(vector& nums)
; int fmax = nums[0], fmin = nums[0];
for(int i = 1 ; i < nums.size() ; ++i)
return ;
}
不難得到,在最好、最壞情況下的比較次數均為\(2(n-1)\),平均比較次數也是\(2(n-1)\)。
因為\(nums[i]\)不可能既大於\(fmax\),又小於\(fmin\),所以可以做出如下修改:
vectormaxmin(vector& nums)
; int fmax = nums[0], fmin = nums[0];
for(int i = 1 ; i < nums.size() ; ++i)
return ;
}
這樣,最好、最壞情況的比較次數分別是\(n-1\),\(2(n-1)\),平均比較次數是\(3(n-1)/2\)。這裡的最好情況是陣列遞增,最壞情況是陣列遞減。
好像看到現在跟分治沒什麼關係qaq。那麼這個問題用分治的思想如何解決呢?不難理解,求\(n\)元陣列的最大最小值,可以轉換為分別求兩個\(n/2\)陣列的最大最小值,然後在得出的兩個最小值裡取最小、最大值裡取最大,即可求出整個陣列的最大最小值,我們可以將子問題分解為大小為1或2,這樣可以直接求出最大最小值。
經過上面的分析,不難寫出**:
vectormaxmin(vector& nums, int start, int end)
else if(start == end - 1)
else
}else
return ;
}
我們可以分析出比較次數,設\(t(n)\)表示比較次數,那麼遞推關係式為:
\[t(n) = \left\
0,n = 1\\
1,n = 2\\
t(\left\lfloor \right\rfloor ) + t(\left\lceil \right\rceil ) + 2,n > 2
\end \right.\]
不妨令\(n=2^k\),則有:
\[\begin
t(n) = 2t(n/2) + 2\\
t(n) = }t(2) + \sum\limits_^ } \\
t(n) = } + 2(} - 1) = 3n/2 - 2
\end\]
無論是哪種情況,比較次數均為\(3n/2-2\),實際上,任何一種以元素比較為基礎的最大最小演算法,其比較次數下界為\(t(n) = \left\lceil \right\rceil - 2\),所以分治的最大最小演算法是最優的。但是需要\(\left\lfloor \right\rfloor + 1\)層遞迴,需要占用較多的記憶體空間,同時元素出入棧也會帶來時間開銷,所以分治求最大最小值未必比直接求最大最小值效率高。舉這個例子主要是解釋分治的思想。
任何一種以比較為基礎的搜尋演算法,其最壞情況用時不可能低於\(\theta(logn)\)。不存在最壞情況下時間比二分查詢數量級還低的演算法。因為二分查詢產生的二叉搜尋樹使得比較樹的深度最低,所以二分查詢是搜尋問題在最壞情況下的最好方法。
這裡的最壞情況是指搜尋邊界值的情況,而且這裡的陣列是排好序的公升序陣列。
我們先看乙個永遠不會單獨使用,但是卻經常被拿出來批判一番的插入排序hhh。插入排序的思想是將乙個數字插入當前的有序陣列中,使得插入後的陣列依舊有序。**如下:
void insort(vector& nums)
}nums[j+1] = temp;
}}
最壞的情況下是排乙個逆序的陣列,所以時間複雜度為\(\theta(n^2)\)。我們不難發現,大部分時間都花費在移動元素上,而且同乙個元素在排序過程中被挪動不止一次。
那麼,怎樣用分治的思路解決這個問題呢?一種思路是將要排序的陣列分成兩個子陣列,分別對這兩個子陣列排序,再將這兩個排好序的子陣列合併起來,可以寫出如下的**:
vectornums;
vectortemp(nums.begin(), nums.end());
void mergesort(int start, int end)
void merge(int start, int end)
else
++k;
}if(i > mid)
else if(j > end)
for(int p = start ; p <= end ; ++p)
}
類似地,可以得到時間複雜度是\(o(nlogn)\)。
同時有如下結論:\(\theta(nlogn)\)是以比較為基礎的排序演算法最壞情況下的時間下界。
可見從時間複雜度來看,歸併排序是時間複雜度最低的排序演算法。但是歸併排序還存在一些問題:
歸併排序一直分解到乙個元素,實際上當元素較少時,直接排序要比歸併排序花費的時間少,因為歸併排序不可避免的要對元素進行拆分合併。
歸併排序中會借用乙個臨時陣列\(temp\)儲存排序後的結果,我們應該使用一種其他的方法,避免陣列\(nums\)中的頻繁換位。
針對第一點,我們規定乙個歸併開始的規模,即當資料規模大於乙個數時,才進行歸併排序;針對第二點,我們引入乙個索引陣列\(link\),其中\(link[i]\)表示第\(i\)個數後面的那個數的下標。
我們可以寫出修改後的mergesort和merge,用mergesortl和mergel表示:
void mergel(int *lhead, int *rhead, int *head)
else
while(i != -1 && j != -1)
else
}if(i == -1) link[k] = j;
else link[k] = i;
}void mergesortl(int start, int end, int *head)
for(int i = start ; i < end ; link[i] = i + 1, ++i);
link[end] = -1;
*head = start;
}else
}int main()
使用索引陣列之後,元素移動的開銷得到了大幅減少,同時規定了最小歸併規模(在上文中規定為5),有效減少了小陣列歸併帶來的出入棧開銷。
老生常談了(背板子使人快樂hhh)
void quicksort(vector& nums, int start, int end)
swap(nums[j], nums[start]);
quicksort(nums, start, j-1);
quicksort(nums, j+1, end);
}
順便用這個分割方法就可以解決第\(k\)小問題(leetcode傳送門)
當然,我們會發現,最壞的情況下(原始陣列不增排列),時間複雜度為\(o(n^2)\),對於快排和第\(k\)小問題,都可以通過精心選擇分界元素\(pivot\),來降低時間複雜度,這裡不做過多的介紹,有興趣的朋友可以自行查閱資料。
終於到了緊張刺激的coding環節hhh,leetcode中單獨有一類分治演算法的習題,各位朋友可以拿來練習練習。
水平有限,倉促成文,不當之處,尚祈教正。
演算法課複習 分治
hdu 5178 pairs 傳送門 題意 n個數,問有多少對數差值小於k。思路 排個序,開個佇列。每次乙個新的數先把比它小的都從佇列中去了,然後答案加上佇列大小,最後把自己塞進佇列。按順序走一遍行了。ac include include include include include includ...
演算法設計與分析複習 分治法演算法描述
分治 劃分 解子問題 組合 每個遞迴演算法均可以轉換為迭代演算法 include include 尋找最大最小元素,最大比較次數 3 n 2 2 minmax low,high if high low 1 if arr low arr high return arr low arr high els...
演算法 分治演算法
分治策略主要利用遞迴來解決問題,它包括以下三個步驟 分解 將問題分解為一與原問題類似並且比原問題規模更小的子問題 解決 當分解的子問題足夠小時,直接給出答案,否則用遞迴打方式求解 合併 將子問題的解合成原問題的解 下面考慮乙個簡單的利用分治演算法的歸併排序的例子 問題的形式化描述如下 輸入 a是 乙...