算法設計與分析---第3章 遞歸與分治策略

分治法的設計思想是,將一個難以直接解決的大問題,分割成一些規模較小的相同問題,以便各個擊破,分而治之。 如果原問題可分割成k個子問題(1<k≤n),且這些子問題都可解,並可利用這些子問題的解求出原問題的解,那麼這種分治法就是可行的。 由分治法產生的子問題往往是原問題的較小模式,這就爲使用遞歸技術提供了方便。
3.1 遞歸算法
程序直接或間接調用自身的編程技巧稱爲遞歸算法(Recursion)。
遞歸需要有邊界條件、遞歸前進段和遞歸返回段。

  • 當邊界條件不滿足時,遞歸前進;
  • 當邊界條件滿足時,遞歸返回。
  • 注意:在使用遞增歸策略時,必須有一個明確的遞歸結束條件,稱爲遞歸出口,否則將無限進行下去(死鎖)。

遞歸的缺點:

  • 遞歸算法解題的運行效率較低。
  • 在遞歸調用過程中,系統爲每一層的返回點、局部變量等開闢了堆棧來存儲。遞歸次數過多容易造成堆棧溢出等。

3.1.1 Fibonacci數列
在這裏插入圖片描述
算法3.1 Fibonacci數列的遞歸算法

int fib(int n) 
{  
	if (n<=1) 
		return 1;  
	return fib(n-1)+fib(n-2); 
}

該算法的效率非常低,因爲重複遞歸的次數太多。
算法3.2 Fibonacci數列的遞推算法

int  fib[50];
void fibonacci(int n) 
{  
	fib[0] = 1;   
	fib[1] = 1;
	for (int i=2; i<=n; i++)   
		fib[i] = fib[i-1]+fib[i-2]; 
}

3.1.2 集合的全排列問題
在這裏插入圖片描述
算法3.3 全排列問題的遞歸

void Perm(int list[], int k, int m) 
{
	if(k==m)  
	{   
		for(int i=0;i<=m;i++)    
			cout<<list[i]<<" ";   
		cout<<endl;
	}  
	else
		for(int j=k;j<=m;j++)   
		{
			swap(list[k],list[j]);    
			Perm(list,k+1,m);    
			swap(list[k],list[j]);   
		} 
}

3.1.3 整數劃分問題
整數劃分問題是算法中的一個經典命題之一。把一個正整數n表示成一系列正整數之和: n=n1+n2+…+nk(其中,n1>=n2>=…>=nk>=1,k>=1)
正整數n的這種表示稱爲正整數n的劃分。正整數n的不同劃分個數稱爲正整數n的劃分數,記作p(n) 。
正整數6有如下11種不同的劃分,所以p(6)=11。
6
5+1
4+2, 4+1+1
3+3, 3+2+1, 3+1+1+1
2+2+2, 2+2+1+1, 2+1+1+1+1
1+1+1+1+1+1
算法分析
在這裏插入圖片描述
算法3.4 正整數n的劃分算法

int split(int n,int m) 
{  
	if(n==1||m==1) 
		return 1;  
	else if (n<m) 
		return split(n,n);
	else if(n==m) 
		return split(n,n-1)+1;  
	else 
		return split(n,m-1)+split(n-m,m); 
}

3.2 分治策略
分治策略是對於一個規模爲n的問題,若該問題可以容易地解決(比如說規模n較小)則直接解決,否則將其分解爲k個規模較小的子問題,這些子問題互相獨立且與原問題形式相同。
遞歸地解這些子問題,然後將各子問題的解合併得到原問題的解。
3.2.1 分治法的基本步驟
分治法在每一層遞歸上都有三個步驟:

  1. 分解:將原問題分解爲若干個規模較小,相互獨立,與原問題形式相同的子問題;
  2. 解決:若子問題規模較小而容易被解決則直接解,否則遞歸地解各個子問題;
  3. 合併:將各個子問題的解合併爲原問題的解。

算法3.5 分治策略的算法設計模式

Divide_and_Conquer(P) 
{  
	if (|P|<=n0 ) 
		return adhoc(P);  
	divide P into smaller substances P1,P2,…,Pk;
	for (i=1; i<=k; k++)    
		yi=Divide-and-Conquer(Pi)  
	Return merge(y1,y2,…,yk)      
}

在用分治法設計算法時,最好使子問題的規模大致相同。如分成大小相等的k個子問題,許多問題可以取k=2。 這種使子問題規模大致相等的做法是出自一種平衡(Balancing)子問題的思想,它幾乎總是比子問題規模不等的做法要好。
3.2.2 分治法的適用條件
分治法所能解決的問題一般具有以下幾個特徵:

  1. 該問題的規模縮小到一定的程度就可以容易地解決;
  2. 該問題可以分解爲若干個規模較小的相同問題,即該問題具有最優子結構性質;
  3. 利用該問題分解出的子問題的解可以合併爲該問題的解;
  4. 該問題所分解出的各個子問題是相互獨立的,即子問題之間不包含公共的子子問題。

3.2.4循環賽日程表
問題描述:設有n=2k個運動員要進行網球循環賽。
現要設計一個滿足以下要求的比賽日程表:

  1. 每個選手必須與其他n-1個選手各賽一次;
  2. 每個選手一天只能參賽一次;
  3. 循環賽在n-1天內結束。

請按此要求將比賽日程表設計成有n行和n-1列的一個表。 在表中的第i行,第j列處填入第i個選手在第j天所遇到的選手,其中1≤i≤n,1≤j≤n-1。

void Table(int k) 
{   
	int i, r;  
	int n = 1 << k; 
	for (i=0; i<n; i++)   
		a[0][i] = i + 1;
	for (r=1; r<n; r<<=1)   
		for (i=0; i<n; i+=2*r)   
		{     
			Copy(r, r + i, 0, i, r);  
			Copy(r, i, 0, r + i, r);
		} 
}

實現方陣的拷貝

//源方陣的左上角頂點座標(fromx, fromy),行列數爲r 
//目標方陣的左上角頂點座標(tox, toy),行列數爲r 
void Copy(int tox, int toy, int fromx, int fromy, int r) 
{
	for (int i=0; i<r; i++)   
		for (int j=0; j<r; j++)      
			a[tox+i][toy+j] = a[fromx+i][fromy+j]; 
}

3.2.6 選擇問題
對於給定的n個元素的數組a[0:n—1],要求從中找出第k小的元素。
輸入
輸入有多組測試例。
對每一個測試例有2行,第一行是整數n和k(1≤k<n≤1000),第二行是n個整數。
輸出
第k小的元素。
利用快速排序算法的思想,來解決選擇問題。
記一趟快速排序後,分解出左子集中元素個數爲 nleft,則選擇問題可能是以下幾種情況之一:

  1. nleft =k﹣1,則分界數據就是選擇問題的答案。
  2. nleft >k﹣1,則選擇問題的答案繼續在左子集中找,問題規模變小了。
  3. nleft <k﹣1,則選擇問題的答案繼續在右子集中找,問題變爲選擇第k-nleft-1 小的數,問題的規模變小了。

算法3.9 採用分治策略找出第k小元素的算法

int select(int left,int right,int k)
{
	if(left>=right)
		return a[left];
	int i=left;
	int j=right+1;
	int pivot=a[left];
	while(true)
	{
		do{
			i=i+1;
		}while(a[i]<pivot);
		do{
			j=j-1;
		}while(a[j]>pivot);
		if(i>=j)
			break;
		swap(a[i],a[j]);
	}
		if(j-left+1==k)
			return pivot;
		a[left]=a[j];
		a[j]=pivot;
		if(j-left+1<k)
			return select(j+1,right,k-j+left-1);
		else
			return select(left,j-1,k);
}

3.2.7輸油管道問題
某石油公司計劃建造一條由東向西的主輸油管道。該管道要穿過一個有n口油井的油田。從每口油井都要有一條輸油管道沿最短路經(或南或北)與主管道相連。
如果給定n口油井的位置,即它們的x座標(東西向)和y座標(南北向),應如何確定主管道的最優位置,即使各油井到主管道之間的輸油管道長度總和最小的位置?
給定n口油井的位置,編程計算各油井到主管道之間的輸油管道最小長度總和。
輸入
第1行是一個整數n,表示油井的數量(1≤n≤10 000)。 接下來n行是油井的位置,每行兩個整數x和y (﹣10 000≤x,y≤10 000)。
輸出
各油井到主管道之間的輸油管道最小長度總和。
設n口油井的位置分別爲pi=(xi, yi) ,i=1~n。由於主輸油管道是東西向的,因此可用其主軸線的y座標唯一確定其位置。主管道的最優位置y應該滿足:
在這裏插入圖片描述
由中位數定理可知,y是中位數。
算法1:對數組a排序(一般是升序),取中間的元素

int n;
int x;
int a[1000];
cin>>n; 
for(int k=0;k<n;k++)  
	cin>>x>>a[k];
sort(a,a+n);
int min=0; 
for(int i=0;i<n;i++)  
	min += (int)fabs(a[i]-a[n/2]); 
cout<<min<<endl;

算法2:採用分治策略求中位數

int n; 
int x; 
int a[1000]; 
cin>>n; 
for (int i=0; i<n; i++)  
	cin>>x>>a[i];
int y = select(0, n-1, n/2); 
int min=0; 
for(int i=0;i<n;i++)  
	min += (int)fabs(a[i]-y); 
cout<<min<<endl;

3.2.8 半數集問題
給定一個自然數n,由n開始可以依次產生半數集set(n)中的數如下。
(1) n set(n);
(2) 在n的左邊加上一個自然數,但該自然數不能超過最近添加的數的一半;
(3) 按此規則進行處理,直到不能再添加自然數爲止。
例如,set(6)={6,16,26,126,36,136}。
半數集set(6)中有6個元素。
注意半數集是多重集
對於給定的自然數n,編程計算半數集set(n)中的元素個數。
設set(n)中的元素個數爲f(n) ,則顯然有:
在這裏插入圖片描述
算法3.12 計算半數集問題的遞歸算法

int comp(int n) 
{  
	int ans=1;  
	if (n>1) 
		for(int i=1;i<=n/2;i++)   
			ans+=comp(i);  
	return ans; 
}

算法3.13 計算半數集問題的遞歸算法—記憶式搜索

int a[1001]; 
int comp(int n) 
{  
	int ans=1;  
	if(a[n]>0)
		return a[n]; 
	for(int i=1;i<=n/2;i++)   
		ans+=comp(i);  
	a[n]=ans;
	return ans; 
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章