# 遞迴 {%hackmd theme-dark %} :::info Def: 遞迴是將大問題分成許多小問題來解決問題的方式,在Programming上,若一函式呼叫他自己,稱該函式為遞迴函式 ::: ## 二分搜 在**已排序**的一堆資料中以Divide & Conquer(分治)理念找到想要的答案 #### Iterative: ```cpp int l = 0, r = n-1; // 設定左右邊界 while(l<r){ int mid = l+(r-l)/2; // 設定中間點,把 (l+r)/2 改成這樣可以避免 overflow if(中間點的值比目標還大){ l = mid+1; // 答案在右邊,左半邊不用看了 }else{ r = mid; // 答案在左邊,右半邊不用看了 } } ``` #### Recursive: ```cpp int BinarySearch(int arr[], int target, int l, int r) { if (l == r) // 找到最接近的 return l; int mid = l + ((r - l) >> 1); // (l+r)/2 if (arr[mid] >= target) // target比mid小,答案在左半邊 return BinarySearch(arr, target, l, mid); else // target比mid大,答案在右半邊 return BinarySearch(arr, target, mid + 1, r); } ``` ## 遞迴使用時機 - 已知邊界(終止)條件 - 可拆分成更小但相同的問題 ## 階乘 #### Iterative: $$ f(n) = n! = \begin{cases} n \times (n-1) \times (n-2) \times \dots \times 1 & n>0 \\ 1 & n = 0 \end{cases} $$ #### Recursive: $$ f(n) = n! = \begin{cases} n \times f(n-1) & n>0 \\ 1 & n = 0 \end{cases} $$ ```cpp Integer Factorial(Integer n){ if(!n) return 1; return n * Factorial(n-1); } ``` ## Box trace 用於幫助了解遞迴運作方式,至少包含: - 輸入參數、區域變數 - 遞迴呼叫得到的值 - 自己要回傳的值 ```mermaid graph LR; func("F(n)") --> 3("n = 3<br> A:F(n-1) = 2<br> return 6") 3 --> 2("n = 2<br> A:F(n-1) = 1<br> return 2") 2 --> 1("n = 1<br> A:F(n-1) = 1<br> return 1") 1 --> 0("n = 0<br> return 1") style func fill: #ffffff, stroke: #ffffff ; ``` ## 字串反轉 ```cpp void WriteBackward(string s) { if (!s.empty()) { cout << s.back(); s.pop_back(); WriteBackward(s); } } ``` ```cpp void WriteBackward2(string s) { if (!s.empty()) { WriteBackward(s.substr(1)); cout << s[0]; } } ``` ## 找最大值 ```cpp int FindMax(int arr[], int l, int r) { if (l == r) return arr[l]; int mid = (l + r) >> 1; int left = FindMax(arr, l, mid); int right = FindMax(arr, mid + 1, r); return max(left, right); } ``` ### 另解(RMQ Problem) #### Segment tree ```cpp struct Node { Node *l, *r; int val; }; Node *build(Node *&root, int l, int r, int *arr) { if (root == NULL) { root = new Node(); } if (l == r) { root->val = arr[l]; return root; } int mid = (l + r) >> 1; root->l = build(root->l, l, mid, arr); root->r = build(root->r, mid + 1, r, arr); root->val = max(root->l->val, root->r->val); return root; } int query(Node *root, int l, int r, int ql, int qr) { if (l > qr || r < ql) return 0; if (l >= ql && r <= qr) return root->val; int mid = (l + r) >> 1; return max(query(root->l, l, mid, ql, qr), query(root->r, mid + 1, r, ql, qr)); } ``` ## 線性遞迴 ### 基底條件 * 至少要有一種基底條件 * 所有遞迴呼叫最終都必須趨向基底條件 * 處理基底條件不應使用遞迴 ### 遞迴次數 * 只呼叫一次 * 可決定如何呼叫,但只能呼叫一次 * 必須趨向基底條件 ### 找第K小值 #### Quick Select: ```cpp int FindKthLargest(int *arr, int k, int l, int r) { int pivot = arr[r]; int i = l - 1; for (int j = l; j < r; j++) { if (arr[j] > pivot) { i++; swap(arr[i], arr[j]); } } swap(arr[i + 1], arr[r]); if (i + 1 == k - 1) return arr[i + 1]; else if (i + 1 > k - 1) return FindKthLargest(arr, k, l, i); else return FindKthLargest(arr, k, i + 2, r); } ``` #### 一般Segment Tree: 葉節點紀錄對應數組(node[i] = arr[i]) #### 權值Segment Tree: 葉節點紀錄數組去重後第i小的出現次數(node[i] = Count(arr[i])) ```cpp int query(int l, int r, int id, int k) { if(l == r) { return l; } int mid = (l + r) >> 1; if(k <= cnt[id << 1]) query(l, mid, id << 1, k); else query(mid + 1, r, id << 1 | 1, k - cnt[id << 1]); } ``` ### 反轉陣列 ```cpp void ReverseArray(int *arr, int l, int r){ if(l < r){ swap(arr[l], arr[r]); ReverseArray(arr, l+1, r-1); } } ``` ### 河內塔 ```cpp void Hanoi(int count, int source, int destination, int spare){ if(count == 1){ cout << "Move a disk from " << source << " to " << destination << '\n'; return; } Hanoi(count-1, source, spare, destination); Hanoi(1, source, destination, spare); Hanoi(count-1, spare, destination, source); } ``` ## 二元遞迴 * 只要不是基底條件就呼叫兩次遞迴 ### 畫尺的標線 ```cpp void drawRuler(int n, int len){ drawOneTick(len, 0); for(int i=1;i<=n;++i){ drawTicks(len-1); drawOneTick(len, 1); } } void drawTicks(int len){ if(len>0){ drawTicks(len-1); drawOneTick(len, -1); drawTicks(len-1); } } void drawOneTick(int len, int tag){ for(int i=0;i<len;++i) cout << "-"; if(tag>=0) cout << " " << tag; cout << '\n'; } ``` ### 費波那契數列 $O(2^n)$ ```cpp int fib(int n){ if(n<=2) return 1; return fib(n-1) + fib(n-2); } ``` #### 線性遞迴版 $O(n)$ ```cpp pair<int,int> fib(int k){ // return {fib(k), fib(k-1)} if(k==1) return {k, 0}; pair<int,int> r = fib(k-1); return {r.first + r.second, r.first}; } ``` ### 快速冪 ```cpp #define ll long long ll fast_pow(ll a, ll n){ if(!n) return 1; if(n&1) return fast_pow(a,n-1) * a; else { ll r = fast_pow(a,n/2); return r * r; } } ll fast_pow2(ll a, ll n){ ll ret = 1; while(n){ if(n&1) ret *= a; n >>= 1; a *= a; } return ret; } ``` ## 阿克曼函數 $$ Acker(m, n) = \begin{cases} n+1 & m = 0 \\ Acker(m-1,1) & n = 0 \\ Acker(m-1,Acker(m,n-1)) & otherwise \end{cases} $$ ## 組合計數 $C^n_k = C^{n-1}_{k-1} + C^{n-1}_k$