遞歸分析和分治算法


遞歸分析一般利用的方法是主定理,輔助的方法有替換法,遞歸樹方法~

主定理:

遞歸樹:

主定理的證明可以通過遞歸樹的方法進行;

 

主定理適用的范圍比較局限,有些情況不能被包括,這些情況就需要利用遞歸樹的方法了,

主定理的case1是f(n)小於nlogba多項式時間,原定理描述為f(n)=O(nlogba)且ε>0,它與case2中f(n)=Θ(nlogba)中間差一些情況,就是f(n)小於nlogba,但是多余的不是多項式時間;

另外就是case2和case3之間相差的部分,就是f(n)大於nlogba,但是如果不大於多項式時間,就不能滿足主定理了;

另外一種是case3中的f(n)不滿足后面的情況;

舉個例子,如果最近點對中間利用快速排序進行排序,則合並時間nlgn,遞歸公式T(n)=2T(n/2)+nlgn,這種情況介於case2和case3,所以利用遞歸樹:

T(n)=nlgn+n(lgn-lg2)+n(lgn-lg4)+...=nlgnlgn-n(lg2+2lg2+3lg2+...+lgnlg2)=nlgnlgn-nlg2((1+lgn)lgn)/2=nlgnlgn=nlg2n;

不過這里我查到mit給的主定理和算法導論有所不同,涵蓋了上面的情況,如下:

可能這算是一種情況來了;

那么這里我在取一個不滿足主定理的例子~

所以主定理不滿足時就利用決策樹進行帶入吧!如果數學計算能力比較強大還是可以計算出來的,畢竟主定理都是決策樹證明的,數學能力不強表示證明有點困難...

 

不過這里有個偷懶的證明方法,直接假設f(n)是一個nk形式的;

T(n)=aT(n/b)+nk

T(n/b)=aT(n/b2)+(n/b)k

...

所以T(n)=a(aT(n/b2)+(n/b)k)+nk=nk(1+a/bk+...+(a/bk)h)=(nk-nlogba)/(1-a/bk),接下來討論a和bk的關系決定了為nk還是nlogba,上面如果為1則為nklogbn了。

簡單的證明,不過不太准確;

 

替換法舉一個例子如下:

    分治方法算是算法設計中一種很常見的設計方式,一般能夠大大提高算法的時間復雜度的~分治的思想很簡單,就是將一個問題切分為兩個或者多個獨立的子問題,子問題的解決方案同,子問題解決之后通過合並算法組合成更大問題的結果,所以分治算法主要有三個步驟,Divide(切分子問題的方案)、Conquer(一般子問題獨立相同的,所以這里一般是遞歸的解決子問題)、Combine(子問題提升至更大問題的時候需要對子問題的解決方案進行合並)。分治算法還是對待不同的問題需要不同的分治方案,所以掌握縮小問題的規模的思想還是比較重要的,比較高級的動態規划和貪心也都是通過縮小問題規模提升時間復雜度的。所以感覺還是多掌握一些具體實例的分治方案,這樣碰到陌生問題的時候可以像熟悉的問題靠攏,所以接下來具體分析一下算法導論中一些分治的實例。(怎么感覺map-reduce也是這么干的?)

下面的內容:1歸並排序  2二分查找  3斐波那契近似求值  4大數相乘  5矩陣乘法  6最近點對

1、歸並排序

Divide:一個數字序列切分為兩部分,各n/2

Conquer:對切分的兩個子問題分別merge-sort,子問題的性質同父問題,所以可以遞歸調用

Combine:對兩個已經排序好的數組進行合並,兩個數組開始均設置一個指針,然后向后讀取,每次取最小,這樣線性時間就可以對兩個排序好的數組合並成為一個大的數組

實現代碼:

//因為merge算法需要額外的空間,算法執行之前動態開辟,准備的算法
bool PreMergeSort(unsigned int* array,int begin,int end) { unsigned int* arrayAssit = new unsigned int[end - begin + 1]; mergeSort(array,arrayAssit,begin,end); delete [] arrayAssit; return true; }
//merge的主程序部分,首先進行切分,找出中間位置,然后切分成前半部分merge-sort和后半部分merge-sort
bool mergeSort(unsigned int* array,unsigned int* arrayAssit,int begin,int end) { //遞歸結束條件
if (end == begin) { return true; } int mid = (begin+end)/2; mergeSort(array,arrayAssit,begin,mid); mergeSort(array,arrayAssit,mid+1,end); //合並部分
merge(array,arrayAssit,begin,mid,end); return true; } //遞歸之后的合並部分,思想就是兩個指示器指示兩個待合並的數組的起始位置,比較大小,每次輸出小的元素,然后相應指示器++,如果一個輸出完畢,則另外一個全部輸出,輸出的為合並好的數組
bool merge(unsigned int* array,unsigned int* arrayAssit,int begin,int mid,int end) { int i,j,k; i = begin; j = mid + 1; k = begin; //比較輸出小的過程
while(i <= mid&&j<= end) { if (array[i] <= array[j]) { arrayAssit[k++] = array[i++]; } else { arrayAssit[k++] = array[j++]; } } //一個輸出完畢,另外一個全部輸出
while (i <= mid) { arrayAssit[k++] = array[i++]; } while (j <= end) { arrayAssit[k++] = array[j++]; } //臨時數組拷貝至原數組
memcpy(array+begin,arrayAssit+begin,(end-begin+1)*sizeof(array[0])); return true; }

時間復雜度分析:

Divide:  Θ(1)

Conquer:2T(n/2)

Combine:Θ(n)

遞歸式

T(n)=  2T(n/2)+Θ(n)+Θ(1)  n>1

         =      1                                     n=1

根據主定理,T(n)=Θ(nlgn)

2、二分查找

輸入有序數組~

Divide:查找中間位置元素,比對大小

Conquer:根據上面直接把問題划分為左邊查找還是右邊查找~

Combine:這個問題沒有合並的步驟

 

時間復雜度分析:

Divide:  Θ(1)

Conquer:T(n/2)

Combine:Θ(1)

遞歸式

T(n)=  T(n/2)+Θ(1)+Θ(1) 

根據主定理,T(n)=Θ(lgn)

3、斐波那契數列

斐波那契數列的分治算法基於以下一個近似算法和以下一個矩陣計算方法:

分治的思想是就是一個求pow的分治,比如pow(a,n)可以分治為pow(a,n/2)*pow(a,n/2)

Divide:Θ(1)

Conquer:T(n/2)

Combine:Θ(1)乘法

由主定理:T(n)=Θ(lgn)

代碼實現:

 

//左邊遞歸
double leftn(int n) { if (n == 1) { return (1+sqrt((double)5))/2; } if (n>=2&&n%2!=0) { double tmp = leftn((n-1)/2); return tmp*tmp*(1+sqrt((double)5))/2; } if (n>=2&&n%2==0) { double tmp = leftn(n/2); return tmp*tmp; } } //右邊遞歸
double rightn(int n) { if (n == 1) { return (1-sqrt((double)5))/2; } if (n%2!=0) { double tmp = rightn((n-1)/2); return tmp*tmp*(1-sqrt((double)5))/2; } if (n%2==0) { double tmp = rightn(n/2); return tmp*tmp; } } //近似求值
double FibonaccinClose(int n) { return ((leftn(n)-rightn(n))/sqrt((double)5)); }

  

 

遞歸的相似同上,矩陣的pow分治

代碼實現:

Rect FibonacciRect(int n)
{
	if (n == 0)
	{
		//  1 1
		//  1 0
		rect.x01 = 0;
		rect.x02 = 0;
		rect.x11 = 0;
		rect.x12 = 0;
		return rect;
	}
	if (n == 1)
	{
		//  1 1
		//  1 0
		rect.x01 = 1;
		rect.x02 = 1;
		rect.x11 = 1;
		rect.x12 = 0;
		return rect;
	}
	//偶數
	if (n >= 2 && n%2 == 0)
	{
		tmpRect1 = FibonacciRect(n/2);
		rect.x01 = tmpRect1.x01*tmpRect1.x01+tmpRect1.x02*tmpRect1.x11;
		rect.x02 = tmpRect1.x01*tmpRect1.x02+tmpRect1.x02*tmpRect1.x12;
		rect.x11 = tmpRect1.x11*tmpRect1.x01+tmpRect1.x12*tmpRect1.x11;
		rect.x12 = tmpRect1.x11*tmpRect1.x02+tmpRect1.x12*tmpRect1.x12;
		
		return rect;
	}
	//奇數
	if (n > 2 && n%2 == 1)
	{
		tmpRect1 = FibonacciRect((n-1)/2);
		tmpRect2.x01 = tmpRect1.x01*tmpRect1.x01+tmpRect1.x02*tmpRect1.x11;
		tmpRect2.x02 = tmpRect1.x01*tmpRect1.x02+tmpRect1.x02*tmpRect1.x12;
		tmpRect2.x11 = tmpRect1.x11*tmpRect1.x01+tmpRect1.x12*tmpRect1.x11;
		tmpRect2.x12 = tmpRect1.x11*tmpRect1.x02+tmpRect1.x12*tmpRect1.x12;

		rect.x01 = tmpRect2.x01 + tmpRect2.x02;
		rect.x02 = tmpRect2.x01;
		rect.x11 = tmpRect2.x11 + tmpRect2.x12;
		rect.x12 = tmpRect2.x11;

		return rect;
	}
}

時間復雜度:

Divide:Θ(1)

Conquer:T(n/2)

Combine:Θ(1)乘法

由主定理:T(n)=Θ(lgn)

 4、分治法求大整數相乘

c=a*b=(a1*10n/2+a0)*(b1*10n/2+b0)=(a1*b1)10n+(a1*b0+b1*a0)10n/2+b0*a0

 = c0*10n+c1*10n/2+c2

如果c1利用上面的(a1*b0+b1*a0)進行計算,

算法分析如下:

Divide:Θ(1)

Conquer:4T(n/2)

Combine:Θ(1)乘法

由主定理:T(n)=Θ(n2

這里時間復雜度沒有減少,具體原因是由於乘法的次數沒有減少,所以c1修改為如下計算:(a1+a0)*(b1+b0)-(c0+c2)

這樣分析就如下

Divide:Θ(1)

Conquer:3T(n/2)

Combine:Θ(1)乘法

由主定理:T(n)=Θ(n1.585

5、矩陣乘法

矩陣的乘法的分治和上面的很相似,也是乘法數量的減少,普通的矩陣分治:

T(n)=8T(n/2)+Θ(n2)

主定理T(n)=Θ(n3),沒有時間復雜度的提高

這樣減少乘法的數量:

T(n)=7T(n/2)+Θ(n2)

主定理T(n)=Θ(n2.81),

6、最近點對

最近點對的分治思想比較容易得到,就是從中部分開,然后分別求兩邊最短的點對,但是這里的難點在於合並,合並的時候可能出現跨越兩個區域的最近點對,合並的時間復雜度如果選擇不當,則會導致整個算法的時間復雜度的提高;

遞歸結束條件為3,3個點的時候暴力求解即可;

T(n)=2T(n/2)+?,問號及為合並的時間,如果算法想要達到nlgn的時間復雜度,則這里的?必須為線性時間的!注意,這個是重點~

合並的時候首先考慮到只需要考慮以下帶狀區域:這里的d為兩邊求取的最小的距離,中間合並只可能出現比這個距離更小的,所以要在帶狀區域尋找

但是如果每個點暴力解決帶狀區域考慮最壞情況可能是n2的時間復雜度,所以不行;

所以考慮以下其中一個點,看看其需要遍歷哪些點,看看是否能夠減少一個點遍歷的點的數量,上面是遍歷所有帶狀區域的點,這里我們發現可以根據某個點划分一個區域:

針對左邊的某個點,右邊划分出的矩形中最多存在6個點與其對應,所以每次只需要遍歷6個點即可,這6個點是y坐標距離左邊某個點最近的6個點;

 

對於左邊范圍內的p點,最多只有6個點。這個可以反推證明,如果右邊這2個正方形內有7個點與p點距離小於δ,例如q點,則q點與下面正方形的四個頂點距離小於δ,則和δSLSR的最小點對距離相矛盾。因此對於左邊的p點,不需求出p點和右邊虛線框內所有點距離,只需計算SR與p點y坐標距離最近的6個點,就可以求出最近點對,節省了比較次數。線性合並時間的關鍵保持一個按照y排序的數組,利用預處理和歸並的思想~

代碼如下:

 

//原理就是歸並排序的合並,
void merge(point y[], point m[], int begin, int end, int mid)
{
	int i, j, k;
	for (i = begin, j = mid + 1, k = begin; i <= mid && j <= end;)
	{
		//左邊開始的為i,右邊開始的為j,i從begin開始,j從mid+1開始
		//然后開始比較i和j的關系,如果j小,則把j移動到i這邊同時j++
		//m保存着已經排序好的
		if (m[i].y > m[j].y)
		{
			y[k++] = m[j];
			j++;
		}
		else
		{
			y[k++] = m[i];
			i++;
		}
	}
	while (i <= mid)
		y[k++] = m[i++];
	while (j <= end)
		y[k++] = m[j++];
	//將排序好的m拷貝到y中
	memcpy(m + begin, y + begin, (end - begin + 1) *sizeof(y[0]));
}

double closepair(point x[],point y[],point m[],int begin,int end,point& px,point& py)
{
	//一個點直接返回0
	if (end-begin==0)
	{
		return 0;
	}
	//小於等於3個點就不遞歸了,直接窮舉計算
	if (end-begin<=2)
	{
		return enumpair(x,begin,end,px,py);
	}
	//取分治的點
	int mid = (begin + end)/2;
	int i,j,k;
	double dl,dr,dm;
	//這里開始把按照y坐標排序的y數組分成左邊按照y排序和右邊按照y排序
	//點的標識這里用到了
	for (i=begin,j=begin,k=mid+1;i<=end;i++)
	{
		if (y[i].index<=mid)
		{
			m[j++]=y[i];
		}
		else
		{
			m[k++]=y[i];
		}
	}
	//遞歸
	dl = closepair(x,m,y,begin,mid,px,py);
	dr = closepair(x,m,y,mid+1,end,px,py);

	dm = min(dl,dr);

	//將上面分的y合並
	merge(y,m,begin,end,mid);

	//找出帶狀Y'按照y排序的那部分
	for (i=begin,k=begin;i<=end;i++)
	{
		if (fabs(y[i].x-x[mid].x)<dm)
		{
			m[k++]=y[i];
		}
	}
	//然后遍歷帶狀Y’中最短的進行
	//合並
	for (i=begin;i<k;i++)
	{
		for (j=i+1;j<k&&m[j].y - m[i].y < dm;j++)
		{
			double tmp = compudis(m[i],m[j]);
			if (tmp<(dm<shortest?dm:shortest))
			{
				//記錄最小距離以及最近點對
				shortest=tmp;
				px.x = m[i].x;
				px.y = m[i].y;
				py.x = m[j].x;
				py.y = m[j].y;
			}
		}
	}
	return shortest;
}

上面算法中尋找6個點的時候排序后又線性的查找了距離的6個點,所以沒有利用上面已經證明的,但是還是保持了線性的性質。可以直接在排序數組中利用6個點的性質。

最近點對的合並算法比較復雜,一般比較難的分治問題的難點就在於合並的難處,它最終也限制了算法的時間復雜度,所以重點理解下最近點對。

T(n)=2T(n/2)+Θ(n)

所以T(n)=nlgn  !

 

有錯誤請指正~轉載請注明出處,謝謝.

 

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM