RRRecursion

Arvin Liu

RRRRRRRRRRRRRRRRRRRRR

什麼是遞迴?

什麼是遞迴?

感受一下?

  • 維基上的定義:

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

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

什麼是遞迴?

現場來寫寫看吧!

N!

求出 N!

給你一個數字 N,請輸出 N!

 

 

N! = \prod_{i=1}^{N} i = 1 \times 2 \times ... \times N-1 \times N

那還不簡單?

不就是基礎 for 迴圈嗎?

來想想看有沒有其他寫法!

int factorial(int N) {
  int ans = 1;
  for (int i=1; i<=N; i++)
    ans *= i;
  return ans;
}

C++

def factorial(N):
  ans = 1
  for i in range(1, N):
    ans *= i
  return ans

Python

求出 N!

給你一個數字 N,請輸出 N!

 

 

假設現在有一個函數:

f(n) = n!

如果不用 "!" 該怎麼寫?

f(n) = n \times(1 \times 2 ... \times N-1) \\
N! = \prod_{i=1}^{N} i = 1 \times 2 \times ... \times N-1 \times N
= n \times (n-1)! \\ = n \times f(n-1)

求出 N!

給你一個數字 N,請輸出 N!

 

 

f(n) = n \times f(n-1)
N! = \prod_{i=1}^{N} i = 1 \times 2 \times ... \times N-1 \times N

直接把它轉成程式?

跑跑看!

int factorial(int N) {
  return N * factorial(N-1);
}
def factorial(N):
  return N * factorial(N-1)

C++

Python

求出 N!

輸出結果 :

4
3
2
1
0
-1
-2
....

跑不完!

這裡就該停了!

#include <stdio.h>
int factorial(int N) {
  printf("%d\n", N);
  return N * factorial(N-1);
}

int main() {
    printf("%d", factorial(4));
}
def factorial(N):
  print(N)
  return N * factorial(N-1)


print(factorial(4))

C++

Python

求出 N!

給你一個數字 N,請輸出 N!

 

 

f(n) = \, \, n \times f(n-1)
N! = \prod_{i=1}^{N} i = 1 \times 2 \times ... \times N-1 \times N
\begin{cases} \\ 1, \text{if n = 0} \end{cases}

這樣就可以了!

int factorial(int N) {
  if (N == 0) return 1;
  return N * factorial(N-1);
}
def factorial(N):
  if N == 0: return 1
  return N * factorial(N-1)

C++

Python

怎麼寫遞迴?

怎麼寫遞迴?

  1. 定義遞迴函式要做什麼? 等於什麼?
    • f(n) = n! 的數值
  2. 觀察如何將大函式用小函完成?
    • f(n) 跟 f(n-1) 有點關係
  3. 記得寫終止條件 (Base Case)
    • f(0) = 1
  • 思考的時候,不要從一開始往上推,例如 n=0 怎麼樣,n=1怎麼樣...
  • 通常情況下,要逆向思考,習慣倒推
    • 例如 n 要怎麼用n-1表示,或者n-2表示..

記得寫終止條件!

N!遞迴的逐步過程

f(n) = \begin{cases} 1 & \text{if } n=0\\ n \times f(n-1) & \text{otherwise} \\ \end{cases}

以           作為舉例

f(3)
f(3) = 3 \times f(2)
f(2) = 2 \times f(1)
f(1) = 1 \times f(0)
f(0) = 1

呼叫

呼叫

呼叫

= 1 \times 1 = 1
= 2 \times 1 = 2
= 3 \times 2 = 6

回傳

回傳

回傳

回傳

實際例子可以參考另一個投影片

練習題!

  1. 如果題目改成 1 + 2 + ... + n,你會寫嗎?
  2. 有沒有覺得遞迴很沒用?不就迴圈可以寫的事情?試著想想看遞迴有什麼優點吧!
  3. Clumsy Factorial

小知識: 對google搜尋遞迴

按按看(?)

爬樓梯問題

東東爬樓梯 (zj d212)

東東爬階梯每次可以走一或兩階。

假設階梯有 n 階,那有幾種走法?

🐦

🐦

3 階有 3 種走法

4 階有 5 種走法

1+1+1+1

1+1+2

1+2+1

2+1+1

2+2

1+1+1

1+2

2+1

有點難?

東東爬樓梯 (zj d212)

  1. 函式定義:
  2. 如何拆解:
  3. Base Case:

f(n) = 走 n 階的可能數

f(n-1)     f(n-2)

🐦

...

n\begin{cases} \\ \\ \end{cases}

可以將到第n階的走法分成兩類

  1. 最後一步走1階的
    • ​​​​最後一步走1階有幾種可能?
    • f(n-1)
  2. 最後一步走2階的
    • ​​f(n-2)

+

東東爬階梯每次可以走一或兩階。

假設階梯有 n 階,那有幾種走法?

東東爬樓梯 (zj d212)

  1. 函式定義:
  2. 如何拆解:
  3. Base Case:

f(n) = 走 n 階的可能數

f(n-1)     f(n-2)

+

輸入可能是 1 以後的正整數。

f(1) 可以用遞迴算出嗎?

f(2) 可以用遞迴算出嗎?

f(3) 可以用遞迴算出嗎?

f(1) = 1, f(2) = 2

東東爬階梯每次可以走一或兩階。

假設階梯有 n 階,那有幾種走法?

東東爬樓梯 (zj d212)

  1. 函式定義:
  2. 如何拆解:
  3. Base Case:

f(n) = 走 n 階的可能數

f(n-1)     f(n-2)

+

f(1) = 1, f(2) = 2

東東爬階梯每次可以走一或兩階。

假設階梯有 n 階,那有幾種走法?

int fib(int N) {
  if (N == 1) return 1;
  if (N == 2) return 2;
  return fib(N-1) + fib(N-2);
}
def fib(N):
  if N == 1: return 1
  if N == 2: return 2
  return fib(N-1) + fib(N-2)

C++

Python

爬樓梯問題與費波那契數列

f(n) = \begin{cases} 1 & \text{if } n = 1 \\ 2 & \text{if } n = 2 \\ f(n-1) + f(n-2) & \text{otherwise} \end{cases}

這就是鼎鼎大名的費波那契數列

fibonacci

  • f(0) = 1

費波那契數列的遞迴過程

f(n) = \begin{cases} 1 & \text{if } n = 1 \\ 2 & \text{if } n = 2 \\ f(n-1) + f(n-2) & \text{otherwise} \end{cases}
f(4) = f(3) + f(2)

以           作為舉例

f(4)
f(3) = f(2) + f(1)
f(2) = 2
f(2) = 2
f(1) = 1

2

1

3

2

= 3
= 5

練習題!

  1. 如果題目改成東東每次可以走 1, 2, 3 階,遞迴會怎麼寫?
  2. 你會用迴圈寫這題嗎?
  3. 你覺得
    1. 遞迴版本的時間複雜度多少?
    2. 迴圈版本的時間複雜度多少?

C(n, m)

請問從 N 顆不同的球裡,
有幾種取出 M 顆球的方法?

有 4 顆相異的球

如果取三顆,共 4 種取法

如果取兩顆,共 6 種取法

請問從 N 顆不同的球裡,
有幾種取出 M 顆球的方法?

有 4 顆相異的球

如果取三顆,共 4 種取法

  1. 函式定義:
  2. 如何拆解:
  3. Base Case:

這個也好難....

C(n, m) = N 取 M 的可能數

請問從 N 顆不同的球裡,
有幾種取出 M 顆球的方法?

有 4 顆相異的球

如果取三顆,共 4 種取法

  1. 函式定義:
  2. 如何拆解:
  3. Base Case:
\}
\}

取了最
後的球

沒取最
後的球

取了      後

需要再從三顆中取兩顆

放棄      後

需要再從三顆中取三顆

C(n, m) = N 取 M 的可能數

請問從 N 顆不同的球裡,
有幾種取出 M 顆球的方法?

n 顆相異的球

想要取 m 顆

  1. 函式定義:
  2. 如何拆解:
  3. Base Case:

取了最
後的球

沒取最
後的球

...

\begin{cases} \\ \\ \\ \end{cases}
\begin{cases} \\ \\ \end{cases}

還剩m顆要選

還剩m-1顆要選

C(n-1, m)

C(n-1, m-1)

+

C(n, m) = N 取 M 的可能數

放棄      後

需要再從n-1顆中取m顆

取了      後

需要再從n-1顆中取m-1顆

請問從 N 顆不同的球裡,
有幾種取出 M 顆球的方法?

  1. 函式定義:
  2. 如何拆解:
  3. Base Case:

f(N,M) = N 取 M 的可能數

C(n-1, m)

C(n-1, m-1)

+

C(3, 2)

C(2, 1)

C(2, 2)

C(1, 0)

C(1, 1)

  1. m = 0 的時候,​表示不取,答案為1
  2. m = n 的時候,表示全取,答案為1

n 跟 m 減到哪裡該停?

想想看 C(3, 2) 的例子吧!

m = 0 或者 n = m 時為1

請問從 N 顆不同的球裡,
有幾種取出 M 顆球的方法?

  1. 函式定義:
  2. 如何拆解:
  3. Base Case:

f(N,M) = N 取 M 的可能數

C(n-1, m)

C(n-1, m-1)

+

m = 0 或者 n = m 時為1

一個看似複雜的題目,遞迴程式卻非常少!

這就是遞迴的魅力之處 (?)

int C(int n, int m){
	if (m == 0 || m == n) return 1;
    return C(n-1, m-1) + C(n-1, m);
}
def C(n, m):
  if m == 0 or m == n: return 1
  return C(n-1, m) + C(n-1, m-1)

C++

Python

練習題!

  1. C(n, m) 其實是有公式。
     

    •  
    • 使用這個公式如果直接在 C++ 用 for 實作,其實會有些問題在,你知道為什麼嗎?
C^n_m = \frac{n!}{m! \times (n-m)!}

0/1 背包問題

給你 N 個物品和物品的重量和價值,以及祖靈的背包的重量限制。

請問祖靈最多可以帶價值東西回家?

拿走什麼? 總重 價值
G+C+S 15kg $8
Y+C+S+B 8kg $15
... ... ...

C

B

Y

G

S

給你 N 個物品和物品的重量和價值,以及祖靈的背包的重量限制。

請問祖靈最多可以帶價值東西回家?

  1. 函式定義:

f(n, W) = 前 n 個物品中,
限重 W 的情況下的最大價值

有點複雜... 試試看裸套題目定義

f(2, 5): 如果可以拿0~2號的物品

背包限重為5回傳所有可能中的的最大價值。

f(7, 10): 如果可以拿0~7號的物品

背包限重為10回傳所有可能中的最大價值。

給你 N 個物品和物品的重量和價值,以及祖靈的背包的重量限制。

請問祖靈最多可以帶價值東西回家?

  1. 函式定義:
     
  2. 如何拆解:

又卡住了 :( ...

其實背包問題跟 C(n,m) 很像!

都是 n 個物品裡面挑幾個東西出來

  • C(n, m) 的限制是只能挑 m 個
  • 背包問題的限制是挑出來的物品限重不能超過 W

f(n, W) = 前 n 個物品中,
限重 W 的情況下的最大價值

給你 N 個物品和物品的重量和價值,以及祖靈的背包的重量限制。

請問祖靈最多可以帶價值東西回家?

  1. 函式定義:
     
  2. 如何拆解:

又卡住了 :( ...

f(n, W) = 前 n 個物品中,
限重 W 的情況下的最大價值

有 n 顆相異的物品,

限重 W

取了最
後的

沒取最
後的

...

💎

👑

📿

💎

💎

0/1 背包問題

有 n 顆相異的球

想要取 m 顆

取了最
後的球

沒取最
後的球

...

C(n, m) 問題

給你 N 個物品和物品的重量和價值,以及祖靈的背包的重量限制。

請問祖靈最多可以帶價值東西回家?

  1. 函式定義:
     
  2. 如何拆解:

有 n 顆相異的物品,

限重 W

取了最
後的

沒取最
後的

...

💎

👑

📿

💎

💎

放棄第 n 個物品,
當它不存在。

可以裝的重量剩

w_{_💎}

但多了價值

v_{_💎}
= f(n-1, W)
= f(n-1, W-w_{_💎}) \\ + v_{_💎}

f(n, W) = 前 n 個物品中,
限重 W 的情況下的最大價值

背包還剩

背包還剩        

W
W-w_{_💎}

給你 N 個物品和物品的重量和價值,以及祖靈的背包的重量限制。

請問祖靈最多可以帶價值東西回家?

  1. 函式定義:
     
  2. 如何拆解:


     
  3. Base Case :  
f(n-1, W)
f(n-1, W-w_n) + v_n

f(n, W) = 前 n 個物品中,
限重 W 的情況下的最大價值

\max
\begin{cases} \\ \\ \\ \end{cases}

n 會一直往下減... 考慮第零號物品!

f(0, W) = \begin{cases} 0 & \text{if } W < w_0 \\ v_0 & \text{otherwise} \end{cases}
  1. 函式定義:
     
  2. 如何拆解:

     
  3. Base Case :  

f(n, W) = 前 n 個物品中,限重 W 的情況下的最大價值

f(n, W) = \max \begin{cases} f(n-1, W), \\ f(n-1, W-w_n) + v_n) & \text{if } W \ge w_n \end{cases}
f(0, W) = \begin{cases} 0 & \text{if } W < w_0 \\ v_0 & \text{otherwise} \end{cases}
int f(int n, int w) {
  if (n == 0)
    return w >= W[n] ? V[n] : 0;
  if (w >= W[n])
    return max(
      f(n-1, w), 
      f(n-1, w - W[n]) + V[n]
    );
  return f(n-1, w);
}
def f(n, w):
  if n == 0: 
    return V[n] if w >= W[n] else 0
  if w >= W[n]:
    return max(
      f(n-1, w),
      f(n-1, w-W[n]) + V[n]
    );
  return  f(n-1, w);

C++

Python

練習題!

  1. 如果將有個函數 f(n, V) 的定義為n個物品裡面選到價值為V的最低重量

    ( 例如f(n,10)=3 表示選到價值為10的物品時,重量最低為3 )


    那麼你會寫出遞迴式嗎?
     

  2. 你要怎麼使用第一題的函數定義來找出限重為 W 的背包最高價值為何?
     
  3. 如果每個物品都可以拿無限次,請問你有辦法改寫遞迴式嗎?

記憶化

記憶化 Memoization

為什麼 fib(40) 很慢?

試試看算出 fib(40) !

理論上不該啊?

記憶化 Memoization

我們以爬樓梯的題目來看

fib(3)一樣
卻會重算!

fib(4)

fib(3)

fib(2)

fib(2)

fib(1)

fib(5)

fib(3)

fib(2)

fib(1)

記憶化 Memoization

如何避免重算?

想像有個函數...

f

輸入 A

輸入 B

輸入 A

第一次碰到!

算出f(A)

第一次碰到!

算出f(B)

算過 A 了!

拿出之前算過的答案

你必須在第一次碰到的時候記錄起來!

記憶化 Memoization

  • 如果沒算過,就算出答案,記錄起來。
  • 如果有算過,就直接從記憶裡撈出來。

用一個方法可以記錄 「問題」 -> 「答案」

例如 陣列、unordered_map、dict 等等

long long dp[1000];
long long fib(int n){
	if (n == 0 || n == 1)
    	return 1;
    if (!dp[n])
    	dp[n] = fib(n-1) + fib(n-2);
    return dp[n];
}
dp = {}
def fib(n):
  if n == 0 or n == 1 :
    return 1
  if n not in dp:
    dp[n] = fib(n-1) + fib(n-2)
  return dp[n]

C++

Python

因為 fib 都會 > 0,所以我們可以用

dp[n] 是不是 0 來決定有沒有算過。

記憶化 Memoization

來分析看看複雜度吧!

重複計算 (原本的code)

重複計算 (記憶化)

f(n) = f(n-1) + f(n-2)

"大概"每多一個n,
就會多算一倍

O(2^n)

算完 f(n-1) 時,

f(n-2) 已經算過了,不用重算

O(n)

記憶化 Memoization

long long dp[1000];
long long f(int n){
	if (n == 0 || n == 1)
    	return 1;
    if (!dp[n])
    	dp[n] = f(n-1) + f(n-2);
    return dp[n];
}

來用人腦跑一次看看吧!如果呼叫 f(4)...

n 0 1 2 3 4
f(n) 1 1 ? ? ?
- f(1) f(2) f(3)
- f(0) f(1) f(2)

1

1

2

2

1

3

3

2

5

f(2) 不用再重新遞迴一次 f(1) 跟 f(0) 了!

練習題

  1. 請問背包問題 (祖靈好孝順 ˋˇˊ) 經過記憶化後的時間複雜度是多少?

     

  2. 我們確信問題的答案都大於零,所以才可以使用 !dp[n] 來表示沒有算過。

    那麼如果答案可能是 0 怎麼辦?
     
  3. 如果參數的範圍很大,大到無法開陣列,但問題的種類數量很小 (DP為稀疏陣列),那怎麼使用記憶化?

    (例如 dp[n] 的 n 有可能超大,但 dp[n] 不為0的值其實沒有那麼多,那要怎麼節省空間?)

遞迴小結

遞迴小結

  1. 定義遞迴函式要做什麼? 等於什麼?
    • f(n) = n! 的數值
  2. 觀察如何將大函式用小函完成?
    • f(n) 跟 f(n-1) 有點關係
  3. 記得寫終止條件 (Base Case)
    • f(0) = 1
  4. 如果遞迴有可能會重複計算,
    使用記憶化 (Memoization)

遞迴小結

  • 「遞迴只應天上有,凡人應當用迴圈」
    • 看起來好像有些遞迴可以用迴圈寫啊?
    • 那就用迴圈寫。
       
  • 那為甚麼要用遞迴寫code?
    • 有些題目用迴圈還真的寫不出來...
    • 為了寫快一點! (遞迴的code通常很短)
    • 注意 stack overflow.

Stack Overflow

什麼是Stack Overflow?

不是指某個回答問題的網站。

什麼是Stack Overflow?

在函式裡宣告的變數都會在stack空間。 (包括main)

當你呼叫非常非常非常多的函式...

stack
警戒線

超出stack警戒線就會導致程式執行錯誤。

(所以是Runtime Error)

怎麼解決Stack Overflow?

stack
警戒線

  • 不要用遞迴寫 (這在幹話?)
    • 如果可以,用for迴圈寫。
      (例如fib可直接用for。)
       
  • 如果不行
    • 使用全域變數或heap空間,用別的方法模擬遞迴 (詳見圖論投影片-bfs/dfs)
    • 黑魔法 - 內嵌組合語言強制遞迴用heap空間

黑魔法 - 強制調用heap空間

extern int main2(void) __asm__ ("main2"); 
 
int main2() { 
    run();
    exit(0); 
} 
 
int main() { 
    // 跟heap借256MB
    int size = 256 << 20;
    char *p = (char *)malloc(size) + size; 
    __asm__ __volatile__( 
        "movq  %0, %%rsp\n" 
        "pushq $exit\n"  
        "jmp main2\n" 
        :: "r"(p)); 
}
  1. 把你原本程式的main改名成run。
  2. 把上面這份code貼在你的程式的最下面。
#include <cstdlib>

輾轉相除法

Euclidean Algorithm

同餘定理

(a + b) \% M = ((a \% M) + (b \% M)) \% M
a = q_{a}M + r_a \\ b = q_{b}M + r_b
(a + b) \% M \\ = ((q_a + q_b)M + (r_a + r_b)) \% M \\ = (r_a + r_b) \% M
(a\%M + b\%M) \% M \\ = ((q_aM +r_a)\%M + (q_bM + r_b)\%M)\%M \\ = (r_a + r_b) \% M

同餘定理

猥瑣罐頭下樓梯(zj a272) 中,

除了要寫東東爬樓梯以外,
多了一個奇怪的條件...

你可能會輸出                      
但在運算的過程中數字太大噴掉...

fib(n) \% M
fib(n)\%M = (fib(n-1)\%M + fib(n-2)\%M) \% M

在每次運算完後直接%就可以避免溢位!

練習題

  1. (a * b) % M 有辦法用像 (a + b) % M 一樣的技巧嗎?
  2. (a ^ b) % M 呢?
  3. (a - b) % M 呢?
    • 如果 a = 7, b = 5, M = 6
      • (7 - 5) % 6 = 2
      • ((7 % 6 = 1) - (5 % 6 = 5)) % 6 = (1 - 5) % 6
        = -4 % 6 ???? (在C++中會是 -4,但python是2)
      • 你該怎麼解決這個問題?

輾轉相除法

(x \pm y) \% M = ((x \% M) \pm (y \% M)) \% M
gcd(a, b) = gcd(a - b, b), \text{if } a \ge b
d
d'
b 整除 d, d', a整除 d, a-b 整除 d'
x = a, y = b, M = d
(a-b) \% d =(a\%d -b\%d) \%d = (0 - 0) \% d = 0
(a-b), a, b 都可以整除 d
d' \ge d

輾轉相除法

gcd(a, b) = gcd(a - b, b), \text{if } a \ge b
d
d'
b 整除 d, d', a整除 d, a-b 整除 d
x = (a-b), y = b, M = d'
a \% d' = ((a-b)\%d' + b\%d') \%d' = (0 - 0) \% d' = 0
a, (a-b), b 都可以整除 d'
d \ge d'
(x \pm y) \% M = ((x \% M) \pm (y \% M)) \% M

輾轉相除法

gcd(a, b) = gcd(a - b, b), \text{if } a \ge b
d
d'
b 整除 d, d', a整除 d, a-b 整除 d
d' \ge d \ge d' \rightarrow d' = d

想想看程式要怎麼寫吧!

gcd(a, b) = \begin{cases} a & \text{if }b = 0 \\ b & \text{if }a = 0 \\ gcd(a-b, b) & \text{if } a \ge b \\ gcd(a, b-a) & \text{if } b \ge a \\ \end{cases}

練習題

  1. 可以簡化成這樣,想想看為什麼?




     
    • Hint 1: % b 的一種看法-b-b-b...減到不能再減
    • Hint 2: 參數的位置不一樣,這確定了 a 跟 b 的一個關係
  2. 你會求出 LCM (最小公倍數) 嗎?
int GCD(int a, int b){
  if(b == 0)
    return a + b;
  return GCD(b, a % b);
}

2^n 暴力搜尋

n! 暴力搜尋

分治法

河內塔

遞迴的複雜度分析

!數學警告!

遞迴的複雜度分析

假設你學會了時間複雜度...

來上點難度吧!

f(n) = \begin{cases} 1 & \text{if } n = 0 \\ f(n-1) +\underbrace c_{c 是一個常數} & \text{otherwise} \end{cases}
def f(n):
  if n == 0:
    return
  f(n-1)

這段程式碼的時間分析大概就長這樣

遞迴的複雜度分析

假設你學會了時間複雜度...

來上點難度吧!

f(n) = \begin{cases} 1 & \text{if } n = 0 \\ f(n-1) +\underbrace c_{c 是一個常數} & \text{otherwise} \end{cases}
f(n) \text{的時間複雜度?}

遞迴的複雜度分析

f(n) =f(n-1) + c
= (f(n-2) + c) + c \\ = ...\\\ = f(0) + cn \\ = 1 + cn \\ \rightarrow O(n)

來更難的吧!

遞迴的複雜度分析

f(n) =f(n-1) + cn
= (f(n-2) + c(n-1)) \\ = f(n-2) + c(n + (n-1)) \\ \\ = f(n-3) + c(n + (n-1) + (n-2)) \\ = ...\\\ = f(0) + c(n + (n-1) + (n-2) ... + 1) \\ = 1 + c \frac{n (n+1)}{2} \\ \rightarrow O(n^2)

遞迴的複雜度分析

f(n) =f(\lfloor \frac{n} {2}\rfloor) + c
\lceil x \rceil = 無條件進位 x \\ \lfloor x \rfloor = 無條件捨去 x
小技巧: 讓 n = 2^k,就不用處理高斯符號
f(n) = f(2^{k-1}) + c \\ = (f(2^{k-2}) + c )+ c \\ = f(0) + \underbrace{ c + c + ... + c}_{k} \\ = ck = c \log2 n \rightarrow O(\log n)

遞迴的複雜度分析

f(n) =f(\lceil \frac{n}{2}\rceil) + f(\lfloor \frac{n}{2} \rfloor) + c

這種函式的程式碼大概長這樣

f(l, r):
  if l+1 == r:
    return
  m = (l + r) / 2
  f(l, m)
  f(m, r)
小技巧: 讓 n = 2^k,就不用處理高斯符號
f(n) =2 \times f(\frac{n}{2}) + c

遞迴的複雜度分析

f(n) =f(\lceil \frac{n}{2}\rceil) + f(\lfloor \frac{n}{2} \rfloor) + c
=2 \times f(\frac{2^k}{2}) + c \\ = 2 \times f(2^{k-1}) + c \\ = 2 \times (2 \times f(2^{k-2})+ c) + c \\ = 2^2 f(2^{k-2}) + c (2^0 + 2^1)

遞迴的複雜度分析

f(n) =2 \times f(\frac{n}{2}) + c
=2 \times f(\frac{2^k}{2}) + c \\ = 2 \times f(2^{k-1}) + c \\ = 2 \times (2 \times f(2^{k-2})+ c) + c \\ = 2^2 f(2^{k-2}) + c (2^0 + 2^1) \\ = ... \\ = 2^k f(2^0) + c(2^0 + 2^1 + ... + 2^{k-1}) \\ = n + c \frac{2^k - 1}{2-1} \\ = n + cn - c\rightarrow O(n)
f(n) = 2 \times f(\frac{n}{2}) + c \cdot n
f(2^k) = 2^1 \times f(2^{k-1}) + c \cdot 2^k \\ = 2^1(2^1 \times f(2^{k-2}) + c \cdot 2^{k-1})+ c \cdot 2^k \\ = 2^2 f(2^{k-2}) + c2^k \cdot(1 + 1) \\ = ... \\ = 2^k f(0) + c2^k ( \underbrace{1 + 1 + 1 ... + 1}_{k}) \\ = n+ ckn \rightarrow O(n \log n)

遞迴的複雜度分析

f(n) = 2 \times f(\frac{n}{2}) + c \cdot n \log n
f(2^k) = 2^1 \times f(2^{k-1}) + ck \cdot 2^k \\ = 2^1(2^1 \times f(2^{k-2}) + c(k-1) \cdot 2^{k-1})+ ck \cdot 2^k \\ = 2^2 f(2^{k-2}) + c2^k \cdot(k + k-1) \\ = ... \\ = 2^k f(0) + c2^k (k + (k-1) + (k-2) ... +1) \\ = n+ cn \frac{k(k+1)}{2} \rightarrow O(n \log^2 n)

遞迴的複雜度分析

遞迴的複雜度分析 - 常見統整

f(n) =f(n-1) + c \rightarrow O(n)
f(n) =2 \times f(\frac{n}{2}) + c \rightarrow O(n)
f(n) = f(\frac{n}{2}) + c \rightarrow O(\log n)
f(n) = 2 \times f(\frac{n}{2}) + c \cdot n \log n \rightarrow O(n \log^2 n)
f(n) = 2 \times f(\frac{n}{2}) + c \cdot n \rightarrow O(n \log n)

合併排序

逆序數對

逆敘數對

給定一個數列 <A_n>,

請問有個對 (i, j) 滿足以下條件:
 

i < j , A_i > A_j
  1. 函式定義:
  2. 如何拆解:
  3. Base Case:

....?

這題老實說直接想是很難想出來的。

我們先來亂拆看看吧!

逆敘數對

1 5 2 7 0 9 3 8 4 6

A

f(n) = f(n-1) + A_n 跟前面數字的逆序數對

O(f(n)) = O(f(n-1) + n) \\ = O(n + n-1 + n-2 ... + 1) \\ = O(n^2)

跟裸做一樣😭

逆敘數對

1 5 2 7 0 9 3 8 4 6

A

如果每次切一半有辦法更好嗎...?

如果我們考慮左邊跟右邊,
那麼逆序數對會有三種可能:

  1. 都在左側的逆序數對
  2. 都在右側的逆序數對
  3. 在兩側的逆序數對

怎麼定義函式?

f(l, r) = f(l, m) + f(m, r) + 兩側逆對

逆敘數對

1 5 2 7 0 9 3 8 4 6

A

f(l, r) = f(l, m) + f(m, r) + 兩側逆對

暴力搜尋兩側逆對:

O(\frac{n^2}{4})

想這麼多還是跟裸做一樣😭

逆敘數對

f(l, r) = f(l, m) + f(m, r) + 兩側逆對

想這麼多還是跟裸做一樣😭

等等!如果兩側都是排序好的呢?

0 1 2 5 7
3 4 6 8 9

L

R

你想得出來怎麼更有效率的做嗎?

逆敘數對

f(l, r) = f(l, m) + f(m, r) + 兩側逆對

想這麼多還是跟裸做一樣😭

等等!如果兩側都是排序好的呢?

0 1 2 5 7
3 4 6 8 9

L

R

從 L 的每個元素看:

我們目標是要找到 R 有幾個元素比 L 還要小

二分搜! 

n \times O(\log n) = O(n \log n)

好像很有機會喔?

逆敘數對

f(l, r) = f(l, m) + f(m, r) + 兩側逆對

想這麼多還是跟裸做一樣😭

等等!如果兩側都是排序好的呢?

def f(l, r):

  • 算出沒有交叉的逆對 f(l, m) + f(m, r)

    •  
  • 排序 [l, m) 以及排序 [m, r)
    •  
  • 二分搜交叉的逆對
    •  
O(n \log n)
O(n \log n)
2 \times f(\frac n2)
f(n) = 2\times f(\frac n2) + O(n \log n) \\ \rightarrow O(n \log^2 n)

逆敘數對

f(l, r) = f(l, m) + f(m, r) + 兩側逆對

想這麼多還是跟裸做一樣😭

等等!如果兩側都是排序好的呢?

0 1 2 5 7
3 4 6 8 9

L

R

再等等!

好像有更好的算法!

你會發現指到的地方

永遠是遞增的!

→雙指針

逆敘數對

f(l, r) = f(l, m) + f(m, r) + 兩側逆對

等等!如果兩側都是排序好的呢?

0 1 2 5 7
3 4 6 8 9

L

R

→雙指針

l

r

r

r

r

l

l

l

l

+0

+0

+0

+2

+3

逆敘數對

f(l, r) = f(l, m) + f(m, r) + 兩側逆對

def f(l, r):

  • 算出沒有交叉的逆對 f(l, m) + f(m, r)

    •  
  • 排序 [l, m) 以及排序 [m, r)
    •  
  • 二分搜交叉的逆對
    •  
O(n \log n)
O(n \log n)
2 \times f(\frac n2)
O(n)

如果你做 Merge Sort 就不用排序

f(n) = 2\times f(\frac n2) + O(n) \\ \rightarrow O(n \log n)

Karatsuba 大數乘法

APCS 不會考,純介紹性質 :)

大數乘法

實作 A * B,

但數字很大。

小學直式乘法!

陣列 A

陣列 B

陣列 C

O(|A| \times |B|)

好慢...

大數乘法

實作 A * B,

但數字很大。

嘗試切一半!

A_L

A_R

B_R

B_L

\frac{n}{2}
A = A_L \times 10^{\frac n2} + A_R
B = B_L \times 10^{\frac n2} + B_R

A_R * B_R

A_L * B_R

A_R * B_L

A_L * B_L 

A \times B = A_LB_L \times 10^{n} + (A_LB_R + A_RB_L) \times 10^{\frac n2} + A_R B_R

大數乘法

實作 A * B,

但數字很大。

嘗試切一半!

A \times B = A_LB_L \times 10^{n} + (A_LB_R + A_RB_L) \times 10^{\frac n2} + A_R B_R
f(A, B) = A \times B
f(n) = 4f(\frac{n}{2}) + c \cdot n
\rightarrow O(n^2)

在做 A*B 的大數乘法的時候,

會需要做四次的一半的乘法

跟裸做一樣😭

f(n) = 4f(\frac{n}{2}) + c \cdot n \\ = 4 f(2^{k-1}) + c \cdot 2^k\\ = 4 (4 f(2^{k-2}) + c \cdot 2^{k-1} ) + c \cdot 2^k\\ = 4^2 f(2^{k-2}) + c (2^k + 2^{k+1}) \\ ... \\ =4^k f(2^0) + c(2^k + 2^{k+1} + ... + 2^{2k-1}) \\ = 4^k + c 2^{k} (2^0 + 2^1 + ...+2^{k-1}) \\ = n^2 + cn \frac{2^k - 1}{2 - 1} = n^2 +cn^2-cn \\ \rightarrow O(n^2)

大數乘法

實作 A * B,

但數字很大。

A \times B = A_LB_L \times 10^{n} + (A_LB_R + A_RB_L) \times 10^{\frac n2} + A_R B_R

A_L

A_R

B_R

B_L

A = A_L \times 10^{\frac n2} + A_R
B = B_L \times 10^{\frac n2} + B_R

A_R * B_R

A_L * B_R

A_R * B_L

A_L * B_L 

(A_L+A_R)(B_L + B_R) = A_LB_L + (A_LB_R + A_RB_L) + A_RB_R

四次乘法,一次加法→兩次加法,三次乘法,兩次減法

(A_LB_R + A_RB_L) = (A_L+A_R)(B_L + B_R) - A_LB_L -A_RB_R

大數乘法

實作 A * B,

但數字很大。

四次乘法,一次加法→兩次加法,三次乘法,兩次減法

  • f(A, B):
    • 左邊的: f(A_L, B_L)
    • 右邊的: f(A_R, B_R)
    • 中間的: f(A_L + A_R, B_L + B_R) - 左邊的 - 右邊的

A_R * B_R

A_L * B_R

A_R * B_L

A_L * B_L 

f(n) = 3f(\frac n2) + c\cdot n
= 3^2 f(2^{k-2}) + c (2^k + 3\times 2^{k-1}) \\ f(n) = 3f(\frac{n}{2}) + c \cdot n \\ = 3 f(2^{k-1}) + c \cdot 2^k\\ = 3 (3 f(2^{k-2}) + c \cdot 2^{k-1} ) + c \cdot 2^k\\ = 3^3 f(2^{k-3}) + c (2^k + 3\times 2^{k-1} + 3^2 \times 2^{k-2}) \\ = 3^3 f(2^{k-3}) + c2^k ((\frac{3}{2})^0 + (\frac{3}{2})^1 + (\frac{3}{2})^2) \\ = ...\\ = 3^k f(2^0) + c2^k(\frac{(\frac{3}{2})^k - 1}{\frac{3}{2} - 1}) \\ \simeq 3 \times 3^k \\ = 3 \times 3^{\log_2 n} =3\times n^{\log_2 3} \\ \simeq O(n^{1.585})

遞迴大結

Made with Slides.com