Recursion

Arvin Liu

一個方便找投影片的目錄

Syllabus - 0

內容 快速連結
What is Recursion? 什麼是遞迴呢? Section 1
Recursion Tree遞迴樹想像遞迴的過程 Section 2
Ex O - 遞迴觀念題 Section 3
Ex I - 遞迴觀念題練習 如何將數學遞迴程式遞迴? Section 4
Recursion Tips 遞迴要注意的事項 Section 5
Ex II - 彩帶問題 從排列組合到遞迴 Section 6
Memorization - 記憶化 一個讓遞迴更快的重要技巧 Section 7
Euclidean Algorithm - 輾轉相除法 Section 8
Exercise III - Rhythm Doctor Section 9
零和問題 嘗試用遞迴暴搜索有可能! Section 10
Pruning - 剪枝一個大家都會,但很重要的技巧 Section 11
Talks - 結語 Section 12

重要章節
主要章節

這個投影片怎麼用呢?

Before Before Class - 0

  • 上下左右切換投影片,用ESC可以看到整個投影片的大綱。
    • 每個直排都是一個主題,右上角有編號,你可按右上角的             回第一頁。
    • 正確觀看順序:一直往下,到底往右。

Esc後大概長這樣,你可以先Esc在上下左右選你要看哪頁投影片。

What is Recursion?

什麼是遞迴呢?

先讓我們看段影片...

What is Recursion ? - 0

我們從這段影片學到了甚麼?

What is Recursion ? - 1

  • 遞迴定義:每個人遇到問題的時候,都會跑去問他的上級
    • 店員遇到解決不了的問題,就該去問經理
    • 經理遇到解決不了的問題,就該去問老闆
    • ​....
  • 基本情況 (​Base case):God能夠解決一切。
  • 維基上的定義:

    遞迴(英語:Recursion),又譯為遞歸,在數學電腦科學中,是指在函數的定義中使用函數自身的方法。遞迴一詞還較常用於描述以自相似方法重複事物的過程。例如,當兩面鏡子相互之間近似平行時,鏡中巢狀的圖像是以無限遞迴的形式出現的。也可以理解為自我複製的過程。

什麼是遞迴呢?

What is Recursion ? - 2

Recursion

  • 維基上的定義:

    遞迴(英語:Recursion),又譯為遞歸,在數學電腦科學中,是指在函數的定義中使用函數自身的方法。遞迴一詞還較常用於描述以自相似方法重複事物的過程。例如,當兩面鏡子相互之間近似平行時,鏡中巢狀的圖像是以無限遞迴的形式出現的。也可以理解為自我複製的過程。

  • 「為了理解遞迴,則必須首先理解遞迴。」
     
  • 舉個例子,下列為某人祖先的遞迴定義:
    • 某人的雙親是他的祖先(基本情況)。
    • 某人祖先的雙親同樣是某人的祖先(遞迴步驟)。

一個經典的例子 - 斐波那契數列

What is Recursion ? - 4

Fibonacci Sequence

\begin{cases} F_1 = 1 \\ F_2 = 1 \\ F_n = F_{n-1} + F_{n-2} \\ \end{cases}
f(n)= \begin{cases} 1, \text{if \,n=1\,or \,2}\\ f(n-1)+f(n-2), \text{otherwise} \end{cases}
  • 寫成這樣也OK (在數學上)。

*otherwise: 否則的意思

  • 常見的表達式長這樣:

這怎麼看呢?

F_4 = F_3 + F_2
F_3 = F_2 + F_1
= 1 + 1 = 2
= 2 + 1 = 3

一個經典的例子 - 斐波那契數列

What is Recursion ? - 5

Fibonacci Sequence

f(n)= \begin{cases} 1, \text{if \,n=1\,or \,2}\\ f(n-1)+f(n-2), \text{otherwise} \end{cases}
  • 寫成這樣也OK (在數學上)。

*otherwise: 否則的意思

int f(int n){
  if(n == 1 || n == 2)
    return 1;
  else 
    return f(n-1) + f(n-2);
}
  • 程式就當作你在寫一般的函式,照著寫就可以了。

一個經典的例子 - C(n, m)

What is Recursion ? - 6

C(n, m)= \begin{cases} 1, \text{if \,m=0\,or n=m}\\ C(n-1, m-1)+C(n-1, m), \text{otherwise} \end{cases}
  • 什麼是C呢? C(n, m) 就是從n個相異東西取出m個的取法總數。
  • 舉例來說,如果從下面4顆球取出2顆球出來,總共有6種取法。
  • 那麼我們 C(4, 2) 就會等於6。
    C(n, m)其實是有遞迴公式的,長的像下面這樣。

有4顆相異的球

一個經典的例子 - C(n, m)

What is Recursion ? - 7

C(n, m)= \begin{cases} 1, \text{if \,m=0\,or n=m}\\ C(n-1, m-1)+C(n-1, m), \text{otherwise} \end{cases}
  • 那麼我們 C(4, 2) 就會等於6。
    C(n, m)其實是有遞迴公式的,長的像下面這樣。
  • 為甚麼呢?
    • C(n, m): 從n顆球取m顆的取法
      • 如果我取了第一顆球,那麼總數會是 "從n-1顆球取m-1顆",C(n-1, m-1)。
      • 如果我不取第一顆球,那麼總數會是 "從n-1顆球取m顆",C(n-1, m)。
    • 這樣你懂為什麼了嗎?

一個經典的例子 - C(n, m)

What is Recursion ? - 8

  • 那麼我們 C(4, 2) 就會等於6。
    C(n, m)其實是有遞迴公式的,長的像下面這樣。
  • 如同剛剛的例子一樣,轉成程式直接照著寫就好了。
int C(int n, int m){
  if(n == 0 || n == m)
    return 1;
  else 
    return C(n-1, m-1) + C(n-1, m);
}
C(n, m)= \begin{cases} 1, \text{if \,m=0\,or n=m}\\ C(n-1, m-1)+C(n-1, m), \text{otherwise} \end{cases}

Recursion Tree

遞迴樹 - 遞迴要怎麼想像呢?

怎麼分析遞迴呢?

What is Recursion Tree ? - 0

Recursion

  • 你大致上可以把遞迴看成是一個分支圖,舉例來說,我們呼叫了f(5):
f(n)= \begin{cases} 1, \text{if \,n=1\,or \,2}\\ f(n-1)+f(n-2), \text{otherwise} \end{cases}
f(5)= \begin{cases} f(4) = \begin{cases} f(3) = \begin{cases}f(2) = 1\\+\\f(1) = 1 \end{cases}\\ +\\f(2) = 1\end{cases}\\ + \\ f(3) = \begin{cases} f(2) = 1 \\ f(1) = 1\end{cases}\end{cases}

1

2

3

4

5

6

7

8

9

  • 紫色數字是呼叫的順序
    • 如果有兩個f,函式會先做完第一個再去做第二個。
  • 所以答案為5

怎麼分析遞迴呢?

What is Recursion Tree ? - 1

Recursion

  • 試試看畫出C(4, 2)的分析圖吧!
C(n, m)= \begin{cases} 1, \text{if \,m=0\,or n=m}\\ C(n-1, m-1)+C(n-1, m), \text{otherwise} \end{cases}
C(4, 2)= \begin{cases} C(3, 1) = \begin{cases} C(2, 0) = 1 \\ + \\ C(2, 1) = \begin{cases} C(1, 0) = 1\\ + \\ C(1, 1) = 1 \end{cases} \end{cases} \\ + \\ C(3, 2) = \begin{cases} C(2, 1) = \begin{cases} C(1, 0) = 1\\ + \\ C(1, 1) = 1\\ \end{cases} \\ + \\ C(2, 2) = 1 \end{cases} \end{cases}
  • 所以答案為6
  • 你會發現一件事情,在分析的時候,有些遞迴會重複,這個時候用之前算過的值就好了。
  • 例如C(2, 1)有算過兩次,第二次直接看第一次算的值(=2)就好了。

為甚麼要叫遞迴呢?

What is Recursion Tree ? - 2

Tree

樹根

樹葉

  • 起先,你以為樹是往上長的。
  • 但在程式 / 資料結構裡面,樹是往下長的。
  • 遞迴的分支圖,其實就是一棵樹,我們可以叫它遞迴

樹葉

樹根

Exercise O

APCS的手寫遞迴題

Exercise O

APCS 手寫練習題

接下來有三題APCS手寫題。

  • 在寫之前請各位拿出一張紙方便計算。
  • 每一題只能寫2分鐘
  • 這裡沒有給選項。

APCS 106/03 觀念題 - 21

Exercise O - APCS遞迴手寫題1 - 0

int F (int x, int y) {
  if (x<1)
    return 1;
  else
    return F(x-y, y) + F(x-2*y, y);
}

21. 若以 F(5,2) 呼叫下方 F() 函式,執行完畢後回傳值為何?

F(5, 2) = F(3, 2) + F(1, 2)

F(3, 2) = F(1, 2) + F(-1, 2)

F(1, 2) = F(-1, 2) + F(-3, 2)

= 1 + 1 = 2

= 2 + 1 = 3

= 3 + 2 = 5 

→ 答案 =  5 

2 mins

APCS 105/03 觀念題 - 24

Exercise O - APCS遞迴手寫題2 - 1

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

24. 若以 g(4) 呼叫 g() 函式,執行完畢後回傳值為何?

int g (int n) {
  int j = 0;
  for (int i=1; i<=n-1; i=i+1) {
    j = j + f(i);
  }
  return j;
}
  1. g(4)答案會等於多少呢?
    = f(1) + f(2) + f(3)。
  2. 那麼f(1)又是多少呢?
    好像不太容易看出來...

2 mins

APCS 105/03 觀念題 - 24

Exercise O - APCS遞迴手寫題2 - 2

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

24. 若以 g(4) 呼叫 g() 函式,執行完畢後回傳值為何?

int g (int n) {
  int j = 0;
  for (int i=1; i<=n-1; i=i+1) {
    j = j + f(i);
  }
  return j;
}
  1. g(4)答案會等於多少呢?
    = f(1) + f(2) + f(3)。
  2. 那麼f(1)又是多少呢?
    好像不太容易看出來...
  3. f(1) = 1 + f(2)
  4. f(2) = 3 + f(3)
  5. f(3) = 1 + f(4)
  6. f(4) = 1
  7. 答案 = 13

= 6

= 5

= 2

= 6 + 5 + 2

2 mins

APCS 107/12 觀念題 - 03

Exercise O - APCS遞迴手寫題3 - 3

int K (int a[], int n) {
  if (n >= 0)
    return (K(a, n-1) + a[n]);
  else
    return 0;
}

int G (int n) {
  int a[] = {5, 4, 3, 2, 1};
  return K(a, n);
}

3. 給定右側 G(), K() 兩函式,執行 G(3) 後所回傳的值為何?

2 mins

  1. G(3)會呼叫K(a, 3)。
  2. K(a, 3) = K(a, 2) + a[3]
  3. K(a, 2) = K(a, 1) + a[2]
  4. K(a, 1) = K(a, 0) + a[1]
  5. K(a, 0) = K(a, -1) + a[0]
  6. K(a, -1) = 0
  7. 答案 = a[0] + a[1] + a[2] + a[3] = 14

Exercise I

警報器 - 簡單的遞迴練習

Exercise I

程式練習題

接下來有一題請你用
程式實做遞迴的題目。

  • 我會花1分鐘讀題。
  • 總共計時10分鐘
  • 開好你的程式環境吧!

226 - 警報器 - 題目

Exercise I - 警報器 - 0

  • 題目:請問警報器長鳴為一次需3秒短鳴一次需1秒每格鳴聲之間停2秒,若鳴聲時間為t秒 (開頭跟結尾必須是鳴聲,不能是間隔的2秒),
    那麼請問有多少種信號?
    (t為1到100的正整數。)
f(t)= \begin{cases} 1, \text{if \,} t = 1\text{, or }t = 3\\ 0, \text{if \,} t \le 0\\ f(t-5)+f(t-3), \text{otherwise} \end{cases}
  • 小小提示,這題答案的遞迴式長這樣,你可以實作出來嗎?

10 mins

  • t = 6總共有兩種可能。

3s

2s

1s

1s

2s

3s

226 - 警報器 - 解答

Exercise I - 警報器 - 1

f(t)= \begin{cases} 1, \text{if \,} t = 1\text{, or }t = 3\\ 0, \text{if \,} t \le 0\\ f(t-5)+f(t-3), \text{otherwise} \end{cases}
  • 小小提示,這題答案的遞迴式長這樣,你可以實作出來嗎?

10 mins

int f(int t) {
  if (t == 1 || t == 3)
    return 1;
  else if (t <= 0)
    return 0;
  else
    return f(t-5) + f(t-3);
}
  • 轉成程式的話就是這樣。使用直接輸出 f(t)就可以了

226 - 警報器 - 引導

Exercise I - 警報器 - 2

f(t)= \begin{cases} 1, \text{if \,} t = 1\text{, or }t = 3\\ 0, \text{if \,} t \le 0\\ f(t-5)+f(t-3), \text{otherwise} \end{cases}
  • 為甚麼題目可以寫成這樣的遞迴呢?

10 mins

  • 在所有長度為t的鳴聲中其實可分為兩類,把這兩種可能加起來就可以了。
    • 最後一段是長鳴
    • 最後一段是短鳴

可能數 = 長度為t-5的鳴聲種數。

可能數 = 長度為t-3的鳴聲種數。

2s

1s

2s

3s

t-5 s的組合鳴聲

t-3 s的組合鳴聲

t s的組合鳴聲

Recursion Tips

遞迴要注意的事情

遞迴的題目要怎麼思考呢?

Recursion Tips - 遞迴要注意的事情 - 0

  • 想辦法把大問題拆成小問題!
    • 你可能要先定義問題本身,再想辦法達成這個問題的答案 (?)
    • 思考的時候,
      • 不要從一開始往上推,例如n=0怎麼樣,n=1怎麼樣...
      • 通常情況下,要逆向思考,例如n要怎麼用n-1表示,或者n-2表示...
    • 舉例來說,思考長度為t的鳴聲怎麼做時,我們要逆向思考
      • 想想長度為t的鳴聲怎麼用t-?的鳴聲來的得到。
      • 而不是思考t=1要怎麼往上推到長度為t。
    • 記的要思考基礎 / 終止條件!
      • 不然遞迴會無止盡的跑下去,最終吃TLERE

記得要寫base case!

Recursion Tips - 遞迴要注意的事情 - 1

小心無窮遞迴!

Recursion Tips - 遞迴要注意的事情 - 2

Exercise II

彩帶問題 - 從遞迴到排列組合

Exercise II

程式練習題

接下來有一題請你用程式實做遞迴的應用題。

  • 我會花1分鐘讀題。
  • 總共計時15分鐘
  • 想一下前面教了什麼。
  • 至少第一組測資要可以AC

227 - 彩帶問題 - 題目

Exercise II - 彩帶問題 - 0

  • 題目:現在Peipei要請你做出一個漂亮的彩帶,每一段你可以選擇三種顏色,分別是紅,橘,藍三種。至於何為漂亮呢?漂亮的彩帶有以下的限制:
    1. 不能有連續的紅色。
    2. 橘色後面一定要接藍色。
    • 你可以告訴我們如果彩帶的長度為n (n介於1到50到之間的整數),
      有幾種可能的漂亮的彩帶呢? (Hint:參數不只一個n。)
  • 如果題目輸入的n為2,答案會是6種。

15 mins

227 - 彩帶問題 - 引導

Exercise II - 彩帶問題 - 1

  • 我們試著以這樣的思維想想看:
    • 在所有長度為n漂亮彩帶的可能中,可以分成以下3個case。
      • 最後一段為紅色的漂亮彩帶。
      • 最後一段為橘色的漂亮彩帶。
      • 最後一段的藍色的漂亮彩帶。
    • 那麼在這3個case,分別會有幾種可能呢?
      • 我們以最後一段為紅色做舉例
        • 數量會等於長度為n-1最後一段為藍色的漂亮彩帶的可能數。

15 mins

?

?

?

...

n-1

?

?

?

...

n-1

?

?

?

...

227 - 彩帶問題 - 引導

Exercise II - 彩帶問題 - 2

  • 如果我們將長度為n,顏色為color定義成 rec(n, color)。
    • rec(n, RED) = rec(n-1, BLUE)。
    • rec(n, ORANGE) rec(n, BLUE) 呢?
    • rec(n, ORANGE) = rec(n-1, RED) + rec(n-1, BLUE)
    • rec(n, BLUE) = rec(n-1, RED) + rec(n-1, BLUE) + rec(n-1, ORANGE)
  • ​終止條件呢?
    • rec(1, 不管甚麼顏色) = 1

15 mins

?

?

?

...

n-1

?

?

?

...

?

?

?

...

?

?

?

...

n-1

?

?

?

...

?

?

?

...

227 - 彩帶問題 - 解答

Exercise II - 彩帶問題 - 3

// RED = 0, ORANGE = 1, BLUE = 2
long long rec(int n, int type) {
  if (n == 1)
    return 1;
  else if (type == RED)
    return rec(n-1, BLUE);
  else if (type == BLUE)
    return rec(n-1, ORANGE) + rec(n-1, BLUE) + rec(n-1, RED);
  else if (type == ORANGE)
    return rec(n-1, BLUE) + rec(n-1, RED);
  else // Should not reach here.
    return 0;
}
  • rec(n, RED) = rec(n-1, BLUE)。
  • rec(n, ORANGE) = rec(n-1, RED), rec(n-1, BLUE)
  • rec(n, BLUE) = rec(n-1, RED) + rec(n-1, BLUE) + rec(n-1, ORANGE)
  • ​終止條件:rec(1, 不管甚麼顏色) = 1。

227 - 彩帶問題 - 解答嗎(?)

Exercise II - 彩帶問題 - 4

// RED = 0, ORANGE = 1, BLUE = 2
long long rec(int n, int type) {
  if (n == 1)
    return 1;
  else if (type == RED)
    return rec(n-1, BLUE);
  else if (type == BLUE)
    return rec(n-1, ORANGE) + rec(n-1, BLUE) + rec(n-1, RED);
  else if (type == ORANGE)
    return rec(n-1, BLUE) + rec(n-1, RED);
  else // Should not reach here.
    return 0;
}
  • rec(n, RED) = rec(n-1, BLUE)。
  • rec(n, ORANGE) = rec(n-1, RED), rec(n-1, BLUE)
  • rec(n, BLUE) = rec(n-1, RED) + rec(n-1, BLUE) + rec(n-1, ORANGE)
  • ​終止條件:rec(1, 不管甚麼顏色) = 1。

怎麼會吃TLE呢??

Memoization

記憶化

遞迴怎麼可以這麼慢啊...

Memoization 記憶化 - 0

  • 讓我們想想之前的遞迴樹。
C(4, 2)= \begin{cases} C(3, 1) = \begin{cases} C(2, 0) = 1 \\ + \\ C(2, 1) = \begin{cases} C(1, 0) = 1\\ + \\ C(1, 1) = 1 \end{cases} \end{cases} \\ + \\ C(3, 2) = \begin{cases} C(2, 1) = \begin{cases} C(1, 0) = 1\\ + \\ C(1, 1) = 1\\ \end{cases} \\ + \\ C(2, 2) = 1 \end{cases} \end{cases}
  • 所以答案為6
  • 你會發現一件事情,在分析的時候,有些遞迴會重複,這個時候用之前算過的值就好了。
  • 例如C(2, 1)有算過兩次,第二次直接看第一次算的值(=2)就好了。
  • 沒道理我們可以省略,
    程式省略不了啊?

什麼是記憶化?

Memoization 記憶化 - 1

  • 就是把以前算過的答案,丟在陣列記起來
    如果下次用到的時候直接給答案,不要再重算了!
  • 記在哪裡?全域變數就可以啦!
  • 舉個例子,記憶化的斐波那契數列會長的像右下角這樣:
    • visit陣列來表示有沒有算過。
    • DP陣列紀錄答案。
  • 所以,實作的時候,如果遞迴時發現...
    • 曾經算過(visit = true):
      • 直接回傳紀錄的答案
    • 沒算過(visit = false):
      1. 遞迴算答案紀錄起來並標示算過

Memorization

bool visit[MAXN];
int DP[MAXN];
int f(int n){
  if(n == 1 || n == 2)
    return 1;
  else if (visit[n])
    return DP[n];
  else {
    visit[n] = true;
    DP[n] = f(n-1) + f(n-2);
    return DP[n];
  }
}
  • MAXN表示最大的N,是個常數。

更複雜一點記憶化 - C(n, m)

Memorzation 記憶化 - 2

  • 在有兩個參數的遞迴中,記憶化也是可以使用的!
  • 舉例來說,C(n, m)的記憶化會長的像下面這樣:
  • 有沒有覺得記憶化的招數都一樣啊? 還真的都一樣。

Memoization

bool visit[MAXN][MAXM];
int DP[MAXN][MAXM];
int C(int n, int m){
  if(n == 0 || n == m)
    return 1;
  else if (visit[n][m])
    return DP[n][m];
  else { // visit[n][m] == false
    visit[n][m] = true;
    DP[n][m] = C(n-1, m-1) + C(n-1, m);
    return DP[n][m];
  }
}
  • MAXN,MAXM表示
    最大的N和最大的M,
    是個常數。

自己實作記憶化! - 彩帶問題

Memoization 記憶化 - 3

  • 那麼,你會把彩帶問題的遞迴記憶化嗎?

Memoization

// RED = 0, ORANGE = 1, BLUE = 2
long long rec(int n, int type) {
  if (n == 1)
    return 1;
  else if (type == RED)
    return rec(n-1, BLUE);
  else if (type == BLUE)
    return rec(n-1, ORANGE) + rec(n-1, BLUE) + rec(n-1, RED);
  else if (type == ORANGE)
    return rec(n-1, BLUE) + rec(n-1, RED);
  else // Should not reach here.
    return 0;
}

5 mins

自己實作記憶化! - 彩帶問題

Memoization 記憶化 - 4

  • 那麼,你會把彩帶問題的遞迴記憶化嗎?

Memoization

// RED = 0, ORANGE = 1, BLUE = 2
bool visit[MAXN][MAXTYPE];
long long DP[MAXN][MAXTYPE];
long long rec(int n, int type) {
  if (n == 1)
    return 1;
  else if (visit[n][type])
    return DP[n][type];
  else if (type == RED)
    DP[n][type] = rec(n-1, BLUE);
  else if (type == BLUE)
    DP[n][type] = rec(n-1, ORANGE) + rec(n-1, BLUE) + rec(n-1, RED);
  else if (type == ORANGE)
    DP[n][type] = rec(n-1, BLUE) + rec(n-1, RED);
  else // Should not reach here.
    return 0;
  visit[n][type] = true;
  return DP[n][type];
}

5 mins

Euclidean Algorithm

輾轉相除法

我們先來想想看...

Euclidean Algorithm 輾轉相除法 - 0

  • 怎麼算出GCD(5, 12)呢?

最大公因數

5 = 5 \, \, \, \, \, \, \, \, \,\ \, \, \, \, \, \\ 12 = 2^2 \times 3
  • 因式分解後找相同,但有沒有更快的方法呢?
  • 兩個數字一直互減,就會減到剩下最大公因數。
    (另一個就會變成是0)

輾轉相除法的基本算法 & 小小驗證

Euclidean Algorithm

  • 兩個數字一直互減,就會減到剩下最大公因數。
    (另一個就會變成是0)
GCD(a, b) = GCD(a, b-a)
  • 如果          ,那麼下面這個式子成立
b \ge a
  • 如果    是       的公因數,令                           。
a = a'd, b = b'd
d
a, b
d
  • 你會發現   也同時會是           和     的公因數。

Euclidean Algorithm 輾轉相除法 - 1

b-a
a

輾轉相除法的基本算法 & 小小驗證

Euclidean Algorithm

GCD(a, b) = GCD(a, b-a)
  • 如果          ,那麼下面這個式子成立。
b \ge a
GCD(a, b) = GCD(a, b\%a)
  • 那麼下面也會成立。(因為%就是減到不能再減。)
  • 如果說已經減到其中一個為0呢?
GCD(a, b) = a+b
GCD(a, b) = GCD(a\%b, b)
  • 同理,          ,那麼下面式子也會成立。
b \le a
  • 到這裡,你會寫輾轉相除法了嗎?

Euclidean Algorithm 輾轉相除法 - 2

輾轉相除法的程式實作

Euclidean Algorithm

GCD(a, b) = GCD(a, b\%a)
  • 如果          ,那麼下面這個式子成立。
b \ge a
GCD(a, b) = a+b
GCD(a, b) = GCD(a\%b, b)
  • 同理,          ,那麼下面式子也會成立。
b \le a
int GCD(int a, int b){
  if(a == 0 || b == 0)
    return a + b;
  if (b >= a)
    return GCD(a, b % a);
  else
    return GCD(a % b, b);
}

GCD的小小程式

Euclidean Algorithm 輾轉相除法 - 3

  • 如果說已經減到其中一個為0呢?

輾轉相除法的更簡單程式實作

Euclidean Algorithm

  • 其實GCD可以寫成右上角的小小小小程式。
    • 為什麼?我們來想想看:
      • 如果a比b大,GCD(a, b)=GCD(b, a%b)
      • 如果b比a大,GCD(a, b)=GCD(b, a%b)=GCD(b, a)
    • 你發現了嗎? 不管a,b大小怎麼樣,下一次遞迴呼叫後,比較大的數字都會在前面。
      • 如果a比b大, b > a%b。
      • 如果b比a大,b > a。
    • 所以,只要移動一下下一次遞迴的參數位置,就可以確保比較大的都在前面。
int GCD(int a, int b){
  if(a == 0 || b == 0)
    return a + b;
  if (b >= a)
    return GCD(a, b % a);
  else
    return GCD(a % b, b);
}

GCD的小小程式

int GCD(int a, int b){
  if(a * b == 0)
    return a + b;
  return GCD(b, a % b);
}

GCD的小小小小程式

Euclidean Algorithm 輾轉相除法 - 4

Exercise III

Rhythm Doctor - 最大公因數的應用問題

Exercise III

程式練習題

接下來有一題請你用程式實做遞迴的應用題。

  • 我會花1分鐘讀題。
  • 總共計時10分鐘
  • 計時之前,好好複習一下GCD的程式。

229 - Rhythm Doctor - 工商

Exercise III - Rhythm Doctor - 0

  • 分享一個 Rhythm Doctor 我最喜歡的一首。

10 mins

229 - Rhythm Doctor - 題目

  • 題目:每個人的生命中都有各自的拍數。假設大家的拍子都從第1秒開始,
    請問下一次大家剛好在同個時間打拍子是什麼時候?
    (保證答案在long long範圍內。)

  • 舉例來說,如果有兩個人AB,A和B的拍數分別為3和4,
    那麼下次同時打拍子會在第12秒。

10 mins

Exercise III - Rhythm Doctor - 1

A

B

3s

3s

3s

3s

4s

4s

4s

229 - Rhythm Doctor - 解析

  • 題目:每個人的生命中都有各自的拍數。假設大家的拍子都從第1秒開始,
    請問下一次大家剛好在同個時間打拍子是什麼時候?
    (保證答案在long long範圍內。)
     
  • ㄚ就是多個數字的LCM(最小公倍數) 啊? 
    • 記得要把code改成long long。

10 mins

Exercise III - Rhythm Doctor - 2

long long GCD(long a, long b){
  if(a * b == 0)
    return a + b;
  return GCD(b, a % b);
}
long long LCM(long long a, long long b){
  return a / GCD(a, b) * b;
}

229 - Rhythm Doctor - 解析

  • 題目:每個人的生命中都有各自的拍數。假設大家的拍子都從第1秒開始,
    請問下一次大家剛好在同個時間打拍子是什麼時候?
    (保證答案在long long範圍內。)
     
  • ㄚ就是多個數字的LCM(最小公倍數) 啊?
    • 就一直拿數字做LCM就好了。

10 mins

Exercise III - Rhythm Doctor - 3

int main() {
  long long n, ans = 1, x;
  cin >> n;
  for (int i=0; i<n; i++) {
    cin >> x;
    ans = LCM(ans, x);
  }
  cout << ans << endl;
}

Exercise IV

零和問題 - 暴力搜尋所有可能

Exercise IV

程式練習題

接下來有一題請你用程式實做遞迴的應用題。

  • 我會花1分鐘讀題。
  • 總共計時10分鐘
  • 這題很不簡單,不寫出來沒關係,但先想想看你要怎麼寫以及遞迴的過程。

228 - 零和問題 - 題目

Exercise IV - Subset Sum Problem 零和問題 - 0

> 20 mins

  • 題目:現在有n個正整數,請問你可不可以從中間選幾個數字使其加總為k
    如果可以輸出"YES",否則輸出"NO"。(如果數字選過了就不能再選了。)
  • 舉例來說,如果有五個數字[3, 7, 11, 9, 4],請問可不可以湊到數字16呢?
    • 輸出YES,因為 7 + 9 = 16。
    • 好像很難也?Hint: 我們來畫畫看這題的遞迴樹。

228 - 零和問題 - 提示

> 20 mins

  • 題目:現在有n個正整數,請問你可不可以從中間選幾個數字使其加總為k
    如果可以輸出"YES",否則輸出"NO"。(如果數字選過了就不能再選了。)

(0, 0)

(1, 3)

(1, 0)

(2, 10)

(2, 7)

(2, 3)

(2, 0)

(3, 21)

(3, 14)

(3, 10)

(3, 3)

(4, 30)

(4, 19)

(4, 21)

(4, 10)

(5, 34)

(5, 25)

(5, 30)

(5, 21)





(看第幾個數字,
現在加到哪裡)

不選

+3

+7

+7

+11

+11

+9

+9

+4

+4

  • 舉例來說,如果有五個數字[3, 7, 11, 9, 4],請問可不可以湊到數字16呢?

Exercise IV - Subset Sum Problem 零和問題 - 1

228 - 零和問題 - 引導

> 20 mins

  • 題目:現在有n個正整數,請問你可不可以從中間選幾個數字使其加總為k
    如果可以輸出"YES",否則輸出"NO"。(如果數字選過了就不能再選了。)
  • 根據我們所寫的遞迴樹,我們會知道怎麼暴力搜尋。
    • 一開始看第0個,接下來我們分成兩個case來看:
      • 選第0個數字 (3),接下來看第1個數字,分成兩個case來看:
        • 選第1個數字 (10),接下來看第2個數字,分成兩個case來看...
        • 不選第1個數字 (3),接下來看第2個數字,分成兩個case來看...
      • ​不選第0個數字 (0),接下來看第1個數字,分成兩個case來看:
        • ​選第1個數字 (7),接下來看第2個數字,分成兩個case來看...
        • ....
  • 舉例來說,如果有五個數字[3, 7, 11, 9, 4],請問可不可以湊到數字16呢?

Exercise IV - Subset Sum Problem 零和問題 - 2

228 - 零和問題 - 引導

> 20 mins

  • 題目:現在有n個正整數,請問你可不可以從中間選幾個數字使其加總為k
    如果可以輸出"YES",否則輸出"NO"。(如果數字選過了就不能再選了。)
  • 遞迴參數:現在看到第幾個數字 i,以及現在總和 S。
    • 要暴搜rec(i, S)的所有可能,可以分成兩個case暴搜:
      • 選第i個數字:rec(i+1, S+A[i])
      • 不選第i個數字:rec(i+1, S)
    • 終止條件?
      • 所有數字都看完了。
        • i = n。

Exercise IV - Subset Sum Problem 零和問題 - 3

228 - 零和問題 - 解答

> 20 mins

  • 題目:現在有n個正整數,請問你可不可以從中間選幾個數字使其加總為k
    如果可以輸出"YES",否則輸出"NO"。(如果數字選過了就不能再選了。)
bool rec (int i, int S) {
  if (i == n)
    return S == k;
  else
    return rec(i+1, S+A[i]) || rec(i+1, S);
}
  • 遞迴參數:現在看到第幾個數字 i,以及現在總和 S。
    • 要暴搜rec(i, S)的所有可能,可以分成兩個case暴搜:
      • 選第i個數字:rec(i+1, S+A[i]) / 不選第i個數字:rec(i+1, S)
    • 終止條件?所有數字都看完了,也就是i = n。

Exercise IV - Subset Sum Problem 零和問題 - 4

228 - 零和問題 - 比較詳細的解答

> 20 mins

#include <iostream>
#define MAXN 30
using namespace std;
int k, A[MAXN], n;
bool rec (int i, int S) {
  if (i == n)
    return S == k;
  else
    return rec(i+1, S+A[i]) || rec(i+1, S);
}
int main () {
  cin >> n >> k;
  for (int i=0; i<n; i++)
    cin >> A[i];
  cout << (rec(0, 0) ? "YES" : "NO") << endl;
}
  • 題目:現在有n個正整數,請問你可不可以從中間選幾個數字使其加總為k
    如果可以輸出"YES",否則輸出"NO"。(如果數字選過了就不能再選了。)

Exercise IV - Subset Sum Problem 零和問題 - 5

Pruning

剪枝

什麼是剪枝?

Pruning 剪枝 - 1

  • 就是不要讓遞迴跑的太多次!
    • 簡單來說,如果接下來一定不會找到答案,我們就別再找下去了,
      直接停掉避免浪費時間。
    • 在遞迴樹上砍掉一些點 → 所以這個技巧被稱作剪枝(Pruning)。

Pruning

在零和問題剪枝?

  • 來看看剛剛遞迴樹怎麼剪枝吧!

Pruning

Pruning 剪枝 - 2

  • 舉例來說,如果有五個數字[3, 7, 11, 9, 4],請問可不可以湊到數字16呢?
  • 把不可能的遞迴跳掉!

(0, 0)

(1, 3)

(1, 0)

(2, 10)

(2, 7)

(2, 3)

(2, 0)

(3, 21)

(3, 14)

(3, 10)

(3, 3)

(4, 19)

(4, 10)

(5, 25)

(5, 10)

(看第幾個數字,
現在加到哪裡)

不選

+3

+7

+7

+11

+11

+9

+4

(4, 23)

(4, 14)

+9

(5, 19)

(5, 14)

+4

在零和問題剪枝?

  • 把不可能的遞迴跳掉!
    • 還可以減掉更多枝嗎?
    • 如果題目數字有可能是負數,還可以這樣做嗎?

Pruning

Pruning 剪枝 - 2

bool rec (int i, int S) {
  if (S > k)
    return false;
  if (i == n)
    return S == k;
  else
    return rec(i+1, S+A[i]) || rec(i+1, S);
}

Talks

關於遞迴的小小雜談

題目表 - 遞迴

題目表 - 0

題號 名稱 註記
226 警報器 上課習題
227 彩帶問題 上課習題
228 零和問題 上課習題
229 Rhythm Doctor 上課習題
230 貝爾三角形
231 填積木問題 經典DP題目
232 邪惡水族箱 APCS 考古題

Top-down & Bottom-up

Talks - 1

  • 其實你隱約知道了,有些遞迴是可以用for迴圈寫的。
    • 我們來複習用for迴圈實作斐波那契數列?



 

  • 你會發現,用for迴圈通常都是慢慢堆答案上去的,
    • 這類的方法稱作Bottom-up,也就是堆上去的方法。
    • 那麼遞迴呢?通常遞迴都是從大問題去呼叫小問題的,
      這類的方法稱作為Top-down
  • 「遞迴只應天上有,凡人應當用迴圈」
int fib[100] = {0, 1, 1};
for (int i=3; i<=n; i++) {
  fib[i] = fib[i-1] + fib[i-2];
}

Talks - 2

  • 想要知道Top-down & Bottom-up的後續嗎?
    • 去看看算法班的動態規劃 DP (Dynamic Programming)吧! (蛋餅的投影片)
  • 但並不是所有題目都適合用Bottom-up,所以還是要會遞迴喔~

Top-down & Bottom-up

更多的遞迴...

Talks - 3

  • 其實最後一個問題 - 零和問題對初心者來說很難啦,
    所以可以回去在複習一次OuO。
  • 雖然零和問題很難,但是這才是遞迴的剛開始而已!
  • 如果你對遞迴很有興趣,歡迎參考我在資訊之芽的遞迴講義
    • 這個講義有四分之一我們剛剛講過了,
      但後半段比我們這堂課還要難很多喔! 例如我們有提及...
      • 河內塔
      • 字串枚舉
      • 數獨
      • 搶數字問題
         
  • 這是我們最後一堂課了QQ,祝你們明天模擬考順利!

End

Q&A?

其實...

Talks - 0

  • 在APCS實作題裡面,其實幾乎沒有裸遞迴題。
    • 但是遞迴非常的重要!
    • 在算法班的課程裡,除了資料結構 / 貪心以外,一定會用到遞迴
      (其他不是用不到,也可能會用到遞迴)
    • 遞迴的概念對新手來說很不好理解,要慢慢適應遞迴的思考方法。