大社課
遞迴 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}\)
再來看一個遞迴的例子
看起來複雜了一些,我們也列幾項看看
\(\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_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
大社課
By princetonbxxxh
大社課
- 266