1. 程式人生 > >演算法設計與分析——分治法

演算法設計與分析——分治法

前言

本文重點回顧了卜老師課堂上關於分治演算法的一些常見的問題。加油吧!ヾ(◍°∇°◍)ノ゙

分治法(Divide and Conquer)

當面對一個問題的時候,我們可能一下子找不到解決問題的方法。此時,我們可以考慮將問題規模最小化,先看看當問題規模變小以後,我們如何去解決;然後逐步擴大問題的規模,看大規模的問題能不能基於小問題的解構造得到。

經過上面的思考以後,我們就可以將原問題一步步地分解為形式一致只是規模較小的問題,直到分解到規模最小化時我們能解決的程度,然後在將這些子問題的解"合併"起來構造出分解前的問題的解。

通常,分治法需要考慮3個問題。

  1. 如何分解?能不能分解?採取什麼樣的策略將大規模問題分解為小規模問題
  2. 最簡單的子問題如何求解?
  3. 如何基於子問題的解,得到原問題的解?

第一個問題,對於能不能分解的問題,我們一般會看問題的輸入是什麼樣的資料結構。一般具有以下資料結構的輸入是很容易分解的:

  • 陣列
  • 矩陣
  • 有向無環圖
  • 集合

至於如何分解,؏؏☝ᖗ乛◡乛ᖘ☝؏؏,策略也挺多的,我們即可以將問題規模按比例分解,例如一個規模為 1 2 n

\frac{1}{2}n ,另一個也為 1 2 n \frac{1}{2}n
也可以將問題規模按一個規模為n-1,一個規模為1的方式分解。
一般,在分解的過程中結合隨機方法,分治法一般會威力巨大,而且問題求解過程相當簡潔。

而至於問題2和問題3,不同問題會有不同策略,我們視具體問題具體分析。

經典問題

下面介紹一些可使用分治法解決的經典問題。

排序問題

排序問題,我們都不會陌生,它是將一組無序的輸入資料變成一組有序的資料。
輸入: 一個長度為n的整數陣列, A [ 0 , 1 , 2 , , n 1 ] A[0,1,2,\dots,n-1]
輸出: A [ 0 , 1 , 2 , 3 , n 1 ] A[0,1,2,3\dots,n-1] ,且 A [ i ] < A [ j ] , i < j A[i]< A[j],任意的i<j
例如:
輸入 A = { 5 , 6 , 8 , 1 , 3 , 4 , 9 , 7 , 2 } A=\{5,6,8,1,3,4,9,7,2\}
需要輸出: A = { 1 , 2 , 3 , 4 , 5 , 6 , 7 , 8 , 9 } A=\{1,2,3,4,5,6,7,8,9\}

插入排序

剛拿到這個問題,我們可能不會做,那麼我們可以看問題規模最小的時候,我們會不會做。
例如n=2時,輸入陣列只包含2個數字。例如 A 2 = { 5 , 6 } A^{2}=\{5,6\} ,那麼我們一眼就可以看出來排序的結果為 { 5 , 6 } \{5,6\}
當n增大的時候,我們看看怎麼搞。
當n=3時,輸入陣列只包含3個數字, A 3 = { 5 , 6 , 8 } A^{3}=\{5,6,8\} ,前面兩個數字是我們剛剛解決已排序好的 A 2 = { 5 , 6 } A^{2}=\{5,6\} 。那我們怎麼把新來的8加入這個排好序的陣列呢?
可以有兩種方法,一種是遍歷前面已經排序好的陣列,將8插入到這個排序好的陣列的合適位置。遍歷完5,6後,我們知道8應該在6的後面。

一種是因為前面的陣列已經排序好了,那麼我們可以使用二分查詢,快速找到這個新元素在陣列中的合適位置。二分的話,一次性就可以查到8在6的後面。
查到合適的位置之後,再將這個位置之後的元素都向後挪移一個位置,給這個元素空出那個合適的位置即可。
假設我們已經將前k個元素排序好了: A k = { a 1 , a 2 , a 3 , a 4 , , a k } A^{k}=\{a_{1},a_{2},a_{3},a_{4},\dots,a_{k}\} ,且這些元素都是按照增序排列的。那麼對於 A k + 1 A^{k+1} ,我們只要將 a k + 1 a_{k+1} 放到合適的地方就可以了。如此反覆,直到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;
		}
	}
}

演算法複雜度分析:
T ( n ) = T ( n 1 ) + c n T(n)=T(n-1)+cn
於是: T ( n ) = O ( n 2 ) T(n)=O(n^{2})

歸併排序

歸併排序的思想其實和插入排序主要區別在於:歸併排序每次將問題分解為兩個規模為 1 2 n \frac{1}{2}n 的子問題,而不是一個規模為 n 1 n-1 ,另一個規模為 1 1 。這樣做的好處是使得子問題的規模可以迅速降低。
程式碼

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];
	}
}

時間複雜度分析
T ( n ) = 2 T ( 1 2 n ) + c n T(n)=2T(\frac{1}{2}n)+cn
T ( n ) = O ( n l o g n ) T(n)=O(nlogn)

快速排序

計算逆序對

選擇第k小的數

乘法問題

矩陣乘法

最近點對

其他問題