演算法設計與分析——分治法
前言
本文重點回顧了卜老師課堂上關於分治演算法的一些常見的問題。加油吧!ヾ(◍°∇°◍)ノ゙
分治法(Divide and Conquer)
當面對一個問題的時候,我們可能一下子找不到解決問題的方法。此時,我們可以考慮將問題規模最小化,先看看當問題規模變小以後,我們如何去解決;然後逐步擴大問題的規模,看大規模的問題能不能基於小問題的解構造得到。
經過上面的思考以後,我們就可以將原問題一步步地分解為形式一致只是規模較小的問題,直到分解到規模最小化時我們能解決的程度,然後在將這些子問題的解"合併"起來構造出分解前的問題的解。
通常,分治法需要考慮3個問題。
- 如何分解?能不能分解?採取什麼樣的策略將大規模問題分解為小規模問題
- 最簡單的子問題如何求解?
- 如何基於子問題的解,得到原問題的解?
第一個問題,對於能不能分解的問題,我們一般會看問題的輸入是什麼樣的資料結構。一般具有以下資料結構的輸入是很容易分解的:
- 陣列
- 矩陣
- 樹
- 有向無環圖
- 集合
至於如何分解,؏؏☝ᖗ乛◡乛ᖘ☝؏؏,策略也挺多的,我們即可以將問題規模按比例分解,例如一個規模為
,另一個也為
;
也可以將問題規模按一個規模為n-1,一個規模為1的方式分解。
一般,在分解的過程中結合隨機方法,分治法一般會威力巨大,而且問題求解過程相當簡潔。
而至於問題2和問題3,不同問題會有不同策略,我們視具體問題具體分析。
經典問題
下面介紹一些可使用分治法解決的經典問題。
排序問題
排序問題,我們都不會陌生,它是將一組無序的輸入資料變成一組有序的資料。
輸入: 一個長度為n的整數陣列,
輸出:
,且
。
例如:
輸入
需要輸出:
插入排序
剛拿到這個問題,我們可能不會做,那麼我們可以看問題規模最小的時候,我們會不會做。
例如n=2時,輸入陣列只包含2個數字。例如
,那麼我們一眼就可以看出來排序的結果為
。
當n增大的時候,我們看看怎麼搞。
當n=3時,輸入陣列只包含3個數字,
,前面兩個數字是我們剛剛解決已排序好的
。那我們怎麼把新來的8加入這個排好序的陣列呢?
可以有兩種方法,一種是遍歷前面已經排序好的陣列,將8插入到這個排序好的陣列的合適位置。遍歷完5,6後,我們知道8應該在6的後面。
一種是因為前面的陣列已經排序好了,那麼我們可以使用二分查詢,快速找到這個新元素在陣列中的合適位置。二分的話,一次性就可以查到8在6的後面。
查到合適的位置之後,再將這個位置之後的元素都向後挪移一個位置,給這個元素空出那個合適的位置即可。
假設我們已經將前k個元素排序好了:
,且這些元素都是按照增序排列的。那麼對於
,我們只要將
放到合適的地方就可以了。如此反覆,直到k=n即可。期間,合併時候的開銷主要來自:1.尋找這個數的合適位置,2.將元素後移出一個空位置。
插入排序的程式碼:
void insert_sort(int *A, int n)
{
if (n > 2)
{
insert_sort(A, n - 1);//將前n-1個數排序
//將第n個數加入到前n-1個已排序好的數裡面
int i = 0;
int unsorted = A[n - 1];
while (A[i] <= unsorted && i<(n - 1))
{
i++;
}
if (i >= (n - 1))
//說明A[n-1]比前n-1個數都大
{
return;
}
else
//A[i-1]<=A[n-1],A[i]>A[n-1]
{
for (int j = n - 1; j >i; j--)
{
A[j] = A[j - 1];
}
}
A[i] = unsorted;
}
else if (n == 2)
//只包含兩個元素
{
if (A[0] > A[1])
{
int tmp = A[0];
A[0] = A[1];
A[1] = tmp;
}
}
}
演算法複雜度分析:
於是:
歸併排序
歸併排序的思想其實和插入排序主要區別在於:歸併排序每次將問題分解為兩個規模為
的子問題,而不是一個規模為
,另一個規模為
。這樣做的好處是使得子問題的規模可以迅速降低。
程式碼
void merge_sort(int *A, int l, int r)
//[l,r]左閉右閉
{
if (l>=r)
//只包含1一個元素,則不用排序
{
return;
}
int mid = (l + r) / 2;
merge_sort(A, l, mid);//左邊排好序
merge_sort(A, mid+1, r);//右邊排好序
//兩個合併在一起
vector<int> L;
int lp = l;
int rp = mid+1;
while (lp <= mid && rp <= r)
{
if (A[lp] < A[rp])
{
L.push_back(A[lp]);
lp++;
}
else
{
L.push_back(A[rp]);
rp++;
}
}
while (lp <= mid)
{
L.push_back(A[lp++]);
}
while (rp <= r)
{
L.push_back(A[rp++]);
}
for (int i = l; i <= r; i++)
{
A[i] = L[i - l];
}
}
時間複雜度分析