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 - C++

stack
警戒線

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

C++ 黑魔法 - 強制調用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>

解決Stack Overflow - Python

  • sys.setrecursionlimit
  • Tail Recursion
  • DP - Call small case first

輾轉相除法

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 暴力搜尋

int f(vector<int> &cur, int n){
    if (cur.size() == n) {
        for (auto x : cur)
            printf("%d", x);
        puts("");
    }
    else {
        for (int nxt : {0, 1}) {
            cur.push_back(nxt);
            f(cur, n);
            cur.pop_back();
        }
    }
}

// Call
vector<int> V;
f(V, 3);
def f(L, n):
    if len(L) == n:
        print(L)
    else:
        for nxt in [0, 1]:
            L.append(nxt)
            f(L, n)
            L.pop()

# Call
f([], 3)

C++

Python

2^n 暴力搜尋

n! 暴力搜尋

int f(vector<int> &cur, int n, vector<int> &visit){
    if (cur.size() == n) {
        for (auto x : cur)
            printf("%d", x);
        puts("");
    }
    else {
        for (int nxt=0; nxt<n; nxt++) {
            if (!visit[nxt]) {
                cur.push_back(nxt);
                visit[nxt] = 1;
                f(cur, n, visit);
                cur.pop_back();
                visit[nxt] = 0;
            }
        }
    }
}
// Call
int n = 3;
vector<int> V, visit(3, 0);
f(V, 3, visit);
def f(L, n, visit):
    if len(L) == n:
        print(L)
    else:
        for nxt in range(n):
            if nxt not in visit:
                L.append(nxt)
                visit.add(nxt)
                f(L, n, visit)
                L.pop()
                visit.remove(nxt)
# Call
f([], 3, set())

C++

Python

n! 暴力搜尋

int n = 3;
vector<int> V(n);
for (int i=0; i<n; i++)
	V[i] = i;

do {
	for (auto v : V) {
    	printf("%d", v);
    }puts("");
} while(next_permutation(V.begin(), V.end()));
n = 3
L = list(range(n))

from itertools import permutations
for p in permutations(L):
    print(p)

C++

Python

n! 暴力搜尋

int n = 3;
vector<int> V(n);
for (int i=0; i<n; i++)
	V[i] = i;

do {
	for (auto v : V) {
    	printf("%d", v);
    }puts("");
} while(next_permutation(V.begin(), V.end()));
n = 3
L = list(range(n))

from itertools import permutations
for p in permutations(L):
    print(p)

C++

Python

next_permutation 的原理

分治法

Divide & Conquer (D&C)

分治法 Divide & Conquer

分治法 Divide & Conquer

Divide

Conquer

Combine

分割:
將大問題切割成小問題

擊破:
將各個小問題解決掉

整合:
將各個小問題的答案整合起來

其實你也不用想太多,把它當遞迴想就可以了。

分治法 Divide & Conquer

快速冪

Fast Pow


int f(vector<int> &cur, int n, vector<int> &visit){
    if (cur.size() == n) {
        for (auto x : cur)
            printf("%d", x);
        puts("");
    }
    else {
        for (int nxt=0; nxt<n; nxt++) {
            if (!visit[nxt]) {
                cur.push_back(nxt);
                visit[nxt] = 1;
                f(cur, n, visit);
                cur.pop_back();
                visit[nxt] = 0;
            }
        }
    }
}
// Call
int n = 3;
vector<int> V, visit(3, 0);
f(V, 3, visit);
def f(L, n, visit):
    if len(L) == n:
        print(L)
    else:
        for nxt in range(n):
            if nxt not in visit:
                L.append(nxt)
                visit.add(nxt)
                f(L, n, visit)
                L.pop()
                visit.remove(nxt)
# Call
f([], 3, set())

C++

Python

快速冪

河內塔

Tower of Hanoi

什麼是河內塔?

Tower of Hanoi

  • 初始: 有大小1~n的圓盤,都在第一個塔。
  • 每次可以移動一個圓盤,移動後只能讓比較小的在上面。
  • 問題: 如何最少次大小1~n的圓盤都搬到最後一層?

n=4的河內塔

想得出來要怎麼做嗎? 玩玩看!

n=4的河內塔

連猩猩都會,你不會嗎(?)

n=4 的河內塔

n = 4 的解法

河內塔 (zj a227)

n

n-2

1

...

n-1

  1. 函式定義:

f(n) = 把 1~n 的圓盤從A移到C

A

B

C

好像不夠詳細... 拆不出來

f(n, s, t) = 把 1~n 的圓盤從 s 移到 t

例如 f(5, A, C) 表示把 1~n 的圓盤從 A 移到 C

來想想看這樣可不可以拆吧!

n

n-2

1

...

n-1

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

A

B

C

f(n, s, t) = 把 1~n 的圓盤從 s 移到 t

n

n

A

B

C

n-2

1

...

n-1

只有在你把 1~n-1的盤子搬到中間,你才可以把第n個圓盤搬到你想要的位置 

河內塔 (zj a227)

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

f(n, s, t) = 把 1~n 的圓盤從 s 移到 t

只有在你把 1~n-1的盤子搬到中間,你才可以把第n個圓盤搬到你想要的位置 

  • 如果你想把1~n的塔從A放到C?
    • 先把1~n-1的塔從A搬到B。
    • 把第n圓盤從A放到C。
    • 再把1~n-1的塔從B搬回C。

n

n-2

1

...

n-1

A

B

C

n

n-2

1

...

n-1

n-2

1

...

n-1

f(n, A, C)

f(n-1, A, B)

f(n-1, B, C)

<print>

f(n, s, t, o)

f(n-1, s, o, t)

<print>

f(n-1, o, t, s)

河內塔 (zj a227)

  1. 函式定義:
     
  2. 如何拆解:
     
  3. Base Case:n = 1 的時候輸出 / n = 0 的時候不做事

f(n, s, t, o) = 把 1~n 的圓盤從 s 移到 t

河內塔 (zj a227)

(source, target, other)

f(n, s, t, o)

f(n-1, s, o, t)

操作: 將 n 從 s 移到 t

f(n-1, o, t, s)

\{
void rec(int n, char from, char to, char other) {
  if (n == 0) return ;
  rec(n-1, from, other, to);
  printf("Move ring %d from %c to %c\n", n, from, to);
  rec(n-1, other, to, from);
}
def rec(n, source, target, other):
  if n == 0: return 
  rec(n-1, source, other, target)
  print(f"Move ring {n} from {source} to {target}\n")
  rec(n-1, other, target, source)

C++

Python

遞迴的複雜度分析

!數學警告!

遞迴的複雜度分析

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

不覺得遞迴的時間複雜度好像看不出來嗎?

來上點難度吧!

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)) + cn \\ = 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 \log_2 n \rightarrow O(\log_2 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) =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} \\ = n + cn\frac{k^2}{2} + cn\frac{k}{2} \\ \rightarrow O(n \cdot k^2) \rightarrow O(n \log n \log n) \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)

合併排序

Merge Sort

給定兩個已排序數列 <A_n>, <B_m>,

請把它合併成一個排序好的序列。

合併排序 Merge Sort - Merge

在講排序之前,我們先來思考這個問題:

A+B

1

2

3

4

5

6

7

8

1

3

4

6

A

2

5

7

8

B

給定兩個已排序數列 <A_n>, <B_m>,

請把它合併成一個排序好的序列。

合併排序 Merge Sort - Merge

在講排序之前,我們先來思考這個問題:

1

3

4

6

A

2

5

7

8

B

小樣!直接 call sort 不就好了嗎?

O(n \log n)

有更好複雜度的做法!想想看吧!

給定兩個已排序數列 <A_n>, <B_m>,

請把它合併成一個排序好的序列。

合併排序 Merge Sort - Merge

在講排序之前,我們先來思考這個問題:

1

3

4

6

A

2

5

7

8

B

A+B

1

2

3

4

5

6

7

8

 

 

 

 

 

 

 

 

看 A,B 最前面的兩個數字,誰最小誰就表示整個最小。

給定兩個已排序數列 <A_n>, <B_m>,

請把它合併成一個排序好的序列。

合併排序 Merge Sort - Merge

在講排序之前,我們先來思考這個問題:

vector<int> merge(vector<int> &A, int Al, int Ar,
                  vector<int> &B, int Bl, int Br){
  vector<int> C;
  while (Al < Ar || Bl < Br) {
    if (Bl == Br || Al < Ar && A[Al] < B[Bl])
      C.push_back(A[Al++]);
    else
      C.push_back(B[Bl++]);
  }
  return C;
}
from collections import deque
def merge(A, B):
  A, B = deque(A), deque(B)
  C = []
  while A or B:
    if not B or A and A[0] < B[0]:
      C.append(A.popleft())
    else:
      C.append(B.popleft())
  return C

C++

Python

看 A,B 最前面的兩個數字,誰最小誰就表示整個最小。

用兩個指針維護

用兩個deque維護

合併排序 Merge Sort

  1. 函式定義:

f(A) = 將陣列 A 排序。

這樣好像...沒法拆?

如果硬要這樣遞迴,
那麼你需要「真的」拆這個陣列。

讓我們來想想看這要怎麼排序吧!

Merge Sort 的核心概念

利用遞迴 + 每次將問題拆一半來排序。

合併排序 Merge Sort

  1. 函式定義:

f(A) = 將陣列 A 排序。

Merge Sort 的核心概念

利用遞迴 + 每次將問題拆一半來排序。

為了有效率處理,

我們通常會用兩個數字 [l, r)

表示處理 [l, r) 區間

[l, r) = 左閉右開,

包含左界不包含右界

f(A, l, r) = 將陣列的 A[l:r] 排序。

讓我們來想想看這要怎麼排序吧!

f(A, 3, 7) = 將陣列的 A[3:7] 排序。

A[l] ~ A[r-1] 

A[3] ~ A[6] 

合併排序 Merge Sort

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

f(A, l, r) = 將陣列的 A[l:r] 排序。

拆一半排序?就是先各自排序左半邊右半邊

Merge Sort 的核心概念

利用遞迴 + 每次將問題拆一半來排序。

 

A

A_l
...
A_{r-1}
l
r
m
...
A_m

希望 m-l 會等於 r-m,這樣才可以切一半

m = l + \overbrace {\frac{r-l}{2}}^{要排序的數量} = \frac{r+l}{2}

中點公式(?) :

=\frac{r+l}{2}

合併排序 Merge Sort

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

f(A, l, r) = 將陣列的 A[l:r] 排序。

拆一半排序?就是先各自排序左半邊右半邊

Merge Sort 的核心概念

利用遞迴 + 每次將問題拆一半來排序。

 

A

A_l
...
A_{r-1}
l
m
...
A_m
=\frac{r+l}{2}

排序 A[2, 3, 4, 5] -> [l=2, r=6), m=4 -> [2, 4) + [4, 6) 

排序 A[2, 3, 4] -> [l=2, r=5), m=3 -> [2, 3) + [3, 5) 

舉例來說:

r

合併排序 Merge Sort

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

f(A, l, r) = 將陣列的 A[l:r] 排序。

f(A, l, r) 要做的事情

  1. 排序左半邊: f(A, l, m)
  2. 排序右半邊: f(A, m, r)
  3. 想辦法排序 A[l:r]。
    • 兩邊都是排序好的要排序? 好像講過!

Merge Sort 的核心概念

利用遞迴 + 每次將問題拆一半來排序。

拆一半排序?就是先各自排序左半邊右半邊

(你呼叫 f(A, l, r) 就會無窮遞迴)

只剩一個元素的時候:

l+1 = r

合併排序 Merge Sort - 實作

f(A, l, r): Merge sort A[l:r]

  1. 排序左半邊: f(A, l, m)
  2. 排序右半邊: f(A, m, r)
  3. Merge 左半邊跟右半邊
vector<int> merge(vector<int> &A, int Al, int Ar,
                  vector<int> &B, int Bl, int Br){
  vector<int> C;
  while (Al < Ar || Bl < Br) {
    if (Al == Ar || Bl < Br && B[Bl] < A[Al])
      C.push_back(B[Bl++]);
    else
      C.push_back(A[Al++]);
  }
  return C;
}
from collections import deque
def merge(A, B):
  A, B = deque(A), deque(B)
  C = []
  while A or B:
    if not B or A and A[0] < B[0]:
      C.append(A.popleft())
    else:
      C.append(B.popleft())
  return C

C++

Python

def merge_sort(A, l, r):
  if l+1 == r: return
  m = (l+r) // 2
  merge_sort(A, l, m)
  merge_sort(A, m, r)
  A[l:r] = merge(A[l:m], A[m:r])
void merge_sort(vector<int> &A, int l, int r) {
    if (l+1 == r) return;
    int m = (l+r) / 2;
    merge_sort(A, l, m);
    merge_sort(A, m, r);
    auto tmp = merge(A, l, m, A, m, r);
    copy(tmp.begin(), tmp.end(), &A[l]);
}

(C++其實有內建的Merge)

合併排序 Merge Sort - 遞迴樹狀圖

f(A, l, r): Merge sort A[l:r]

  1. 排序左半邊: f(A, l, m)
  2. 排序右半邊: f(A, m, r)
  3. Merge 左半邊跟右半邊

[0, 9)

目標:排序9個元素

m = 4

[0, 4)

m = 2

[4, 9)

m = 6

[0, 2)

[0, 1)

[1, 2)

[2, 4)

[2, 3)

[3, 4)

[6, 9)

[4, 5)

[5, 6)

[4, 6)

[6, 7)

[7, 9)

m = 8

[7, 8)

[8, 9)

m = 2

m = 3

m = 5

m = 7

合併排序 Merge Sort - 視覺化

看一下 Merge Sort 的視覺化吧!

合併排序 Merge Sort - 複雜度

f(A, l, r): Merge sort A[l:r]

  1. 排序左半邊: f(A, l, m)
  2. 排序右半邊: f(A, m, r)
  3. Merge 左半邊跟右半邊
T(n)
T(\lfloor \frac n2 \rfloor)
T(\lceil \frac n2 \rceil)
O(n)
T(n) = 2 T(\frac n2) + O(n) \rightarrow O(n \log n)

如果你很懶得 Merge,直接 Call Sort

O(n \log n)
T(n) = 2 T(\frac n2) + O(n \log n) \rightarrow O(n \log^2 n)

不過你都Call Sort 了,直接 Sort 整個陣列不就好了(?)

練習題

  1. 拆成兩個排序的 Merge Sort 是 n log n。
    如果拆成k個呢?
    • 現在有k個排序好的序列,要怎麼merge?
    • 整體複雜度會變得怎麼樣?
  2. Quick Sort 的時間複雜度?

f(A, l, r): Quick Sort A[l:r]

  1. 隨機在 A[l:r] 中找一個數字 Pivot。
  2. 把 Pivot 小的放左邊,
    把 Pivot 放中間, (假設 Pivot 在 m 的位置上)
    然後把 Pivot 大的放右邊。
  3. f(A, l, m)
  4. f(A, m, r)

逆序數對

Inversion Pair

給定一個數列 <A_n>,

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

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

....?

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

我們先來亂拆看看吧!

逆序數對 Inversion

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)

跟裸做一樣😭

逆序數對 Inversion

1 5 2 7 0 9 3 8 4 6

A

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

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

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

怎麼定義函式?

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

逆序數對 Inversion

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}) = O(n^2)

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

逆序數對 Inversion

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

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

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

0 1 2 5 7
3 4 6 8 9

L

R

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

逆序數對 Inversion

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)

好像很有機會喔?

逆序數對 Inversion

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)

逆序數對 Inversion

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

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

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

0 1 2 5 7
3 4 6 8 9

L

R

再等等!

好像有更好的算法!

你會發現指到的地方

永遠是遞增的!

→雙指針

逆序數對 Inversion

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

逆序數對 Inversion

=5

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)

逆序數對 Inversion

逆序數對 - 變形

幼稚園 (TOI 2020/08 - 8 新手賽)

幼稚園 (TOI 2020/08 - 8 新手賽)

給定一個數列 A,

你可以交換A中任兩相鄰數。

請問最少交換幾次才可使數列滿足
「遞增」或「遞減」或「先遞增再遞減」?

好像有點困難?

我們先換成簡單版的題目:

(弱化版原題)
給定一個數列 A,

你可以交換A中任兩相鄰數。

請問最少交換幾次才可使數列「遞增」?

(非嚴格)

幼稚園 (TOI 2020/08 - 8 新手賽)

(弱化版原題) 給定一個數列 A,

你可以交換A中任兩相鄰數。

請問最少交換幾次才可使數列「遞增」?

2

3

1

考量三個數字

交換一個相鄰的逆對

2

3

1

逆序數對會 - 1

幼稚園 (TOI 2020/08 - 8 新手賽)

(弱化版原題) 給定一個數列 A,

你可以交換A中任兩相鄰數。

請問最少交換幾次才可使數列「遞增」?

交換一個相鄰的逆對

逆序數對會 - 1

遞增數列逆序數對是 0

最少交換次數 =
A的逆序數對

這也就是說:如果你對 A 做泡沫排序,

交換的次數 = A 的逆序數對

裸做: O(n^2)
DC: O(n \log n)

如果使數列「遞減」呢?

幼稚園 (TOI 2020/08 - 8 新手賽)

給定一個數列 A,你可以交換A中任兩相鄰數。

問最少交換幾次才可使數列滿足
「遞增」或「遞減」或「先遞增再遞減」?

考慮 A 裡面的一個數字 x,

那麼 x 的最終歸屬只有兩種可能。

 

A

x

x

x

將 x 往右所需交換次數:x 往右看的順序數對

將 x 往左所需交換次數:x 往左看的逆序數對

\}\min
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;

void rec(vector<int> &inv, vector<pair<int, int>> &A, int l, int r) {
    if (l+1 == r) return;
    int m = (l + r) / 2;
    rec(inv, A, l, m);
    rec(inv, A, m, r);
    int pr = m;
    for (int pl = l; pl < m; pl++) {
        while(pr != r && A[pl].first >= A[pr].first) pr++;
        inv[A[pl].second] += r-pr;
    }
    vector<pair<int, int>> tmp(r-l);
    merge(&A[l], &A[m], &A[m], &A[r], tmp.begin());
    copy(tmp.begin(), tmp.end(), &A[l]);
}
int main() {
    int n, x;
    scanf("%d", &n);
    vector<pair<int, int>> A;
    for (int i=0; i<n; i++) {
        scanf("%d", &x);
        A.push_back({x, i});
    }
    auto B = A;
    reverse(B.begin(), B.end());
    vector<int> inv_f(n), inv_b(n);
    rec(inv_f, A, 0, n);
    rec(inv_b, B, 0, n);
    int ans = 0;
    for (int i=0; i<n; i++)
        ans += min(inv_f[i], inv_b[i]);
    printf("%d\n", ans);
    return 0;
}

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}) + O(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
f(n) = 3f(\frac n2) + c\cdot n \\ = 3f(2^{k-1}) + c\cdot 2^k \\ = 3 (3 f(2^{k-2}) + c \cdot 2^{k-1} ) + c \cdot 2^k\\ = 3^2 f(2^{k-2}) + c (2^k + 3\times 2^{k-1}) \\ = 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 (1 + 2c) \times 3^k \\ \simeq 2c \times 3^{\log_2 n} =2c \times n^{\log_2 3} \\ \rightarrow O(n^{\log_2 3}) \simeq O(n^{1.585})

大數乘法 - 變形

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

我找不到好題目 :(

預計會是多項式乘法 + DP

分治小結

Title Text

  • 矩陣乘法
  • O(n) 中位數
  •  

逆對: 線段樹

https://tioj.ck.tp.edu.tw/problems/1232: 題目要求複雜度很低 (r, n le 10),但你可以做得很漂亮

遞迴大結

Recursion 2024

By Arvin Liu

Recursion 2024

Recursion

  • 1,613