大社課

遞迴 Recursion

佛倫斯

遞迴

一種數學的工具

\(a_n=\begin{cases} 1&n=1 \\ a_{n-1}+1&n>1\end {cases}\)

\(a_2=a_1+1=1+1=2 \\ a_3=a_2+1=2+1=3 \\ a_4=a_3+1=3+1=4 \\ \vdots\)

觀察可以發現\(a_n=n\)

這個遞迴式的特色

剛剛舉的例子:\(a_n=\begin{cases} 1&n=1 \\ a_{n-1}+1&n>1\end {cases}\)

\(a_n\)在\(n=1\)時有給定一個值,\(n\)大於1以後數列的某一項和前一項有關係,一直往前推會發現和數列第一項(也就是\(a_1\))有某種關係

 

是什麼關係?請看下頁

紅框內的式子全部相加,\(a_1,\ a_2,\ \cdots,\ a_{n-1}\)會被消光光,剩下左式\(a_n\)和右式\(n-1\)個\(1\)加上\(a_1\),得到\(a_n=a_1+(n-1)\)

\(a_1=1\)代入得剛剛觀察得來的:\(a_n=n\)

\(a_n=\begin{cases} 1&n=1 \\ a_{n-1}+1&n>1\end {cases}\)

\(a_2=a_1+1\)

\(a_3=a_2+1\)

\(\vdots\)

\(a_n=a_{n-1}+1\)

這個也是遞迴

\(a_n=\begin{cases}1 & n=1 \\ 2a_{n-1} & n\geq 2\end{cases}\)

下面的式子全部相乘:

\(a_2=2a_1\)

\(a_3=2a_2\)

\(\vdots\)

\(a_n=2a_{n-1}\)

得到\(a_n=2^{n-1}\cdot a_1=2^{n-1}\)

再來看一個遞迴的例子

a_n=\begin{cases}1 & n=1 \\ \\ \displaystyle \frac{3}{7} & n=2 \\ \\ \displaystyle \frac{a_{n-2}\cdot a_{n-1}}{2a_{n-2}-a_{n-1}} & n \geq 3\end{cases}

看起來複雜了一些,我們也列幾項看看

\(\displaystyle a_3 = \frac{1\cdot \frac{3}{7}}{2\cdot 1-\frac{3}{7}} = \frac{\frac{3}{7}}{\frac{11}{7}}=\frac{3}{11}\)

 

\(\displaystyle a_4 = \frac{\frac{3}{7}\cdot \frac{3}{11}}{2\cdot \frac{3}{7}-\frac{3}{11}} = \frac{\frac{9}{77}}{\frac{45}{77}}=\frac{1}{5}\)

 

\(\displaystyle a_5 = \frac{\frac{3}{11}\cdot \frac{1}{5}}{2\cdot \frac{3}{11}-\frac{1}{5}} = \frac{\frac{3}{55}}{\frac{19}{55}}=\frac{3}{19}\)

這個遞迴式從第3項以後,每一項都可以用前兩項表示

把\(a_1\)和\(a_4\)換成\(\displaystyle \frac{3}{3},\ \frac{3}{15}\),你發現了什麼?

\(\displaystyle a_1=\frac{3}{3},\ a_2=\frac{3}{7},\ a_3=\frac{3}{11},\ a_4=\frac{3}{15},\  \)

 

\(\displaystyle a_5=\frac{3}{19},\ \cdots,\ a_n=\frac{3}{4n-1}\)

要證明對任意整數\(n\),\(a_n=\displaystyle \frac{3}{4n-1}\)都是對的

就比較複雜了

需要用到一點數學

使用數學歸納法證明\(\displaystyle a_n=\frac{3}{4n-1}\)

確認n=1或2時\(a_n\)是\(\frac{3}{4n-1}\)

並證明當n=k、n=k+1均成立時,n=k+2也會成立

這裡一點也不重要,看看就好

\(n=1\):\(a_1=\frac{3}{4\times 1-1}=1\)

\(n=2\):\(a_2=\frac{3}{4\times 2-1}=\frac{3}{7}\)

\(n=k\):\(a_k=\frac{3}{4k-1}\)

\(n=k+1\):\(a_{k+1}=\frac{3}{4(k+1)-1}=\frac{3}{4k+3}\)

\(n=k+2\):\(a_{k+2}=\frac{a_k\cdot a_{k+1}}{2a_k-a_{k+1}}=\displaystyle \frac{\frac{3}{4k-1}\cdot \frac{3}{4k+3}}{2\cdot \frac{3}{4k-1}-\frac{3}{4k+3}}\)

\(=\displaystyle \frac{\frac{9}{(4k-1)(4k+3)}}{\frac{6(4k+3)-3(4k-1)}{(4k-1)(4k+3)}}=\)\(\frac{9}{12k+21}=\frac{3}{4k+7}=\frac{3}{4(k+2)-1}\)

前面遇到的遞迴式都是可以用數學方法求通式的

 

但是有一堆更複雜的遞迴式

求通式是超越我們能力所及的

所以寫程式遇到有遞迴題

不會要我們用紙筆求通式然後代進去

敢考在數學的要嘛可以求通式,要嘛值代一代很好看出規律或是有循環

遞迴函式

自己呼叫自己的函式

\(a_n=\begin{cases} 1&n=1 \\ a_{n-1}+1&n>1\end {cases}\)

\(a_2=a_1+1=1+1=2 \\ a_3=a_2+1=2+1=3\)

把\(a_n\)當作是一個函式,\(n\)代表輸入的數字(input)

輸入3:\(a_3=a_2+1\)

也就是呼叫自己然後把2輸入進去

輸入2:\(a_2=a_1+1\)

呼叫自己後把1數入進去

輸入1:\(a_1=1\)

\(a_n\)是\(n\)的函數

int a(int n) {
    if (n==1) {
        return 1;
    }
    else if (n>1) {
        return a(n-1)+1;
    }
}

\(a_n=\begin{cases} 1&n=1 \\ a_{n-1}+1&n>1\end {cases}\)

遞迴式如果給你了,那麼就照著遞迴式,把數學式翻譯成程式語言就好了(像上面一句一句翻)

我懶得畫箭頭,希望各位辨色力正常

費波納契數列(Fibonacci sequence

\(a_n = \begin{cases}1&n=1,2\\a_{n-1}+a_{n-2}&n\geq 3\end{cases}\)

數列的第一、二項都是1,第三項開始每一項都是前兩項的和

 

這下不好求\(a_n\)通式了吧~^_^

得靠電腦了

(其實還是可以求通式,但那真的很難,我不會)

一樣,你敢給遞迴式,我敢照著翻譯

\(a_n = \begin{cases}1&n=1,2\\a_{n-1}+a_{n-2}&n\geq 3\end{cases}\)

int a(int n) {
    if (n==1 || n==2)	return 1;
    else if (n>=3)	return a(n-1)+a(n-2);
}

那遇到條件比較奇怪的呢?

ZJ e357 遞迴函數練習

定義一函數\(F(x)\),

\(F(x)=\begin{cases}1 &x=1\\F \left(\frac{x}{2}\right)& 2|x\\F(x-1)+F(x+1)&x=3, 5, 7, \dots\end{cases}\)

輸入一正整數\(x\),輸出\(F(x)\)

範例輸入:6           範例輸出:2

\(F(6)=F(3)=F(4)+F(2)=F(2)+F(1)\)

\(= 1+F(1)=2\)

#include <iostream>
using namespace std;

int F(int x) {
    if (x == 1)     return 1;
    if (x%2 == 0)   return F(x/2);
    return F(x+1)+F(x-1);
}

int main() {
    int x;  cin >> x;
    cout << F(x) << '\n';
}

\(F(x)=\begin{cases}1 &x=1\\F \left(\frac{x}{2}\right)& 2|x\\F(x-1)+F(x+1)&x=3, 5, 7, \dots\end{cases}\)

如果x不是1也不是偶數,那它一定是非1的奇數(x=3, 5, 7, ...),所以最後沒必要再if

其實還是照著題目給的條件和輸出寫

並沒有特別難

最討厭就是題目沒給遞迴式

要你自己列

像是這題

東東爬階梯不是一次走一階,就是一次走兩階。

假設階梯有三階,那他有三種走法

ZJ d212 東東爬階梯

n=1:1種

n=2:2種(1+1或2)

一:第一步走一階,第二步走二階。
二:第一步走二階,第二步走一階。
三:全程都走一階。

這題要問你,假設階梯有\(n\)階(\(0<n<100\)),

那東東有幾種走法?

分析東東最後是走2階還是1階

假設有\(n\)階,有\(a_n\)種走法

\(a_1=1,\ a_2=2\)

若最後走2階,前面一共有\(a_{n-2}\)種走法再爬兩階

若最後走1階,前面一共有\(a_{n-1}\)種走法再爬一階

總走法數:\(a_n=a_{n-2}+a_{n-1}\)

\(\Rightarrow a_n=\begin{cases}1&n=1 \\ 2&n=2 \\ a_{n-2}+a_{n-1}&n\geq 3\end{cases}\)

\(a_n=\begin{cases}1&n=1 \\ 2&n=2 \\ a_{n-2}+a_{n-1}&n\geq 3\end{cases}\)

#include <iostream>
using namespace std;

unsigned long long int fib[100];

void F(int x=1, unsigned long long int a1=1, 
unsigned long long int a2=2) {
    if (x!=100) {
        fib[x] = a1;
	F(x+1, a2, a1+a2);
    }
}

int main() {
    int h; F();
    while (cin >> h) {
	cout << fib[h] << '\n';
    }
} 

還有這題

有一個長度為\(n\)字串只有包含\(A,\ B\)兩種字元,而有\(S(n)\)種\(A,\ B\)的排列方式滿足字串中沒有超過3個連續的\(A\)或超過3個連續的\(B\)

求\(S(2015)\)除以12的餘數。

\(S(1)=2\)

\(S(2)=2^2=4\)

\(S(3)=2^3=8\)(才3而已不會被限制)

但\(S(4)\)開始就要考慮連續\(A,\ B\)的問題了

2015 AMC 12A 第22題

\(AAA\)

\((BBB)\)

 

\(BAA\)

\((ABB)\)

 

\(BBA\)

\(ABA\)

\((BAB)\)

\((AAB)\)

\(+A\) 不行

\(+B\)

\(+B\)

\(+A\)

\(+A\)

\(+B\)

討論字串末端有連續1、2、或3個相同字母

  • 末端有3同後面只能加異(變成末端1同)
  • 末端有2同可以加同(變成末端3同)或是加異(變成末端1同)
  • 末端有1同可以加同(變成末端2同)或是加異(變成末端1同)

假設字串長度為\(n\geq3\)時,

末端1同的排列數為\(a_n\),

末端2同的排列數為\(b_n\),

末端3同的排列數為\(c_n\)

由剛才的討論可以列出遞迴式

\(a_3=4,\ b_3=2,\ c_3=2\)

\(\begin{cases} a_{n+1}=a_n+b_n+c_n \\ b_{n+1}=a_n \\ c_{n+1}=b_n \end{cases}\quad n\geq3\)

而題目所求\(S(n)=a_n+b_n+c_n\)(所有排列狀況數包含末1同、末2同、末3同)

\(x\)只是用來記錄遞迴幾次而已

題目最後問除以12的餘數,遞迴過程中只涉及加法,所以遞迴過程中取餘數和遞迴完再取餘數的結果是一樣的,不如一直取餘數讓數字不要太大

舉個例就知道了:28%12=4、69%12=9

28+69=97,4+9=13,97%12=13%12=1

\(a_3=4,\ b_3=2,\ c_3=2\)

\(\begin{cases} a_{n+1}=a_n+b_n+c_n \\ b_{n+1}=a_n \\ c_{n+1}=b_n \end{cases}\quad n\geq3\)

int S(int x, int a=4, int b=2, int c=2) {
    if (x==3)	return (a+b+c)%12;
    else    return S(x-1, (a+b+c)%12, a%12, b%12);
}

int main() {
    int cnt=0;
    cout << S(2015) << '\n';
}

答案是8

再一個範例:錯排問題

有10張成績單發給10個人,全部發錯的可能數有幾種?

跟前面一樣,從\(D_1\)開始列,再觀察每多一位會發生什麼事

先列幾個

\(D_1=0\)(廢話,只有一個位子,1又不能填那)

\(D_2=1\)(1擺第2位,2擺第1位)

\(D_3=2\)(231、312)

有1~n n個數字,1不排第1位、2不排第2位、⋯⋯、n不排第n位的排法有\(D_n\)種,求\(D_{10}\)

討論\(D_n\)和前面幾項的關係

原本有n-1格,最後面再加一格(第n格),但是n不能在第n格,要跟前面的第i格交換位子

_ _ _ _ ... _ _

若i不在第i格,表示前面n-1格是錯排(有\(D_{n-1}\)種),而i有n-1種選法(可以選第1格、第2格、⋯⋯、第n-1格)

若i在第i格,表示前面n-1格除了第i格以外是錯排(有\(D_{n-2}\)種),而i有n-1種選法(可以選第1格、第2格、⋯⋯、第n-1格)

i在第i格:有\(D_{n-2}\times (n-1)\)種排法

(選擇i有n-1種選法,前面錯排有\(D_{n-2}\)種排法,適用乘法原理)

i不在第i格:有\(D_{n-1}\times (n-1)\)種排法

最後得:\(D_n=(n-1)(D_{n-1}+D_{n-2})\)

(i在第i格的排法和i不在第i格的排法適用加法原理)

(這裡把(n-1)提出來了)

\(D_n=\begin{cases}0&n=1 \\ 1&n=2 \\ (n-1)(D_{n-2}+D_{n-1}) & n\geq 3\end{cases}\)

#include <iostream>
using namespace std;

unsigned long long int D(int n) {
	if (n==1)	return 0;
	if (n==2)	return 1;
	return (D(n-1)+D(n-2))*(n-1);
}

int main() {
	cout << D(10) << '\n';
}

\(D_n=\begin{cases}0&n=1 \\ 1&n=2 \\ (n-1)(D_{n-2}+D_{n-1}) & n\geq 3\end{cases}\)

剛剛的函式寫起來也沒多少行

遞迴關係也不是很複雜

但是要想到那個遞迴關係是最難的部分

這種題目就多練習吧~

題目們

  • ZJ b558:求數列第n項

  • ZJ d881:作業苦多(要用while cin)

  • ZJ d420:00694 - The Collatz Sequence

題解

ZJ b558:求數列第n項

\(a_1 = 1 \\a_2 = a_1+1 \\a_3 = a_2+2 \\ \vdots \\ a_n = a_{n-1}+(n-1)\)

 

\(a_n=\begin{cases}1 & n=1 \\ a_{n-1}+(n-1) & n>1\end{cases}\)

#include <iostream>
using namespace std;

int a(int n) {
    if (n == 1)	return 1;
    else	return a(n-1)+(n-1);
}

int main() {
    int x;
    while (cin >> x) {
        cout << a(x) << '\n';
    }
} 

\(a_n=\begin{cases}1 & n=1 \\ a_{n-1}+(n-1) & n>1\end{cases}\)

ZJ d881:作業苦多

數列每一項成等差,再求數列和

a_1 = 1 \\ a_2 = a_1+1 \\ a_3 = a_2+(1+d) \\ a_4 = a_3+(1+2d) \\ \vdots \\ a_n = a_{n-1}+(1+(n-2)d) \\ \\ a_n = \begin{cases}1 & n=1 \\ 2 & n=2 \\ a_{n-1}+(1+(n-2)d) & n\geq 3\end{cases}

剛剛找到\(a_n\)數列的規則後,再用for迴圈求\(a_1+a_2+\cdots +a_{50}\)即可

#include <iostream>
using namespace std;

int a(int d, int n) {
    if (n==1)	return 1;
    else if (n==2)	return 2;
    return a(d, n-1)+(1+d*(n-2));
}

int main() {
    int d;
    while (cin >> d) {
	int x=0;
	for (int i = 1; i <= 50; i++) {
    	    x += a(d, i);
	}
        cout << x << '\n';
    }
} 

ZJ d420:00694 - The Collatz Sequence

依題敘可列出:\(a_{n+1}= \begin{cases} \displaystyle \frac{a_n}{2} & 2 \mid a_n  \\ \\  3a_n+1 & 2 \nmid a_n \end{cases}\)

從\(a_1\)開始遞迴,遞迴到\(a_i\)滿足

  • \(a_i = 1\)
  • \(a_i > limit\)

其中之一時停止遞迴,並記錄\(i\)

小心!\(a_i\)超過limit的那一項不算

#include <iostream>
#include <cmath>
using namespace std;

int f(int x, int lim, int n=0) {
    if (x==1)	return n+1;
    else if (x>lim)	return n;
    else if (x%2==1) return f(3*x+1, lim, n+1);
    else	return f(x/2, lim, n+1);
}

\(a_{n+1}= \begin{cases} \displaystyle \frac{a_n}{2} & 2 \mid a_n  \\ \\  3a_n+1 & 2 \nmid a_n \end{cases}\)

遞迴終止條件

int main() {
    int A, L, i=1;
    while (true) {
        cin >> A >> L;
        if (A < 0 && L < 0)	break;
        cout << "Case " << i << ": A = " 
        << A << ", limit = " << L 
        << ", number of terms = " 
        << f(A, L) << '\n';
        i++;
    }
}

下面輸出不是很重要,但是一定要看清楚是怎麼輸出的,不然會一直WA

Made with Slides.com