遞歸問題整理

不敢說是總結,就是把自己看到的一些遞歸相關題目整理一下,並按照自己的理解歸下類~

  • 單路遞歸(一個遞歸過程中只有一個遞歸入口)
  • 多路遞歸(一個遞歸過程中有多個入口)
  • 間接遞歸(函數可通過其他函數間接調用自己)
  • 迭代遞歸(每次遞歸調用都包含一次循環遞歸)
  • 下面一一整理,注意許多題目都有更優解法,如DP,但是暫不討論。

先說說解遞歸的一般思路吧,把原問題分解爲更小的子問題,再從子問題裏慢慢尋找原問題的解。實際上遞歸是一種思路,解題時首先列出遞歸表達式,然後用程序語言的方式把他表現出來。往往遞歸都可轉化爲循環或者模擬調用棧來實現,但是遞歸表達更利於理解。

一,單路遞歸(遞歸鏈)

1,求n的階乘(經典實例)

int factorial(int n)
{
   if (n == 0) //基線條件(base case)
   {
      return 1;
   }
   else
   {
      return n * factorial(n - 1); //將問題規模逐漸縮小,或者說轉化爲更小更簡單的子問題
   }
}

2,堆結構的維護(可參考數據結構相關書籍)

上面的例題是比較簡單的單路遞歸,類似的還有遞歸遍歷目錄下所有文件,下面介紹複雜一點的,不同的遞歸調用在不同的條件判斷語句裏,這也是單路遞歸:)

3,迷宮問題(實際上屬於深度優先搜索的範疇)

通用解法:

bool FindPathThroughMaze( Maze maze, Point position ){
     //if the position has already been tried, don't try it again
     if( AlreadyTried( maze, position ) ) {
         return false;
     }
     
     //if this position is the exit, declare success
     if( ThisIsTheExit( maze, position ) ) {
         return true;
     }
     
     //Remember that this position has been tried
     RememberPosition( maze, position );
     
     //check the path to the left, up, down, and to the right; if 
     //any path is successful, stop looking
     
     if( MoveLeft( maze, position, &newPosition ) ) {
         if( FindPathThroughMaze( maze, newPosition ) ) {
             return true;
         }
     }
     
     if( MoveUp( maze, position, &newPosition ) ) {
         if( FindPathThroughMaze( maze, newPosition ) ) {
             return true;
         }
     }
     
     if( MoveDown( maze, position, &newPosition ) ) {
         if( FindPathThroughMaze( maze, newPosition ) ) {
             return true;
         }
     }
     
     if( MoveRight( maze, position, &newPosition ) ) {
         if( FindPathThroughMaze( maze, newPosition ) ) {
             return true;
         }
     }
     
     //can't find the exit.
     return false;
}
4,POJ 2756 http://poj.grids.cn/problem?id=2756
思路:
  • 這個題目要求樹上任意兩個節點的最近公共子節點。分析這棵樹的結構不難看出,不論奇數偶數,每個數對2 做整數除法,就走到它的上層結點。
  • 我們可以每次讓較大的一個數(也就是在樹上位於較低層次的節點)向上走一個結點,直到兩個結點相遇。
  • 如果兩個節點位於同一層,並且它們不相等,可以讓其中任何一個先往上走,然後另一個再往上走,直到它們相遇。
  • 設common(x, y)表示整數x 和y的最近公共子節點,那麼,根據比較x 和y 的值,我們得到三種情況:
  • (1) x 與y 相等,則common(x, y)等於x 並且等於y
  • (2) x 大於y,則common(x, y)等於common(x/2, y)
  • (3) x 大於y,則common(x, y)等於common(x y/2)

所以程序如下:

#include int common(int x, int y)
{
    if(x == y)
        return x;
    if(x > y)
        return common(x / 2, y);
    return
        common(x, y / 2);
}
int main(void)
{
    int m, n;
    scanf("%d%d", &m, &n);
    printf("%d/n", common(m, n));
    return 0;
}
5,二分查找(經典例子,可查閱數據結構相關書籍)

二,多路遞歸(遞歸樹)

1,樹的前中後序遍歷(經典例子,也形象地反映了多路遞歸過程的樹狀結構)

2,合併排序,快速排序(都是將問題化解我兩個更小的子問題去解決,也是樹狀結構,各節點是動態構造的)

3,放蘋果(POJ 1664)

問題描述:

把M個同樣的蘋果放在N個同樣的盤子裏,允許有的盤子空着不放,問共有多少種不同的分法? 5,1,1和1,5,1 是同一種分法。(1<=M,N<=10)

如輸入M = 7, N = 3,應輸出8

(7 0 0 )(6 1 0 )(5 2 0 )(5 1 1 )(4 3 0 )(4 2 1 )(3 3 1 )(3 2 2)

思路分析:

  • 所有不同的擺放方法可以分爲兩類:至少有一個盤子爲空和所有盤子都不空。對於至少空着一個盤子的情況,則N 個盤子擺放M 個蘋果的擺放方法數目與N-1 個盤子擺放M 個蘋果的擺放方法數目相同。對於所有盤子都不空的情況,則N 個盤子擺放M 個蘋果的擺放方法數目等於N 個盤子擺放M-N 個蘋果的擺放方法數目。我們可以據此來用遞歸的方法求解這個問題。
  • 設f(m, n) 爲m 個蘋果,n 個盤子的放法數目,則先對n 作討論,如果n>m,必定有n-m 個盤子永遠空着,去掉它們對擺放蘋果方法數目不產生影響;即if(n>m) f(m,n) =f(m,m)。當n <= m 時,不同的放法可以分成兩類:即有至少一個盤子空着或者所有盤子都有蘋果,前一種情況相當於f(m , n) = f(m , n-1); 後一種情況可以從每個盤子中拿掉一個蘋果,不影響不同放法的數目,即f(m , n) = f(m-n , n)。總的放蘋果的放法數目等於兩者的和,即 f(m,n) =f(m,n-1)+f(m-n,n) 。整個遞歸過程描述如下:
int ways(int m, int n)
{
	if (m == 0 || n == 1) //base case
		return 1;
	else if (m < n)
		return ways(m , m); 
	else
		return ways(m, n - 1) + ways(m - n, n); //轉化爲更小更簡單的子問題
}

4,斐波那契數(遞歸方法是很搓的,勿用)

int Fib( int n ){
    if( n <  2 )
        return;
    else return Fib(n-2) + Fib(n-1);
}
5,最大子段和(最優解法是DP)
求a[1:n]的最大子段和可分解爲a[1:n/2], a[n/2+1,n]的最大子段和情況,有如下3種情況

a[1:n]的最大子段和與a[1:n/2]的最大子段相等;

a[1:n]的最大子段和與a[n/2+1,n]的最大子段相等;

a[1:n]的最大子段和經過a[1:n/2]與a[n/2+1,n],這種情況最大子段和等於兩個最大子段和相加;

代碼:

int MaxSubSum(int *a, int left, int right)
{
    int sum = 0;
    if(left == right)
        sum = a[left] > 0 ? a[left] : 0;
    else
    {
        int center = (left + right) / 2;
        int leftsum = MaxSubSum(a, left, center);
        int rightsum = MaxSubSum(a, center+1, right);
        
        int s1 = 0;
        int lefts = 0;
        for(int i = center; i >= left; i--)
        {
                lefts += a[i];
                if(lefts > s1)
                    s1 = lefts;
        }
        
        int s2 = 0;
        int rights = 0;
        for(int i = center + 1; i <= right; i++)
        {
                rights += a[i];
                if(rights > s2)
                    s2 = lefts;
        }
        
        sum = s1 + s2;
        
        sum = (sum < leftsum ? leftsum : sum);
        sum = (sum < rightsum ? rightsum : sum);
    }
    return sum;
}

三,間接遞歸(類似遞歸鏈)

1,遞歸下降解釋器(解釋器用的比較多,以後我會寫篇相關文章)
原理就是exp1()->exp2()->exp3()->………..->exp1(),其中按優先級,越後面的表達式優先級越高,然後遞歸又從exp1()調用。

 

四,迭代遞歸(類似圖的深度優先遍歷)

1,圖的深度優先遍歷

通用深度優先搜索框架:

void dfs(int deep, State curState)
{
if (deep > Max) //深度達到極限
{ 	
    if (curState == target) //找到目標
     {
//...
    }
}
else
 {
for (i = 1; i <= totalExpandMethod; i++)
        {
	   dfs(deep + 1, expandMethod(curState, i));
        }
 }
}
2,廣義水仙花數
問題描述:一個三位數abc如果滿足abc = a^3 + b^3 + c^3 那麼就把這個數叫做水仙花數。

如果一個N位數所有數碼的N次方的和加起來等於這個數字本身,我們把這樣的數叫做廣義水仙花數,容易看出來水仙花數是N = 3的廣義水仙花數現在,我們的任務是,輸入一個m (m < 7) ,讓你求出所有滿足N = m的廣義水仙花數。

3 (153 370 371 407)

5 (54748 92727 93084)

方法:數據規模很小,可以直接枚舉所有情況,然後判斷是否滿足條件。

難點:循環層數不確定

於是我們現在的問題是,怎麼實現這個m重循環?

答案是:遞歸。

完整代碼:

#include 
#include 
using namespace std;

int m;

int Pow(int x, int n)
{
	int res = 1;
	while (n--) res *= x;
	return res;
}

void dfs(int deep, int curNum, int curSum)
{
	if (deep > m) //類似於base case
	{
		if (curNum == curSum)
			printf("%d/n", curNum);
	}
	else if (deep <= m)
	{
		int start = (deep == 1); //第一位不爲0
		for (int i = start; i <= 9; i++)
			dfs(deep + 1, curNum * 10 + i, curSum + Pow(i, m)); //縮小問題規模
	}
}

int main()
{
	
	while (scanf("%d", &m), m)
	{
		dfs(1, 0, 0);
	}
	return 0;
}

3,全排列(可網上查閱。。)
 
 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章