├── CNAME ├── README.md ├── _posts ├── lessons.md └── note │ ├── advanced-ds │ ├── fenwick-tree.md │ ├── segment-tree.md │ └── sparse-table.md │ ├── basic-ds │ ├── array-like-ds.md │ └── linked-list.md │ ├── binary │ ├── bitwise-operator.md │ ├── int-and-float.md │ └── radix.md │ ├── dp │ └── aliens.md │ ├── geometry │ ├── convex-hull.md │ ├── vector-application.md │ └── vector.md │ ├── math │ ├── gcd.md │ ├── pow.md │ └── prime-and-factor.md │ ├── misc │ ├── complexity.md │ ├── io-optimize.md │ ├── iostream.md │ ├── prefix-sum.md │ ├── preprocessor.md │ └── stringstream.md │ ├── sqrt │ ├── mo-algorithm.md │ └── sqrt-decomposition.md │ └── string │ └── longest-palindromic-substring.md ├── home.md ├── images ├── advanced-ds │ ├── fenwick-tree │ │ ├── bit.png │ │ └── bit.tex │ ├── segment-tree │ │ ├── segtree.png │ │ ├── segtree.py │ │ └── segtree.tex │ └── sparse-table │ │ └── sparse-table.png ├── dp │ └── aliens │ │ └── animation.gif ├── geometry │ ├── convex-hull │ │ ├── build.png │ │ ├── build2.png │ │ ├── convex-hull.png │ │ ├── rotating-calipers1.png │ │ ├── rotating-calipers2.png │ │ ├── rotating-calipers3.png │ │ ├── rotating-calipers4.png │ │ └── sweep-line.png │ ├── vector-application │ │ ├── area.png │ │ ├── collinearity.png │ │ ├── inline.png │ │ ├── inpoly1.png │ │ ├── inpoly2.png │ │ ├── intersect.png │ │ └── intersection.png │ └── vector │ │ ├── cross.png │ │ ├── cross1.png │ │ ├── cross2.png │ │ ├── dot.png │ │ ├── dot1.png │ │ ├── dot2.png │ │ ├── minus.png │ │ ├── multiply.png │ │ ├── plus.png │ │ └── vector.png └── sqrt │ └── sqrt-decomposition │ └── sqrt-decomposition.png └── menu.md /CNAME: -------------------------------------------------------------------------------- 1 | cp.wiwiho.me -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # WiwiHo 的競程筆記 2 | 3 | https://cp.wiwiho.me/ 4 | 5 | - Framework: [Hexo](https://hexo.io/) 6 | - Theme: [Book](https://github.com/kaiiiz/hexo-theme-book) 7 | -------------------------------------------------------------------------------- /_posts/lessons.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: 我教過的課 3 | --- 4 | # 我教過的課 5 | 6 | **我不會特別去修以前投影片或講義上的錯,有很多比較久以前上的課有不少錯誤,請注意不要完全相信以下東西的內容。** 7 | 8 | ## 2019 師大附中北市賽培訓 9 | - [計算幾何](https://hackmd.io/@wiwiho/SJJJXCuKr/) 10 | - [進階資料結構與莫隊算法](https://hackmd.io/@wiwiho/SJPF5GrqB) 11 | 12 | ## 108 下學期師大附中電算社社課 13 | - 競賽基本知識:[講義](https://drive.google.com/file/d/1zKU1k9WOu37fmAHyaePmZEI_jwDM81s4/view?usp=sharing) ⦁ [練習答案](https://drive.google.com/file/d/1sDuIcpVw-L1yX6pAvRcY7DMBp9GRtb_s/view?usp=sharing) ⦁ [簡報](https://hackmd.io/@wiwiho/crc1082algo01) 14 | - 搜尋與排序:[講義](https://drive.google.com/open?id=1j_4irJ5BqZwv3XJ5rAQ_ZZm8gS0_WQq2) ⦁ [練習答案](https://drive.google.com/open?id=1aAV-l5eUdbvAdWIjmjUKtI7MEDQuJxZ2) ⦁ [簡報](https://hackmd.io/@wiwiho/crc1082algo02) 15 | - 枚舉與 Greedy:[講義](https://drive.google.com/file/d/16Vkr0CEnEle5QeqGRqr2bqPN2rZTPupZ/view?usp=sharing) ⦁ [簡報](https://hackmd.io/@wiwiho/crc1082algo03) 16 | - 分治法與動態規劃:[講義](https://drive.google.com/file/d/1SxuBzDDbgmpo2j2PyXEQ2Oe8BgVqM_DH/view?usp=sharing) ⦁ [簡報](https://hackmd.io/@wiwiho/crc1082algo04) 17 | - 圖論 I:[講義](https://drive.google.com/file/d/1ULVU0j--X5j_9WDWDchr2rnfosfb2hw-/view?usp=sharing) ⦁ [簡報](https://hackmd.io/@wiwiho/crc1082algo05) 18 | - 圖論 II:[講義](https://drive.google.com/file/d/1RlfGR9POKmnsloUOh8jMxZS9rxnDr_nE/view?usp=sharing) ⦁ [簡報](https://hackmd.io/@wiwiho/crc1082algo06) 19 | 20 | ## 2020 師大附中暑期培訓 21 | - 數學:[講義](https://drive.google.com/file/d/1xXk20zUGjOW5w06peq-lTYuqHWek7qDe/view?usp=sharing) ⦁ [簡報](https://drive.google.com/file/d/1X74agYn6VgjSCQRgyg9HDxbfx_sQ14zZ/view?usp=sharing) ⦁ [練習](https://hackmd.io/@wiwiho/HSNU2020summer-math) 22 | - 進階圖論:[講義](https://drive.google.com/file/d/1oc8ABGO5HGYkgOOyu5fNtAqYZJPjG4ez/view?usp=sharing) ⦁ [簡報](https://drive.google.com/file/d/1N4q3zE--x3F5J82zfhTV9yY-yB8YTVjV/view?usp=sharing) ⦁ [練習](https://hackmd.io/@wiwiho/HSNU2020summer-advgraph) 23 | - 計算幾何:[講義](https://drive.google.com/file/d/1D0ot4yBTYOOMpw2n28K6gkB1_52A57TK/view?usp=sharing) ⦁ [簡報](https://drive.google.com/file/d/1vEVHiDmq7GqK2o5zymBYeXA_ikPqObIa/view?usp=sharing) ⦁ [練習](https://hackmd.io/@wiwiho/HSNU2020summer-geometry) 24 | - 字串:[講義](https://drive.google.com/file/d/1tS7L3wzmiTr378iqfaSbfvoKbXHlfAjn/view?usp=sharing) 25 | - 進階資料結構:[講義](https://drive.google.com/file/d/1q2I8a9bznxdF6_JynzxeDvNC3abKj6qc/view?usp=sharing) 26 | 27 | ## 2020 師大附中北市賽培訓 28 | - 數學:[講義](https://drive.google.com/file/d/1UvWuy2TrVI64HowzogGRc9wZRjrjvXqx/view?usp=sharing) ⦁ [簡報](https://drive.google.com/file/d/1cfK5-c83It0nG0KPZRpDLZAPKO9IAxwv/view?usp=sharing) 29 | - 計算幾何:[講義](https://drive.google.com/file/d/1m22T3llYm2DpTYcif54OwvXHJYo-CsLT/view?usp=sharing) ⦁ [簡報](https://drive.google.com/file/d/1nKskqTWaTC6oEaW57s5wcraQAeKJWL0x/view?usp=sharing) 30 | - 字串:[講義](https://drive.google.com/file/d/1xnc89nQbvL2wcdHT3OUP2alRB0aHZP6K/view?usp=sharing) ⦁ [簡報](https://drive.google.com/file/d/1tn2FLrbTSDhPyDhA7jgHhdkzPVRNnvmb/view?usp=sharing) 31 | - 進階資料結構:[講義](https://drive.google.com/file/d/1XT5vANL4HWSprjnR4FZ9qkoMFSEzH-vE/view?usp=sharing) 32 | 33 | ## 2021 師大附中暑期培訓 34 | - 進階圖論:[簡報](https://drive.google.com/file/d/1v2HFy6gVBVp-kC2ZV1iEwMFRIAPthEl3/view?usp=sharing) 35 | - 數學:[簡報](https://drive.google.com/file/d/1Uycbou4RC201-DlQ-2s4foBxuTzVkuCO/view?usp=sharing) 36 | - 計算幾何:[簡報](https://drive.google.com/file/d/1tWumEKJCgP05Hs1Ufk1B0r6HsH28VIpi/view?usp=sharing) 37 | - 進階資料結構:[簡報](https://drive.google.com/file/d/1Dz-1vnsIGpa5zPi85xxtbkL9URMMRr6M/view?usp=sharing) 38 | 39 | ## 2021 師大附中北市賽培訓 40 | - 比賽策略:[簡報](https://drive.google.com/file/d/1ENf2A7jyENqMrDaaOhs5tR1H3bvSwG2w/view?usp=sharing)(內含分治法) 41 | - 基礎演算法:[簡報](https://drive.google.com/file/d/1Oaj5MKubLd6zaieRZ9C5QBEmM-PlmRrt/view?usp=sharing) 42 | - 圖論:[簡報](https://drive.google.com/file/d/1a1mgK8KFJWNoXATHwi3E6ceStn22QmZl/view?usp=sharing) 43 | - 數學:[簡報](https://drive.google.com/file/d/10mXQwUZ-7OQasV3DFYGN_VW8R47QNttu/view?usp=sharing) 44 | - 資料結構:[簡報](https://drive.google.com/file/d/1VuKVgbQW8He8jzJV8bcDxHYK3edJQlD-/view?usp=sharing) 45 | -------------------------------------------------------------------------------- /_posts/note/advanced-ds/fenwick-tree.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: 樹狀數組 3 | --- 4 | # 樹狀數組 5 | 6 | > 有一個序列 $A$,一開始都是 0,有 $Q$ 筆操作,每筆操作都是以下其中一種: 7 | > 8 | > 1. 把 $A_i$ 加上 $v$ 9 | > 2. 詢問 $[1,i]$ 區間和 10 | 11 | 雖然帶修改前綴和也可以用線段樹來做,不過有一個更適合用在前綴和的可修改結構——樹狀數組/二元索引樹(Binary Indexed Tree,簡稱 BIT)。BIT 的常數小,實作也更簡單。 12 | 13 | BIT 的空間只要 $n$,比線段樹的 $4n$ 小,只需要用一個一維陣列就可以儲存一棵 BIT 了。 14 | 15 | BIT 的每一個位置表示的值根據它的位置編號而定,$BIT_i$ 定義為:以 $A_i$ 為結尾,長度為 $lowbit(i)$ 的區間和,也就是 $[i-lowbit(i)+1,i]$ 這個範圍的和,$lowbit(i)$ 指的是把 $i$ 轉成二進位後,把最低位的 $1$ 以外的 $1$ 都變成 $0$,例如 $$lowbit((56)_{10})=lowbit((111000)_2)=(1000)_2=(8)_{10}$$ 16 | 17 | 所以說,$BIT_{56}$ 就是 $[49,56]$ 這個區間的和。 18 | 19 | 要得到 $lowbit(x)$ 的方法是 `x&-x`,也就是把它和它的相反數取 bitwise and,因為 `-x` 會是 `x` 的補數加 $1$,`x` 在最低位的是 $1$ 的位元會變為 $0$,右邊的 $0$ 在取補數後會變為 $1$,加上 $1$ 之後,這些又會變為 $0$,而本來是最低位的 $1$ 的地方也會變為 $1$,但在左邊的每一個位元都會和本來不同,因為取 bitwise and 後,就可以得到 $lowbit(x)$。 20 | 21 | 節點 $0$ 代表的是一個空區間,而節點 $1$ 的區間只包含 $[1,1]$,因此,序列的索引應該要從 $1$ 開始。 22 | 23 | 那麼 BIT 畫成樹是什麼樣子?其實它並不是二元樹,binary 指的是二進位。 24 | 25 | 26 | 27 | - 節點 $i$ 的父節點是 $i-lowbit(i)$。 28 | - 節點 $i$ 的深度與 $i$ 是 $1$ 的位元數相同。 29 | - 節點 $i$ 的右兄弟節點(如果有的話)是 $i+lowbit(i)$。 30 | 31 | 節點 $0$ 就是根節點,然後按照上述規則,可以畫出一棵樹。仔細觀察一下,會發現每個節點代表的區間有這些性質: 32 | 33 | - 節點 $i$ 的父節點是 $p$,則節點 $i$ 的區間是 $[p+1,i]$,因為 $p=i-lowbit(i)$。 34 | - 節點 $i$ 的區間包含它所有左兄弟節點的區間,因為它們的父節點都一樣,所以它們區間的起點都一樣,但 $i$ 的結束點較後面。 35 | - 節點 $i$ 的區間包含所有左兄弟節點 $j$ 的子孫節點的區間,因為 $j$ 子孫節點區間必在 $j$ 和 $i-1$ 之間,而 $i$ 的區間肯定包含這段。 36 | 37 | 在知道這些性質後,就可以來討論怎麼處理詢問了。 38 | 39 | ## 單點修改 40 | 41 | 如果現在要修改一個位置 $pos$,那要找到所有區間包含這個位置的節點,第一個這樣的節點是 $pos$,接下來,它的所有右兄弟節點都包含這裡,用 $pos+lowbit(pos)$ 可以得到右兄弟節點,而其父節點的右兄弟節點也包含這個點,若 $pos$ 最右邊的兄弟節點是 $t$,那其父節點的右兄弟會是 $t+lowbit(t)$。 42 | 43 | 所以只要一直把 $pos$ 加上 $lowbit(pos)$,就可以得到它和它祖先的所有右兄弟節點,修改這些節點的答案就好了。 44 | 45 | 實作相當簡單: 46 | ```cpp 47 | void modify(int pos, int x){ // 把 pos 改成 x 48 | // n 是序列大小 49 | for(; pos <= n; pos += lowbit(pos)){ 50 | 修改節點 pos 的答案; 51 | } 52 | } 53 | ``` 54 | 55 | 可以發現 $lowbit(pos)$ 會不斷增加,所以複雜度是 $O(\log n)$。 56 | 57 | ## 前綴查詢 58 | 59 | 要查詢以 $pos$ 為結尾的前綴,就應該找到幾段不重疊,且聯集恰好是所求前綴的區間。 60 | 61 | 區間以 $pos$ 為結尾的節點是 $pos$,而節點 $pos$ 的區間剛好緊接在它的父節點之後,它的父節點是 $pos - lowbit(pos)$,所以只要找到 $pos$ 和它所有祖先節點,這些區間聯集起來就是我們想要的前綴。 62 | 63 | 實作也非常簡單: 64 | ```cpp 65 | type query(int pos){ 66 | type ans; 67 | for(; pos > 0; pos -= lowbit(pos)){ 68 | 更新ans; 69 | } 70 | return ans; 71 | } 72 | ``` 73 | 74 | 同樣地,$lowbit(pos)$ 會不斷增加,因此複雜度是 $O(\log n)$。 75 | 76 | ## 建構 77 | 78 | 如果有初始值的話,就把每一個元素分別 modify 就好了,複雜度是 $O(n \log n)$。 79 | 80 | ## 應用 81 | 82 | BIT 最基礎的應用就是拿來算前綴和,因為和是一種「可返回」的東西,減去本來的值在加上新的值就可以得到新的和,而有了前綴和就也可以算區間和了,且它比線段樹好寫很多。 83 | 84 | BIT 前綴和和差分結合使用,就可以做到 $O(\log n)$ 區間修改、$O(\log n)$ 單點查詢。 85 | 86 | BIT 也可以區間修改、區間查詢前綴和。先算出要做的序列 $A$ 的差分序列 $D$,$A$ 的索引從 $1$ 開始,然後我們可以得出 $[1,x]$ 這個區間的和是: 87 | $$\sum_{i=1}^x A_i = \sum_{i=1}^x D_i \times (x-i+1)\\ 88 | =D_1 \times x + D_2 \times (x-1) + \dots + D_x \times 1$$ 89 | 90 | 然後可以把它化成: 91 | 92 | $$(x+1) (\sum_{i=1}^x D_i) - (\sum_{i=1}^x D_i \times i)$$ 93 | 94 | 那麼只要用兩個 BIT,一個維護 $D_i$、另一個維護 $D_i \times i$,這樣就可以做區間修改、區間求和了。 95 | 96 | 至於 BIT 可不可以做最大最小值這種「不可返回」的操作呢?如果在求最大值的數字時,把其中一個位置改小,那麼是沒辦法單靠被修改的位置就得出新的最大值的,因此如果要用 BIT 做最大值,數字只能被改大,做最小值,數字只能被改小。 97 | 98 | ## 練習題 99 | 100 | - [ZeroJudge d799](https://zerojudge.tw/ShowProblem?problemid=d799) - 區間加值、詢問區間和 101 | - [TIOJ 1175](https://tioj.ck.tp.edu.tw/problems/1175) - LIS 102 | - [TIOJ 1869](https://tioj.ck.tp.edu.tw/problems/1869) - 二維 BIT 103 | - [TIOJ 1080](https://tioj.ck.tp.edu.tw/problems/1080) - 逆序數對 104 | -------------------------------------------------------------------------------- /_posts/note/advanced-ds/segment-tree.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: 線段樹 3 | --- 4 | # 線段樹 5 | 6 | 一些本文使用的實作習慣: 7 | 8 | - 所有區間都是左閉右閉。 9 | - 要把區間 $[L,R]$ 分成兩半時,令 $M=\lfloor \frac{L+R}{2} \rfloor$,左半是 $[L,M]$,右半是 $(M,R]=[M+1,R]$。 10 | 11 | ## 從分治到線段樹 12 | 13 | > 有一個序列 $A_1,A_2,\dots,A_N$,有 $Q$ 筆詢問,詢問有兩種: 14 | > 15 | > 1. 把 $A_i$ 加上 $v$ 16 | > 2. 求 $A_l,A_{l+1},\dots,A_r$ 的最大值 17 | > 18 | > $N,Q \leq 10^6$ 19 | 20 | 先想想看如果我們只是要問一次某個區間的最大值要怎麼做。最暴力的作法就是在第一種操作時直接把值改掉,第二種操作暴力掃過一次陣列,不過這樣的作法好像沒有什麼可發展性,所以我們來看一個感覺有點殺雞用牛刀的作法: 21 | 22 | 我們對整個陣列分治,令 `query(l,r,L,R)` 會回傳**區間 $[l,r]$ 與區間 $[L,R]$ 交集的這個區間的最大值**,並且假設我們呼叫時這個交集都不是空的。`[l,r]` 是我們想要知道最大值的範圍,所以我們要做的事情是每次都把 `[L,R]` 分成兩半,並且遞迴下去做。 23 | 24 | ```cpp 25 | int query(int l, int r, int L, int R){ 26 | if(L == R) return a[L]; 27 | int M = (L + R) / 2; 28 | if(r <= M) // [l,r] 只在左半部 29 | return query(l, r, L, M); 30 | else if(l > M) // [l,r] 只在右半部 31 | return query(l, r, M + 1, R); 32 | else // [l,r] 跨越兩半 33 | return max(query(l, r, L, M), query(l, r, M + 1, R)); 34 | } 35 | ``` 36 | 37 | 這樣做有什麼好處呢?考慮一下分治過程,把所有分治時可能出現的 $[L,R]$ 節點畫成一棵樹,並且把我們詢問 $[l,r]$ 時走過的節點標記起來,會像是這樣: 38 | 39 | 40 | 41 | 這是一個 $N=32,l=10,r=26$ 的例子,有塗顏色的點是詢問時有走過的點。這個作法需要花的時間是 $O(\text{走過的點數})$,要是 $[l,r]$ 是 $[1,N]$,那所有點都會被走過,因此時間複雜度是 $O(N)$。 42 | 43 | 注意到有一些點,我們會走到它為根的子樹裡全部的點,這些點是滿足 $l \leq L \leq R \leq r$ 的點,要是我們預先算好每一個點的區間內最大值是多少,就可以在走到一個滿足這個條件的點時,直接回傳最大值,而不需要再往子樹裡走。 44 | 45 | ```cpp 46 | #define lc 2 * id 47 | #define rc 2 * id + 1 48 | void build(int L, int R, int id){ 49 | if(L == R){ 50 | mx[id] = a[id]; 51 | return; 52 | } 53 | int M = (L + R) / 2; 54 | build(L, M, lc); 55 | build(M + 1, R, rc); 56 | mx[id] = max(mx[lc], mx[rc]); 57 | } 58 | int query(int l, int r, int L, int R, int id){ 59 | if(l <= L && R <= r) return mx[id]; 60 | int M = (L + R) / 2; 61 | if(r <= M) 62 | return query(l, r, L, M, lc); 63 | else if(l > M) 64 | return query(l, r, M + 1, R, rc); 65 | else 66 | return max(query(l, r, L, M, lc), 67 | query(l, r, M + 1, R, rc)); 68 | } 69 | ``` 70 | 71 | 先 `build(1, N, 1)` 找出所有節點對應區間的最大值,之後就可以用 `query(l, r, 1, N, 1)` 詢問。計算最大值時發揮分治的精神,從子節點(兩半部)的資訊(最大值)得出這個節點(整個區間)的資訊。 72 | 73 | 為了方便儲存每個節點的最大值,我們給每個節點一個編號。這裡的根節點編號是 1,節點 $x$ 的左子節點是 $2x$,右子節點則是 $2x+1$。這種編號方式所使用的節點編號最大會 $<4N$,所以可以用一個長度 $4N$ 的陣列儲存最大值,也有一些其他不同的方法,後面的章節會再細講。 74 | 75 | `build` 需要 $O(N)$ 的時間,那麼一次詢問要多少時間呢?這樣的作法中,我們只會走到上面那張圖裡紅色與藍色的節點,藍色的節點就是我們遇到 `l <= L && R <= r` 然後直接回傳記錄的最大值的時候,也就是遞迴的**終止節點**。仔細觀察一下,其實紅色與藍色的節點們都一定是「對應到 $[l,l]$ 和 $[r,r]$ 的節點的祖先,以及它們祖先的兄弟」,而因為樹深度是 $O(\log N)$,因此一次詢問的時間複雜度是 $O(\log N)$。 76 | 77 | 現在我們已經可以處理快速處理多次詢問了,那麼加上修改呢?注意到我們詢問時需要的資訊就只有**每個節點儲存的區間最大值**而已,因此我們只要想辦法維護這些資訊。修改 $a_i$ 時,最大值會改變的節點就是所有包含區間 $i$ 的節點,也就是 $[i,i]$ 對應的節點的所有祖先。 78 | 79 | ```cpp 80 | // 把 a[x] 改成 v 81 | void modify(int x, int v, int L, int R, int id){ 82 | if(L == R){ // 節點對應的區間是 [x,x] 83 | mx[id] = v; 84 | return; 85 | } 86 | int M = (L + R) / 2; 87 | if(x <= M) // x 在左半部 88 | modify(x, v, L, M, lc); 89 | else // x 在右半部 90 | modify(x, v, M + 1, R, rc); 91 | // 重新用子節點的最大值計算這個節點的最大值 92 | mx[id] = max(mx[lc], mx[rc]); 93 | } 94 | ``` 95 | 96 | 這樣一來一次修改的時間就和深度相同,是 $O(\log N)$,我們就成功做出這個問題了! 97 | 98 | 這棵把分治過程畫出來的樹就是我們所稱的線段樹。線段樹大致上來說可以視為是在將分治的過程記錄下來,線段樹最重要的是應用到分治將一個區間分成 $O(\log N)$ 個區間,也就是上面的遞迴終止節點將詢問的區間分成互不相交的 $O(\log N)$ 段,以及用到深度很小易於查詢與修改的性質。 99 | 100 | 線段樹不只可以算最大值,像區間總和也可以算,只要記錄每個節點的區間總和,詢問時將所有終止節點的區間和加起來(因為它們的範圍是互不重疊的)就好。 101 | 102 | ## 線段樹的節點編號 103 | 104 | 前面的實作中,我們用一個陣列儲存線段樹節點的資訊,因此需要給線段樹每個節點一個編號。 105 | 106 | 剛剛使用的方法是,根節點的編號是 1,$x$ 的左子節點編號是 $2x$,右子節點編號是 $2x+1$。這個編號方式和通常用於編號完全二元樹(complete binary tree,除了最後一層以外每一層的節點數量都是最大值,且最後一層節點靠左的二元樹)中節點的方式一樣。這種編號的二進位會是最高位的 1,後面的 bit 表示從根節點走到那個節點的方法,0 代表往左、1 代表往右。畫出來的話,看起來會是深度 0 的節點編號是 $1$、深度 1 的是 $2,3$、深度 2 的是 $4,5,6,7$,依此類推。 107 | 108 | 一棵 $N$ 個節點的線段樹,在這種編號方式用到的最大節點編號 $< 4N$。假設我們將線段樹填滿成一棵高度相同,節點數量最大的二元樹,這個數量就是我們可能會用到的最大編號。這個數量會是 $2^{\text{深度} + 1} - 1$,深度恰為 $\lceil \log_2 N \rceil < \log_2 N + 1$,因此數量不超過是 $2^{\log_2 N + 1 + 1} - 1 = 4N-1 < 4N$。 109 | 110 | 節點也有一些別的編號方法,像是根節點是 0,$x$ 的左右子節點分別是 $2x+1$ 和 $2x+2$,就只是上面那種編號都少 1 的版本而已。 111 | 112 | 還有一種神奇的編號方式是區間是 $[L,R]$ 的節點編號是 `(L + R) | (L != R)`,這樣每個節點的編號都會是不重複的,因為: 113 | 114 | - 當 $L \neq R$ 也就是節點是非葉節點、 `L != R` 是 `1` 時,不難發現到每個節點的 $\lfloor (L+R)/2 \rfloor$ 也就是子節點區間的分界點都不一樣,換句話說 `(L + R) >> 1` 都不一樣,因此 `(L + R) | 1` 也會不一樣。 115 | - 當 $L=R$ 時,很明顯地 $L+R$ 都不一樣,而且 $L+R$ 一定是偶數,而非葉節點編號一定是奇數,所以不會和非葉節點衝突。 116 | 117 | 這樣就只會用到 $2N$ 以內的編號(沒有非葉節點的 $L+R$ 會是 $2N$)。 118 | 119 | ## 指標型線段樹 120 | 121 | 存二元樹的方法不是只有陣列,當然也有存左右子節點的指標,實作通常是直接幫每個節點開一個 struct: 122 | 123 | ```cpp 124 | struct Node{ 125 | Node *l, *r; 126 | // 其他節點資訊 127 | } 128 | ``` 129 | 130 | 也可以使用偽指標(memory pool),就是一樣用一個陣列存節點資訊,但是在節點上存左右子節點在那個陣列中的位置。實作上可以用一個變數儲存現在陣列裡放了多少東西,每次需要新的節點(例如 build 或動態開點)時,就把這個數量 +1 且這個節點放在第數量個位置。 131 | 132 | 這樣會用到的空間大小就是節點數量,線段樹的節點數量恰好會是 $2N-1$,可以用數學歸納法證明:$N=1$ 時節點數量是 $2N-1=1$,而 $N>1$ 時,假設左子樹的節點數量是 $t$,右子數是 $N-t$,那麼節點總數就是 $(2t-1)+(2(N-t)-1)+1=2N-1$。 133 | 134 | ## 區間修改與懶人標記 135 | 136 | 如果前面區間詢問最大值、單點修改的題目,把單點修改改成區間加上某個數,那還可不可以做呢? 137 | 138 | 當修改區間 $[l,r]$ 時,資訊會更新的節點是所有區間和 $[l,r]$ 有交集的節點,單點修改時只有 $O(\log N)$ 個,但是區間修改時就會像我們一開始在區間詢問時一樣有 $O(N)$ 個。 139 | 140 | 不過,一樣會有一些節點是**它的子樹裡所有節點都需要修改**,那我們乾脆一樣把它們當成是遞迴的終止節點,把它的資訊更新,然後放一個標記說「這個子樹全部人都要做這個修改」,當我們需要走到它的子節點,就根據標記更新子節點的資訊,並且把標記移到子節點身上。 141 | 142 | ```cpp= 143 | struct Node{ 144 | int mx; // 區間最大值 145 | int tag; // 子樹裡所有人都要加上 tag 146 | }; 147 | 148 | vector seg; 149 | 150 | // 節點 id 的整個區間要加上 tag 151 | void addtag(int tag, int id){ 152 | seg[id].mx += tag; // 最大值會加上 tag 153 | seg[id].tag += tag; // 注意可能本來就有標記了,所以是 += 154 | } 155 | 156 | // 更新子節點資訊並把標記移到子節點身上 157 | void push(int id){ 158 | addtag(seg[id].tag, lc); 159 | addtag(seg[id].tag, rc); 160 | seg[id].tag = 0; // 標記被移到子節點上所以要改成 0 161 | } 162 | 163 | // 區間 [l,r] 加上 v 164 | void modify(int l, int r, int v, int L, int R, int id){ 165 | if(l <= L && R <= r){ 166 | addtag(v, id); 167 | return; 168 | } 169 | push(id); 170 | if(r <= M) modify(l, r, v, L, M, lc); 171 | else if(l > M) modify(l, r, v, M + 1, R, rc); 172 | else{ 173 | modify(l, r, v, L, M, lc); 174 | modify(l, r, v, M + 1, R, rc); 175 | } 176 | seg[id].mx = max(seg[lc].mx, seg[rc].mx); 177 | } 178 | 179 | int query(int l, int r, int L, int R, int id){ 180 | if(l <= L && R <= r) return seg[id].mx; 181 | push(id); 182 | int M = (L + R) / 2; 183 | if(r <= M) return query(l, r, L, M, lc); 184 | else if(l > M) return query(l, r, M + 1, R, rc); 185 | else return max(query(l, r, L, M, lc), 186 | query(l, r, M + 1, R, rc)); 187 | } 188 | ``` 189 | 190 | 這個標記就被稱作懶人標記,而把標記移到子節點身上的這個動作被稱為下推標記。 191 | 192 | 注意到 modify 往下走時有時可以不需要 push,像在這邊其實可以不用,不過在操作沒有交換律(做事順序會影響結果)的時候就會需要,例如如果操作是區間改成同一個值,那麼就會需要在修改時 push,因為沒有 push 的話,子孫的新標記可能之後會被祖先的舊標記蓋掉。 193 | 194 | 這樣一來區間修改的複雜度就和區間詢問一樣是 $O(\log N)$ 了。 195 | 196 | ## 線段樹的框架 197 | 198 | 如上所說,線段樹不一定是詢問最大值,修改也不一定是加值。基本上,只要維護的資訊是**父節點的資訊能從兩個子節點的資訊得出**、操作有**結合律**(可以把相鄰的操作合起來)就能用線段樹維護。以下是一個基本的線段樹框架: 199 | 200 | ```cpp 201 | #define lc (id 的左子節點) 202 | #define rc (id 的右子節點) 203 | 204 | struct Node{ 205 | Info info; // 線段樹上儲存的資訊 206 | Tag tag; // 懶人標記 207 | }; 208 | 209 | vector seg; // 存線段樹的陣列 210 | 211 | void pull(int id){ /* 從 id 的子節點資訊更新 id 的資訊 */ } 212 | void addtag(Tag tag, int id){ 213 | seg[id].info = 用 tag 更新 seg[id].info; 214 | seg[id].tag += tag; 215 | } 216 | void push(int id){ 217 | addtag(seg[id].tag, lc); 218 | addtag(seg[id].tag, rc); 219 | // 把 seg[id].tag 清空 220 | } 221 | 222 | void build(int L = 1, int R = N, int id = 1){ 223 | if(L == R){ 224 | // 從原始資料算 seg[id].info 225 | return; 226 | } 227 | int M = (L + R) / 2; 228 | build(L, M, lc); 229 | build(M + 1, R, rc); 230 | pull(id); 231 | } 232 | 233 | void modify(int l, int r, Tag v, int L, int R, int id){ 234 | if(l <= L && R <= r){ 235 | addtag(v, id); 236 | return; 237 | } 238 | push(id); 239 | if(r <= M) modify(l, r, v, L, M, lc); 240 | else if(l > M) modify(l, r, v, M + 1, R, rc); 241 | else{ 242 | modify(l, r, v, L, M, lc); 243 | modify(l, r, v, M + 1, R, rc); 244 | } 245 | pull(id); 246 | } 247 | 248 | Info query(int l, int r, int L, int R, int id){ 249 | if(l <= L && R <= r) return seg[id].info; 250 | push(id); 251 | int M = (L + R) / 2; 252 | if(r <= M) return query(l, r, L, M, lc); 253 | else if(l > M) return query(l, r, M + 1, R, rc); 254 | else return query(l, r, L, M, lc) + 255 | query(l, r, M + 1, R, rc)); 256 | } 257 | ``` 258 | 259 | `Tag` 是表示**修改資訊**(懶標其實就是在把修改資訊記在結點上),例如要加上多少、要改成什麼等等。`Tag` 的相加是表示將兩個操作合起來,例如加上 $a$ 再加上 $b$ 可以合成加上 $a+b$,而 Info 相加則是把兩個節點的資訊拼起來,例如在上面的例子中是把兩個數取 max。 260 | 261 | 節點上可以維護很多資訊,像是當然可以同時維護最小值和最大值,有時也需要存一些不會改變的資訊,例如在詢問區間和並支援區間加值時,就會需要在節點上存區間大小,也可以在 `addtag` 時傳入區間大小,因為更新區間總和是,區間總和會加上「要加的值乘上區間大小」。 262 | 263 | 線段樹也可以一次支援很多種操作,只是這樣在結合兩個操作時需要注意一下順序。例如要同時支援區間加值和區間改值,可以在每個節點記錄要加多少和要改成什麼,打懶標或下推懶標時就要注意怎麼修改節點的懶標。 264 | 265 | 還有例如同時支援區間加值和乘值,每個操作都可以描述成 $(a,b)$ 代表把 $v$ 改成 $v \times a + b$,那麼先做 $(a_1,b_1)$ 再做 $(a_2,b_2)$ 就會變成 $(v \times a_1 + b_1) \times a_2 + b_2 = v \times a_1 \times a_2 + b_1 \times a_2 + b_2$,所以結合後的操作是 $(a_1 \times a_2, b_1 \times a_2 + b_2)$。基本的原則是「如果操作一對操作二有分配律,那麼操作一先做」,可以試試看如果定義是 $(v+b) \times a$,這樣就很難將兩個操作結合起來並保持相同的表示方式。 266 | 267 | 268 | ## 動態開點線段樹 269 | 270 | 前面的例子中,都是「完整的開出一整棵線段樹」,但要是我們想要開一個很大的線段樹呢? 271 | 272 | 假設 $N$ 很大,例如 $10^9$,我們當然不可能開出至少是 $2N-1$ 的空間。注意到我們會真正用到的節點數量其實很少,每一個操作和詢問其實都只會用到 $O(\log N)$ 個節點,那我們乾脆需要用到再開出這個節點就好了。 273 | 274 | 實作上通常會用指標線段樹(當然也可以偽指標),一開始只有根節點,子節點都是 `nullptr`,當詢問時走到不存在的節點就當作整個區間都是預設值並回傳相應的答案,操作時走到不存在的節點就開一個新節點,同樣用預設值初始化。一種好用的寫法是,讓 `modify` 回傳節點的指標,這樣就可以在遞迴下去時直接把那一個子節點改成回傳值。 275 | -------------------------------------------------------------------------------- /_posts/note/advanced-ds/sparse-table.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Sparse Table 3 | --- 4 | # Sparse Table 5 | 6 | > 有一個序列 $A$,有 $Q$ 筆詢問,每筆詢問求 $[l,r]$ 的區間最大值。 7 | 8 | 如果是要求區間和的話,前綴和在經過 $O(n)$ 預處理後,可以 $O(1)$ 求得區間和,但顯然這樣的方式不能用在區間最大或最小值上。那麼有沒有辦法可以在經過預處理後 $O(1)$ 得到區間極值呢? 9 | 10 | 如果算出每一個 $2^k$ 長的區間中的極值,在詢問的時候,就可以把一些區間拼起來,得到我們想要的答案。而且計算也很方便,因為每個 $2^k$ 長的區間,都是兩個 $2^{k-1}$ 長的區間拼起來。 11 | 12 | 13 | 14 | ($2^k$ 切塊示意圖,事實上會計算到的區間不只這樣。) 15 | 16 | 令 $s[i][j]$ 為以元素 $i$ 為起點,長 $2^j$ 的區間中的極值,也就是說,這個區間的範圍是 $[i, i+2^j-1]$(會超出範圍的就不用算,也用不到)。 17 | 18 | 長度 $2^j$ 的區間可以分割成兩個長度 $2^{j-1}$ 的區間,起點分別是 $i$ 和 $i + 2^{j-1}$。因此,$s[i][j]$ 的值是 $s[i][j-1]$ 和 $s[i + 2^{j-1}][j-1]$ 取極值。 19 | 20 | 當 $j=0$ 時,$2^j=1$,因此以 $i$ 為起點,$2^j$ 大小的區間長度只有 $1$,內容也只有第 $i$ 個元素,因此,$s[i][0]$ 就是第 $i$ 個元素。 21 | 22 | 最後,當我們要求 $[l,r]$ 的極值,就用兩個長 $2^{\lfloor \log_2 (r - l + 1) \rfloor}$ 的區間來湊出答案就好,其中一個的起點是 $l$,另一個起點是 $r-2^{\lfloor \log_2 (r - l + 1) \rfloor} + 1$,因此,令 $k=\lfloor \log_2 (r - l + 1) \rfloor$,我們可以用 $s[l][k]$ 和 $s[r-2^k+1][k]$ 來得到我們想要的答案,它們有部分重疊也沒有關係,只要兩個區間範圍聯集起來是我們想要的區間就好,極值一定會出現在其中一區間。 23 | 24 | 這樣一來,我們就可以先預處理整個 $s$ 了。我們需要求出以每個元素為起點,各個 $2^k$ 長的區間的極值,至於 $k$ 的最大值應該要是多少?會用到的最大的 $k$ 會出現在詢問區間是整個數列的時候,我們會需要 $k \leq \lfloor \log_2 n \rfloor$,所以 $k$ 的範圍只需要 $[0,\lfloor \log_2 n \rfloor]$,預處理複雜度是 $O(n \log n)$。 25 | 26 | 因為同一個元素會被很多區間包含,所以 Sparse Table 不能做修改。 27 | 28 | ## 練習題 29 | 30 | - [ZeroJudge d539](https://zerojudge.tw/ShowProblem?problemid=d539) - 區間詢問最大值 31 | - [TIOJ 1338](https://tioj.ck.tp.edu.tw/problems/1338) - 詢問區間內有沒有一個數字整除區間內全部的數字 -------------------------------------------------------------------------------- /_posts/note/basic-ds/array-like-ds.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: 類陣列容器 3 | --- 4 | # 類陣列容器 5 | 6 | 本文提到的資料結構在 STL 裡都有實作,詳細用法請自己看 [cppreference](https://en.cppreference.com/w/cpp/container)。 7 | 8 | ## 動態陣列 9 | 10 | 原始的陣列是開一塊連續記憶體來放資料,這段空間一開始就要決定,且不能改變。那麼有沒有可以改變長度的陣列呢? 11 | 12 | STL 中,有一個容器叫 vector,它就是一個動態陣列,可以改變長度。 13 | 14 | 它改變長度的方式是,一開始它會先開一段連續記憶體來用,如果陣列長度增長到超出這個空間,那麼會再開一段更長的連續記憶體,然後把所有元素搬移到新的空間去,雖然乍聽之下每次加入東西最慘會花到 $O(n)$,總共就 $O(n^2)$ 了,但事實上每次搬東西的量會是 $2^0,2^1,\dots,2^{\lfloor log_2 n \rfloor}$,加起來只有 $O(n)$ 而已。 15 | 16 | 不過常數還是有點大,建議兩種作法,有元素的部分稱為「陣列大小」,目前有的空間稱為「空間大小」: 17 | - 一開始就把陣列開大,也就是一開始就讓它有很多個元素,也就是說,這麼做之後你完全可以把它當普通陣列來用,這用在不需要 `push_back` 的狀況。 18 | - 用 `reserve` 來把空間大小開大,這個作法是用在需要 `push_back` 的狀況。 19 | 20 | vector 除了有長度可變的優點外,也有很多附加功能,比原始的陣列好用很多,所以即使長度不會改變,大多數的時候我也都會用 vector 來代替陣列。 21 | 22 | 在 C++11 中,`push_back` 有了個替代品叫 `emplace_back`,它們的時間複雜度同為 $O(1)$,但 `emplace_back` 常數比較小。 23 | 24 | 一些複雜度: 25 | - 存取:可隨機存取,$O(1)$。 26 | - 插入:在最後面是 $O(1)$(均攤)、其他地方是 $O(n)$,所以不要隨便用 `insert`。 27 | - 刪除:在最後面是 $O(1)$、其他地方是 $O(n)$。 28 | - resize、clear:$O(n)$。 29 | 30 | ## deque 31 | 32 | deque 的全稱是 double-ended queue,也就是雙向佇列的意思,不過我不喜歡把它當成一種 queue。它也算是一種動態陣列,和 vector 不同的是,它可以在前後 $O(1)$ 插入刪除。 33 | 34 | deque 的實作原理是,它的元素會被分成很多段放在記憶體上,不像 vector,變太大還要搬走,但實際上 deque 的常數是比 vector 大的,所以非必要最好別用 deque。 35 | 36 | deque 的操作和 vector 幾乎一樣,而且可以用 `[]` 存中間的元素。 37 | 38 | ## 堆疊(stack) 39 | 40 | stack 是一種先進後出的結構,像是疊盤子一樣,先放下去的會最慢被拿出來。可以把 stack 想成是一疊元素,最上面的元素就是堆頂。 41 | 42 | stack 的功能是 vector 功能的子集,以 vector 來說,堆頂就是 `back()`、放東西到堆頂是 `push_back()`、把堆頂丟掉是 `pop_back()`,同理 deque 也可以拿來實作 stack,至於 STL 中的 stack 預設是用 deque 實作的,所以它其實是 deque 劣化版 (?),不過寫成 stack 的可讀性比較高一點。 43 | 44 | STL 的 stack 只能問堆頂是什麼,有時候會需要用到 stack 裡除了堆頂以外的元素,例如凸包,這個時候可以改用 vector。 45 | 46 | ## 佇列(queue) 47 | 48 | queue 是一種先進先出的結構,也就是先放進去的元素,會先被取出來,跟排隊一樣,先排隊的人會先離開隊伍。 49 | 50 | queue 的功能是 deque 的功能的子集,實際上 STL 裡的 queue 也是用 deque 實作的,而且和 stack 一樣,queue 只能問最前面和最後面的元素,所以在需要知道其他元素的狀況,可以改用 deque。 51 | -------------------------------------------------------------------------------- /_posts/note/basic-ds/linked-list.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: 鏈結串列 3 | --- 4 | # 鏈結串列 5 | 6 | 陣列和動態陣列都可以隨機存取,但如果要移除一個元素而不留空位,就得花 $O(n)$ 的時間把後面的元素往前移,在中間插入一個元素也是需要把後面所有元素後移,也要 $O(n)$ 的時間,那麼如果需要頻繁進行在序列中間刪除或插入的動作怎麼辦呢? 7 | 8 | 鏈結串列(Linked List)可以做到這件事,它把每一個元素放進一個節點裡,每個節點除了儲存元素的值外,也儲存一些指標,這些指標指向哪裡會根據你的需求而有所不同。 9 | 10 | 最基礎的鏈結串列是**單向鏈結串列**,每一個節點有一個指標,指向下一個元素所在的節點,如果沒有下一個,就指向 `nullptr`。 11 | 12 | ```cpp= 13 | template 14 | struct Node{ 15 | T value; 16 | Node* nxt = nullptr; 17 | }; 18 | ``` 19 | 20 | 要插入一個元素的話,就把前一個元素的節點的下一個節點改成新插入的節點,而新插入的節點指向前一個節點本來的下一個節點。 21 | 22 | ```cpp= 23 | template 24 | Node* insert(Node* node, int value){ 25 | Node* n = new Node(); 26 | n->nxt = node->nxt; 27 | n->value = value; 28 | node->nxt = n->value; 29 | return n; 30 | } 31 | ``` 32 | 33 | 要刪除節點的話,就把要刪除的節點的上一個節點,接到要刪除的節點的下一個節點。不過單向鏈結串列要找一個節點的上一個節點的話,必須從頭開始線性搜尋,會比較麻煩一點。這個時候就可以用雙向連結串列,也就是再存一個指向上個節點的指標,這樣就可以馬上知道上一個點是誰了。 34 | 35 | 還有很多種可能的狀況,像是你的鏈結串列是環狀的,就可以讓最後一個節點的下一個節點是第一個節點。 36 | 37 | 除了可以用指標來做外,也可以開兩個陣列,這兩個陣列中的相同位置一起表示一個節點,一個陣列用來存值、另一個存下一個節點的位置編號,如果要做雙向的話也可以再開一個陣列。 38 | 39 | 指標實作鏈結串列最大的缺點是不能隨機存取,但陣列實作可以彌補,雖然還是不能直接隨機存取串列裡第幾個元素,但你可以給位置編號特殊意義,像是在陣列裡的位置 $i$ 表示編號是 $i$ 的東西,這樣你就可以隨機存取每一個東西的上一個或下一個東西是什麼。 40 | 41 | ## STL 42 | 43 | STL 中的 `list` 是雙向鏈結串列,但是很難用,沒什麼用處。 -------------------------------------------------------------------------------- /_posts/note/binary/bitwise-operator.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: 位元運算 3 | --- 4 | # 位元運算 5 | 6 | 位元運算就是對二進位的 `0` 和 `1` 做一些處理,然後會有一些很神奇的性質和用途。 7 | 8 | ## bool 運算 9 | 10 | `bool` 只有 `true` 跟 `false`,然後可以對它們做一些運算: 11 | 12 | | |符號|C++| 13 | |--|------|-----| 14 | |AND| $\land$ | `a && b` | 15 | |NOT| $\lnot$ | `!a` | 16 | |OR| $\lor$ | `a || b` | 17 | |XOR| $\oplus$ | `a != b` | 18 | 19 | 其中比較奇怪的東西是 xor 在兩個運算元中有恰一個是 `true` 時,結果是 `true`,反之就是 `false`。你可能會想蛤 `!=` 不是不等於嗎,其實只有兩個運算元的 xor 就是不等於的意思。(有時候會遇到超過兩個東西一起 xor,這個時候 XOR 的定義是有奇數個是 `true` 時結果為 `true`。) 20 | 21 | ## 位元運算 22 | 23 | 進入正題,除了 `bool` 可以做以上那些運算以外,`int`、`long long` 這樣的整數型態也可以做這些運算,做法就是每個位元分別做運算,例如: 24 | 25 | $(100110)_2$ 和 $(011000)_2$ 做 or 運算會是: 26 | 27 | |bit|5|4|3|2|1|0| 28 | |-|-|-|-|-|-|-| 29 | |A|1|0|0|1|1|0| 30 | |B|0|1|1|0|0|0| 31 | |ANS|1|1|1|1|1|0| 32 | 33 | 這叫做 bitwise operation。 34 | 35 | 以下是 C++ 的位元運算子列表: 36 | 37 | | |運算子| 說明 | 38 | |-|-----|-----| 39 | |AND|`A & B`| bitwise and| 40 | |OR|`A | B`| bitwise or| 41 | |XOR| `A ^ B` | bitwise xor| 42 | |左移| `A << n` | 將 A 的每個位元左移 $n$ 位,右方補 `0` | 43 | |右移| `A >> n` | 將 A 的每個位元右移 $n$ 位,右方補 `0` 或 `1` 依 A 本來的最高位決定 | 44 | |NOT| `~A` | bitwise not,將 A 為 `0` 的位元換成 `1`,為 `1` 換成 `0`,也就是取補數 | 45 | 46 | 一些範例: 47 | 48 | - 要判斷一個數 $A$ 的右方數來第 $n$ 位是 `0` 還是 `1`: 49 | ```cpp 50 | A & (1 << n) //>0 的話為 1,否則為 0 51 | ``` 52 | - 把一個數 $A$ 用很怪的方式換成相反數: 53 | ```cpp 54 | ~A+1 55 | ``` 56 | - 把 $A$ 除了最低位的 `1` 以外的 `1` 都換成 `0`: 57 | ```cpp 58 | A & -A 59 | ``` 60 | - 乘以 2 和除以 2 61 | ```cpp 62 | a <<= 1 // a *= 2 63 | a >>= 1 // a /= 2 64 | ``` 65 | 66 | ### Bitwise xor 67 | 68 | 因為 xor 太多常用性質,所以特別拿出來講。先來幾個基本原則: 69 | 70 | - $A \oplus B = B \oplus A$,$(A \oplus B) \oplus C = A \oplus (B \oplus C)$,也就是有交換律和結合律。 71 | - $A \oplus A = 0$ 72 | - 若 $A \oplus B = C$,則 $A \oplus C = B$ 73 | - $A \oplus 0 = A$ 74 | 75 | 這些只要稍微想一下就可以得出來了,如果覺得很難想,可以先用只有一個 bit 的狀況來想,反正每一個位元互不干擾。 76 | 77 | 舉例來說,把兩個變數 `a`、`b` 互換可以這樣做: 78 | ```cpp 79 | a = a ^ b; 80 | b = a ^ b; 81 | a = a ^ b; 82 | ``` 83 | 84 | > 稍微解釋一下,若 `a`、`b` 的原始值分別為 $a_0$、$b_0$,新值為 $a_1$、$b_1$,那麼: 85 | > 設 $t = a_0 \oplus b_0$ 86 | > $b_1 = t \oplus b_0 = a_0 \oplus b_0 \oplus b_0 = a_0$ 87 | > $a_1 = t \oplus b_1 = a_0 \oplus b_0 \oplus a_0 = b_0$ 88 | 89 | 可以發現到在做 xor 時,每個人都是自己的反元素,因為 $A \oplus A = 0$,既然如此,就可以拿去做前綴 xor: 90 | $$S_l \oplus S_{l + 1} \oplus \dots \oplus S_{r - 1} \oplus S_r = (S_1 \oplus S_2 \oplus \dots \oplus S_{r - 1} \oplus S_r) \oplus (S_1 \oplus S_2 \oplus \dots \oplus S_{l - 2} \oplus S_{l - 1})$$ 91 | -------------------------------------------------------------------------------- /_posts/note/binary/int-and-float.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: 整數與浮點數 3 | --- 4 | # 整數與浮點數 5 | 6 | ## 整數 7 | 8 | 在寫數字的時候,表示負數很簡單,就加個負號就好了。那在電腦中該怎麼表示負數? 9 | 10 | 最簡單的方式是用最高位元來表示正負號,0 代表正,1 代表負,像是如果用 8 個位元來表示一個數,$-5$ 就是 `1000 0101`,$-3$ 就是 `1000 0011`,但這樣會發生一個問題,就是表示 $0$ 有兩種方式:不管最高位是 `1` 或 `0`,只要其他位都是 `0`,這樣它就是表示 $0$,所以勢必需要另一個方法來有效利用空間。 11 | 12 | ### 補數和二補數 13 | 14 | 先用一個不那麼位元的想法來想,有 $2^k$ 個 $k$ 位元的二進位數字,介於 $[0, 2^k-1]$,如果分一半給非負整數、另一半給負整數,有一個很簡單的方法,就是對於 $\geq 2^{k-1}$ 的數,都分配給負數,$b$ 表示的負數就是和它模 $2^k$ 下同餘、最大的那個負數 $c$,也就是 $c=b-2^k$。 15 | 16 | 這樣做有一個好處,就是既然我們是在模 $2^k$ 下做事,代表它可以套用所有模除的性質,我們只要把數字對到 $[-2^{k-1}, 2^{k-1}-1]$ 的範圍裡就好。既然在電腦裡減法不方便,就都先換成用來表示負數的那個數,再相加就好了,非常方便。 17 | 18 | 回到位元的看法,$b$ 的補數定義為 $2^k-1-b$,也就是將一個二進位數字的 0、1 互換。例如: 19 | `0000 0001` 的補數是 `1111 1110` 20 | `0101 1010` 的補數是 `1010 0101` 21 | 在 C++ 中,`~` 這個運算子就是用來做這件事情。 22 | 23 | 而 $b$ 的二補數的定義是 $2^k-b$,也就是補數再加上 1,注意我們還是在模 $2^k$ 下做事,也就是如果進位到超出範圍,就把超出的部分捨去,這樣就得到了一個數的二補數,例如: 24 | `0000 0001` 的補數是 `1111 1110`,二補數是 `1111 1111` 25 | `0101 1010` 的補數是 `1010 0101`,二補數是 `1010 0110` 26 | `0000 0000` 的補數是 `1111 1111`,**加 1 後是** `1 0000 0000`,二補數是 `0000 0000` 27 | `1000 0000` 的補數是 `0111 1111`,二補數是 `1000 0000` 28 | 29 | 這就是目前最常見的電腦儲存有號(正負)整數的方式,這裡說的二補數的字面上的值,就是我們剛剛說的用同餘得出來的數字。 30 | 31 | C++ 中常見的整數型態範圍是這樣: 32 | (`unsigned` 開頭的是**無號**的,也就是只有 $0$ 和正數,因此 $k$ 位元的無號整數最大值是 $2^k-1$) 33 | 34 | |型態|byte 數|最大整數|最小整數| 35 | |---|---|---|---| 36 | |char|1|127|-127| 37 | |int|4|2147483647|-2147483648| 38 | |long long|8|9223372036854775807|-9223372036854775808| 39 | |unsigned int|4|4294967295|0| 40 | |unsigned long long|8|18446744073709551615|0| 41 | 42 | :::info 43 | 建議大概記一下這些的範圍,像是 `int` 的最大值大約是 $2 \times 10^9$、`long long` 的最大值大約是 $9 \times 10^{18}。$ 44 | ::: 45 | 46 | 接下來,有個特殊狀況,就是有兩個數取二補數後會跟原本一樣,這兩個數分別是 $0$ 和最小整數。$0$ 的相反數本來就是 $0$,$[0,2^k-1]$ 也沒有人跟它同餘;最小整數是 $-2^{k-1}$,但表示的數字範圍只到 $2^{k-1}-1$。 47 | 48 | 結論: 49 | 50 | - $k$ 位元可以表示的有號數範圍為 $[-2^{k-1}, 2^{k-1}-1]$ 51 | - $k$ 位元可以表示的無號數範圍為 $[0, 2^k-1]$ 52 | - $k$ 位元的同個二進位值所表示的有號數 $a$ 和無號數 $b$ 滿足 $a \equiv b \pmod{2^k}$ 53 | - $0$ 和最小整數取二補數後不變 54 | - $k$ 位元二進位值 $a$ 的二補數是 $2^k-a$ 55 | 56 | ### 大數運算 57 | 58 | 事實上所有整數進位制都可以套用類似二補數的規則,$r$ 進位制 $k$ 位數可以用的數字有 $[0, r^k-1]$,如果 $r$ 是偶數,可以和二進位一樣,非負整數和負數各一半,表示範圍是 $[-r^k/2,r^k/2-1]$;奇數的話,則是 0 給 0、剩下正負數各分一半,表示範圍是 $[-\lfloor r^k/2 \rfloor,\lfloor r^k/2 \rfloor]$。這樣的話可以保證 $r^k-b$ 表示的數一定是 $b$ 表示的數的相反數,或是等於 $b$。 59 | 60 | 這可以幹嘛呢?就是大數運算啦,以前大數運算還要多寫減法,現在只要寫加法就夠了!算 $r^k-b$ 的方法很簡單,$r^k-1$ 就是每一位都是 $r-1$ 的 $k$ 位數字,減去 $b$ 再把 1 加回去就好了。 61 | 62 | ## 浮點數 63 | 64 | 會存整數之後我們也要存小數,我們會用一種叫浮點數的東西來存它。用來規定浮點數表示方式的標準叫 [IEEE 754](https://zh.wikipedia.org/wiki/IEEE_754),大部分程式語言都會按照這個標準。 65 | 66 | 浮點數是用類似科學記號的方式來儲存,只是換成二進位而已。用來儲存一個浮點數的記憶體由高位(左)到低位(右)被分成三段: 67 | - **符號位**,只佔一個位元,`0` 表示此數為正,`1` 表示此數為負。 68 | - **指數域**,實際表示的值是指數域儲存的值減去**指數偏移量**,$k$ 位指數域的偏移量是 $2^k-1$。 69 | - **小數域**,表示一個介於 $[(1)_2, (10)_2)$ 的小數,因為我們知道這個小數的個位數字是 $1$,這也是唯一在小數點前的數字,所以不需要儲存它,因此這裡只儲存**小數點後的位數**。 70 | 71 | 若指數域所表示的指數值是 $n$,而小數域表示的值是 $f$,那麼這個浮點數是: 72 | $$\pm n \times 2^f$$ 73 | 至於是正或負,依符號位而定。並且 $n \in [(1)_2, (10)_2) = [(1)_{10},(2)_{10})$。 74 | 75 | 注意到負指數不是用二補數表示,而是減去指數偏移量。若指數偏移量是 $b$,且指數域有 $k$ 個位元,則可表示的指數範圍是 $[-b+1, 2^k-b-2]$,至於 $-b$ 和 $2^k-b-1$ 被用作特殊用途。 76 | 77 | 為小數域的長度是有限的,但小數有無限小數,無限小數中又有循環小數和無理數,因此會發生不精確的問題,造成計算和判斷上的錯誤,遇到這種狀況有這幾種辦法: 78 | - 盡量不要用到小數,需要做運算的話,如果不需要做到太多難做的操作(例如乘法就是很簡單的操作),可以先用分子分母分開存的方式來計算,要輸出的時候再換成小數就好。 79 | - 判斷一個浮點數是否等於一個值的時候,允許一個誤差範圍(相差小於一個極小的數),例如: 80 | ```cpp 81 | double a = /*...*/; 82 | if(a == 0){/*...*/} 83 | ``` 84 | 改成 85 | ```cpp 86 | double a = /*...*/; 87 | if(fabs(a - 0) < 1e-9){/*...*/} 88 | ``` 89 | 90 | 因為浮點數利用科學計號的方式儲存一個數,因此它除了可以表示分數以外,也可以表示一個極大的整數。 91 | 92 | 以下是 C++ 常見的浮點數型態,注意 `double` 的有效位數並不比 `long long` 來得長。 93 | 94 | |資料型態|指數域長度(bit)|指數偏移量|小數域長度(bit)| 95 | |------|-----|-----|----| 96 | |float|8|127|23| 97 | |double|11|1023|52| 98 | 99 | ### 特殊值 100 | 101 | 前面有提到當 $k$ 位指數域的指數偏移量是 $b$ 時,$-b$ 和 $2^k-b-1$ 用於表示特殊值,而特殊值如下: 102 | 103 | #### 零與負零 104 | 指數為 $-b$ 的浮點數,符號位為正表示 $0$,為負則為 $-0$。因為一般小數域只儲存小數點後的位數,也就是自動默認小數點前有一個 $1$,因此 $0$ 這個數字必須要用特殊值來存。至於正零跟負零唯一的差別在於負零輸出的時候會有負號,把負數一直除以超大數字就會變成 -0,例如 `-1 / 1e200 / 1e200`,不過比較的時候是不會出事的。 105 | 106 | #### 正負無窮 107 | 指數為 $2^k-b-1$,小數域為 `0`,符號位為正表示正無窮,為負表示負無窮,需要它的話,在 C++ 中可以用 `INFINITY` 來得到,它在 `math.h` 裡面,只是在競程上沒啥用途,頂多就是除以 $0$ 會出現無窮,可以幫你 debug。 108 | 109 | #### NaN 110 | Not a Number 的縮寫,指數為 $2^k-b-1$,小數域不是 `0`,在反三角函數丟一些超出定義域的數字時會跑出這東西。 111 | 112 | ## 黑科技 113 | 114 | 有兩個很大的型態叫 `__int128` 和 `long double`,前者是 128 位元整數,可以存到大約 $10^38$ 的整數,在運算過程會溢位的時候很有用,不過 Windows 不能用,還有沒有內建輸入輸出,所以需要的話你可以自己寫,或是輸入輸出不超過 long long 範圍的話,也可以用 long long 輸入輸出,再轉過來或轉過去。 115 | 116 | 至於 `long double` 是一個非標準的浮點數,通常有 80 位元,指數域 15 位、小數域 64 位,也就是說它可以不失任何精度地存下 long long 範圍內的數字。既然它是非標準的東西,就代表它實際長怎樣依編譯器而定。它可以在 Windows 用、可以輸入輸出,可以當一般的浮點數型態用。 117 | -------------------------------------------------------------------------------- /_posts/note/binary/radix.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: 進位制 3 | --- 4 | # 進位制 5 | 6 | 平常我們用的數字系統是十進位制的,而計算機使用的是二進位制,而時、分、秒這樣的時間單位,用的是六十進位制……,這裡是進位制的轉換和一些性質。 7 | 8 | 接下來我會用 $(a)_n$ 這樣的方式來表示括號中的數字 $a$ 是用 $n$ 進位制表示的,例如 $(12345)_{10}$ 表示十進位的 $12345$,$(\text{ABCDE})_{16}$ 表示十六進位的 $\text{ABCDE}$,也就是十進位的 $703,710$。 9 | 10 | ## 基本轉換 11 | 12 | 一個十進位的數字 $a_{n}a_{n-1} \dots a_2a_1a_0$ ($a_i$ 表示一個位數)可以表示為: 13 | $$ 14 | a_n \times 10^n + a_{n-1} \times 10^{n-1} + \dots + a_2 \times 10^2 + a_1 \times 10^1 + a_0 \times 10^0 15 | $$ 16 | 舉例來說,$(1234)_{10}$可以表示為: 17 | $$ 18 | 1 \times 1000 + 2 \times 100 + 3 \times 10 + 4 \times 1 19 | $$ 20 | 21 | 相同的,非整數的十進位數字 $a_{n}a_{n-1} \dots a_1a_0.a_{-1}a_{-2}\dots a_{-(m-1)}a_{-m}$ 可以表示為: 22 | $$ 23 | a_n \times 10^n + \dots + a_0 \times 10^0 + a_{-1} \times 10^{-1} + \dots 10^{-m} 24 | $$ 25 | 也就是 26 | $$ 27 | \sum_{i=-m}^{n} a_i \times 10^i 28 | $$ 29 | 30 | 而 $k$ 進位制的數字 $a_{n} \dots a_0.a_{-1} \dots a_{-m}$ 表示的值就是: 31 | $$ 32 | \sum_{i=-m}^{n} a_i \times k^i 33 | $$ 34 | 35 | 舉例來說 $(101.11)_2$ 轉換為十進位會是: 36 | $$ 37 | 1 \times 2^2 + 0 \times 2^1 + 1 \times 2^0 + 1 \times 2^{-1} + 1 \times 2^{-2}\\ 38 | = 4 + 0 + 1 + 0.5 + 0.25 = 5.75 39 | $$ 40 | 而 $(ABC.DD)_{16}$ 轉換為十進位會是: 41 | $$ 42 | 10 \times 16^2 + 11 \times 16^1 + 12 \times 16^0 + 13 \times 16^{-1} + 13 \times 16^{-2} \\ 43 | = 2560 + 167 + 12 + 0.8125 + 0.05078125 = 2739.86328125 44 | $$ 45 | 46 | ## 浮點數精確性 47 | 48 | 在算十進位除法的時候,經常會出現循環小數,例如: 49 | $$ 50 | \begin{align*} 51 | \frac{1}{3}&=0.\overline{3} \\ 52 | \frac{1}{7}&=0.\overline{142857} 53 | \end{align*} 54 | $$ 55 | 仔細看一下,$(\frac{1}{7})_{10}=(7^{-1})_{10}=(0.1)_7$,由此可知,有些在十進位會出現循環小數的數值,在其他進位制可能就不會有循環小數的狀況,反過來也一樣,在十進位看起來很簡單的值,在其他進位制可能會是很複雜的循環小數,更重要的是,二進位很容易發生這種狀況: 56 | $$ 57 | \begin{align*} 58 | (0.55)_{10}&=(0.10\overline{0011})_2\\ 59 | (0.12)_{10}&=(0.\overline{00011110101110000101})_2 60 | \end{align*} 61 | $$ 62 | 這會導致一個很討厭的結果,在儲存浮點數時,只能儲存有限的位數,因此有小數時很容易發生不精準的狀況,在[整數與浮點數](/int-and-float)會介紹更多。 63 | 64 | ## $k^i$ 進位制的互相轉換 65 | 66 | $2^i$ 進位的每一位範圍是 $[0, 2^i-1]$,這恰好跟二進位的 $i$ 個 bit 能表示的範圍一樣,因此,在進行 $2^i$ 進位的互相轉換時,可以先轉成二進位。 67 | 68 | 舉例來說,$(147)_8$ 要轉換為四進位,那麼我們可以先轉成二進位,以 3 個 bit 表示八進位下的一個位數: 69 | 70 | | 1 | 4 | 7 | 71 | |:-:|:-:|:-:| 72 | |001|100|111| 73 | 74 | 接下來,用 2 個 bit 表示四進位下的一個位數: 75 | 76 | |01|10|01|11| 77 | |:-:|:-:|:-:|:-:| 78 | | 1| 2| 1| 3| 79 | 80 | 我們得出 $(147)_8=(1213)_4$ 81 | 82 | 事實上,$k^i$ 進位的一個位數都可以用 $k$ 進位的 $i$ 個位數去表示,因為 $k^i$ 進位的每一位數範圍都是 $[0, k^i-1]$,$k$ 進位的 $i$ 個位數能表示的範圍恰好也是這樣。 83 | 84 | ## 進位制的互轉 85 | 86 | 當一個數字乘以 $k$ 時,它在 $k$ 進位下的小數點會右移一位;而除以 $k$ 時,它在 $k$ 進位下的小數點會左移一位;對整數部分 $\bmod k$ 時,可以得到它的個位數。 87 | 88 | 這是一種常見的的十進位轉二進位的方法:有一個數字 $n$,整數部分是 $a$,小數部分是 $b$,將 $a$ 不斷除以 $2$ 並向下取整,直到 $a=0$ 為止,在這個過程中得到的餘數,倒過來寫就會是 $a$ 的二進位,例如: 89 | 90 | $$ 91 | \begin{align*} 92 | a&=19 \\ 93 | 19 \div 2 &= 9 \dots 1 \\ 94 | 9 \div 2 &= 4 \dots 1 \\ 95 | 4 \div 2 &= 2 \dots 0 \\ 96 | 2 \div 2 &= 1 \dots 0 \\ 97 | 1 \div 2 &= 0 \dots 1 98 | \end{align*} 99 | $$ 100 | 然後我們可以得到 $(19)_{10}=(10011)_2$。 101 | 102 | 可以這麼做的原因是,每次取 $a \bmod 2$ 時,我們會得到它在二進位下的個位數,接著把 $a$ 除以 $2$ 後,$a$ 在二進位下會右移一位,重複操作就可以依序得到從個位數開始往左的每一位數。 103 | 104 | 接著是小數部分 $b$,將 $b$ 不斷乘以 $2$,記錄它的整數部分後,把整數部分扣掉,再繼續重複這個動作,直到小數後沒東西為止,然後將記錄下來的整數部分依序寫下來就是 $b$ 的二進位。例如:(箭頭後方是整數部分) 105 | $$ 106 | \begin{align*} 107 | b&=0.375 \\ 108 | 0.375 \times 2 &= 0.75 \rightarrow 0\\ 109 | 0.75 \times 2 &= 1.5 \rightarrow 1\\ 110 | 0.5 \times 2 &= 1 \rightarrow 1\\ 111 | \end{align*} 112 | $$ 113 | 我們得到 $(0.375)_{10}=(0.011)_2$。 114 | 115 | 可以這麼做是因為,每次乘以 $2$ 時,$b$ 在二進位下的小數點後第一位會變成個位數,如此一來我們就可以依序得到小數點後第一位開始往右的位數。 116 | 117 | 至於 $n$,就把 $a$ 和 $b$ 加起來就好了,以上面的舉例來說,我們會得到 $(19.375)_{10}=(10011.011)_2$。 118 | 119 | 其實無論多少進位,都可以使用這種方式互相轉換,且這個方式無論在手動計算或寫程式計算都很方便,像這樣可以將 $n$ 轉為 $k$ 進位: 120 | ```cpp 121 | #include 122 | #include 123 | using namespace std; 124 | string convert(double n, int k){ 125 | int a = (int) n; 126 | double b = n - a; 127 | string ans; 128 | while(a > 0){ 129 | ans += a % k + '0'; 130 | a /= k; 131 | } 132 | reverse(ans.begin(), ans.end()); 133 | if(b == 0) return ans; 134 | ans += '.'; 135 | while(b > 0){ 136 | b *= k; 137 | ans += (int)b + '0'; 138 | b -= (int)b; 139 | } 140 | return ans; 141 | } 142 | ``` 143 | 144 | 需要注意的是,有可能會遇到某進位制無法精確表示的狀況(見上方的[浮點數精確性](#%E6%B5%AE%E9%BB%9E%E6%95%B8%E7%B2%BE%E7%A2%BA%E6%80%A7)),此時在 `while(b > 0)` 這個迴圈就會卡住,例如 `convert(0.375, 3)` 就會發生這個狀況,可以加個位數限制之類的,至於手算時會比較容易注意到這個問題,就沒有太大影響。 -------------------------------------------------------------------------------- /_posts/note/dp/aliens.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Aliens 優化 3 | --- 4 | # Aliens 優化 5 | 6 | Aliens 優化是 DP 優化的一種,先看例題:[AI-666 賺多少](https://zerojudge.tw/ShowProblem?problemid=c457),雖然這題有 Greedy 的作法,但我們要用 Aliens 優化來做。 7 | 8 | 先想想看如果沒有交易次數的限制可以怎麼做,令 $dp_0[i]$ 是到第 $i$ 個時間點結束時,手上沒有商品的最大獲利,而 $dp_1[i]$ 是手上有商品的最大獲利,$a[i]$ 是時間點 $i$ 時商品的價格,然後可以很簡單地得到轉移式: 9 | $$\begin{align*} 10 | dp_0[i] &= \max(dp_0[i - 1], dp_1[i - 1] + a[i]) \\ 11 | dp_1[i] &= \max(dp_1[i - 1], dp_0[i - 1] - a[i]) 12 | \end{align*}$$ 13 | 答案就是 $dp_0[N]$,算這個的同時也可以得到最大獲利需要的最少交易次數是多少。用相似的作法再加上一個維度,就可以算在交易次數限制下的最大獲利了,不過那樣的時間複雜度是 $O(NK)$,所以我們不會這樣做。 14 | 15 | 令 $f(k)$ 是做恰 $k$ 次交易時的最大獲利,雖然我們不能很快地算出 $f(k)$,但我們可以用 $O(N)$ 的時間算出 $\max\{f(k)\}$ 和得到極值發生的 $k$,也就是最大獲利和需要的交易次數。先知道這件事,待會會用到。 16 | 17 | $f(k)$ 有一個重要的性質是,它是一個凹函數(concave function),凹函數的意思是這個函數的切線斜率也就是差分(我們都在整數上做事)非嚴格遞減,用直覺的方式說明一下就是如果 $f(k-1) - f(k-2) < f(k) - f(k-1)$,也就是 $f(k)$ 多做的那次獲利比 $f(k-1)$ 多做的那次多,那麼我乾脆把 $f(k-1)$ 多做的那次交易換成 $f(k)$ 多做的那次,所以多出來的獲利一定會越來越少。注意到這樣的說法肯定是不嚴謹的,不過這種性質的證明通常不太容易,因此我們暫且先跳過這個性質的證明。 18 | 19 | 那麼凹函數可以幹嘛呢?我們要做一件神奇的事情,就是對每次交易多收 $p$ 元的手續費,令得到的新函數是 $f'(k)=f(k)-kp$,而 $f'(k)$ 也會是凹函數,因為: 20 | $$f'(k)-f'(k-1)=(f(k)-kp) - (f(k - 1)-(k-1)p)=f(k)-f(k-1)-p$$ 21 | 所以就只是 $f(k)$ 的差分全部減去 $p$ 而已,差分遞減這件事不會改變。 22 | 23 | 要計算 $\max\{f(k)-kp\}$ 和極值發生的 $k$ 很簡單,只要修改一下轉移式: 24 | $$dp_1[i] = \max(dp_1[i - 1], dp_0[i - 1] - a[i] - p)$$ 25 | 同樣可以 $O(N)$ 算出來。 26 | 27 | 接下來注意到一件事:對於一個凹函數,它的極值發生點就是第一個 $f'(k+1)-f'(k) \leq 0$ 的 $k$,而「把差分全部減去 $p$」就會改變這個位置,然後我們又可以輕鬆算出來極值和它的位置,還有顯然 $p$ 越大,極值就會在越左邊的地方,因此我們可以二分搜 $p$(為了實作方便,**我們讓這個 $p$ 是最小的那一個**,它會等於 $f(k+1)-f(k)$,讓 $f'(k+1)-f'(k)$ 剛好變 0),讓極值發生在我們想要的 $K$,就可以得到 $f(K)-Kp$,把 $Kp$ 加回去就是答案了。(這題有一個要特別注意的地方是如果本來極值位置就 $\leq K$,要直接輸出答案。) 28 | 29 | 示意圖,藍色是 $f'(k)$,紅色是 $f'(k+1)-f'(k)$,金色是極值的位置: 30 | 31 | 32 | 33 | 34 | 35 | 有一個特殊狀況是,因為我們是找「最大獲益需要的最少交易次數」,也就是最左邊的極值位置,而在 $f(K)-f(K-1)=f(K+1)-f(K)$,也就是 $f(K-1),f(K),f(K+1)$ 等差時,$K$ 永遠不會是差分第一個變成 $\leq 0$ 的點,也就不會是最左邊的極值位置。在這個時候,如果直接二分搜的話,會得到一個 $t$ 滿足 36 | $$p=f(t+1)-f(t)=f(t+2)-f(t+1)=\dots=f(K)-f(K-1)=f(K+1)-f(K)$$ 37 | 也就是 $t$ 到 $K$ 會是一條直線,且 $t$ 是整條等差的最左邊那個點,可以推得: 38 | $$\begin{align*} 39 | &f'(t+1)-f'(t)=\dots=f'(K+1)-f'(K) \\ 40 | =&f(t+1)-f(t)-p=\dots=f(K+1)-f(K)-p \\ 41 | =&p-p=0 42 | \end{align*}$$ 43 | 也就是 $f'(t)=f'(t+1)=\dots=f'(K)$,因此 $f'(t)+Kp=f'(K)+Kp$,直接加 $Kp$ 也會是好的,結論就是完全不用理會這個狀況。 44 | 45 | 至於二分搜的上下界呢?因為我們希望最後找到 $p=f(K+1)-f(K)$,所以這個數字一定要在二分搜的範圍裡,因此二分搜範圍就和差分的範圍一樣。~~如果不會算的話也可以隨便開一個很大的範圍,反正二分搜很快,TLE 再縮小就好。~~ 46 | 47 | 時間複雜度是 $O(N \log C)$,比 $O(NK)$ 好多了。 48 | 49 | ## 總結 50 | 51 | Aliens 優化就是用這種「移動極值」的想法,來把本來要多開一個維度的事用二分搜做掉。如果用互動題來講,會像是這樣: 52 | 53 | > 有一個凹函數 $f(k)$,你不知道它長什麼樣子,但是你可以把一個數字 $p$ 丟進一台機器裡,它會告訴你最大的 $f(k)-kp$ 是多少,以及讓這個值最大的 $k$ 為何,求 $f(K)$。 54 | 55 | 除了凹函數可以用以外,凸函數(差分非嚴格遞增的函數)也可以用 Aliens 優化做,因為它和凹函數同樣有差分具單調性的特性,這樣的題目會變成是求最小值,而極值發生的位置變成第一個 $f(k+1)-f(k) \geq 0$ 的地方,還有 $f(k)-kp$ 要換成 $f(k)+kp$(這樣才不用改太多東西 (?)),就是想成它是一個凹函數倒過來就好了。 56 | 57 | :::warning 58 | 如果 $p$ 可能是負數,二分搜的時候注意除法會向零取整。 59 | ::: 60 | 61 | ### 實作細節 62 | 63 | 實作就二分搜而已,但有一件特別重要的事情是 $p$ 是要找最小那一個,在這個時候我們一定會找到 $p=f(K+1)-f(K)$,剛剛我們都是以這件事為前提做事的,現在來仔細分析一下不這樣做會怎樣。 64 | 65 | 我們的主要目標是「找到 $p$ 使得 $f(k)-kp$ 的極值在 $K$」,在 $f(K)-f(K-1) \neq f(K+1) - f(K)$ 時,而這個 $p$ 的範圍可以是 $[f(K+1)-f(K), f(K)-f(K-1))$ 中的任何一個數,在這種時候無論我們找到的 $p$ 是多少,只要落在這個範圍就會是好的,因為極值一定在 $K$。 66 | 67 | 而當 $f(K)-f(K-1) = f(K+1) - f(K)$ 就會發生前面有提到的,$K$ 不可能是極值位置的狀況發生,這個時候,如果你的二分搜長這樣: 68 | 69 | ```cpp= 70 | while(l < r){ 71 | int m = (l + r + 1) / 2; 72 | if(calc(m) >= K) l = m; // calc(p) = f(k)-kp 的最左邊極值位置 73 | else r = m - 1; 74 | } 75 | ``` 76 | 77 | 也就是你試圖去找到最大的好的 $p$,而不是最小的,這樣你其實會得到 $p=f(K+1)-f(K)-1$,因為一旦 $p \geq f(K+1)-f(K)$,極值位置就會跑到 $K$ 的左邊去,這樣子 $f'(K+1)-f(K)$ 會是 1,且得到的極值位置會是整條等差最右邊那個點,跟我們剛剛想要的 $f'(K+1)-f(K)=0$ 完全不一樣,直接加 $Kp$ 回去也會是錯的。 78 | 79 | 所以二分搜要長這樣才對: 80 | ```cpp= 81 | while(l < r){ 82 | int m = (l + r) / 2; 83 | if(calc(m) <= K) r = m; 84 | else l = m + 1; 85 | } 86 | ``` 87 | 88 | 其實也不是一定要這樣,如果你拿上面兩份程式碼各跑一次,會得到兩個 $p$,假設找最小版是 $p_1$、找最大版是 $p_2$,而得到的極值位置會在整條等差的最左和右邊,假設是 $v_1$ 和 $v_2$,你可以得到 $f(v_1)$ 和 $f(v_2)$,反正它們兩個之間等差且 $K$ 在中間,那就用它們算出 $f(K)$ 就好: 89 | $$f(K)=f(v_1)+\frac{f(v_2)-f(v_1)}{v_2-v_1} \times (K- v_1)$$ 90 | 但是直接加 $Kp$ 的作法顯然輕鬆許多,所以就好好找最小的 $p$ 吧。 91 | 92 | ### AI-666 賺多少的凹函數證明 93 | 94 | TODO 95 | -------------------------------------------------------------------------------- /_posts/note/geometry/convex-hull.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: 凸包 3 | --- 4 | # 凸包 5 | 6 | 你有一大堆點,然後你要找出一個可以圍住這些點且面積最小的凸多邊形,這個凸多邊形稱為凸包。 7 | 8 | 顯而易見,如果要面積最小,那凸包的頂點勢必得是這一大堆點的其中幾個點,你也可以想成是用一條橡皮筋把這些點圈起來。 9 | 10 | 11 | 12 | 那麼,如何有效率地把凸包蓋出來呢? 13 | 14 | 先把各個點按 $x$ 座標由小到大排序,$x$ 座標相同則再按 $y$ 座標由小到大排序,排序之後的點順序會是由左至右、由下至上,這樣一來我們就可以按這個順序遍歷這些點,這種往固定方向掃描的方式,稱為掃描線。 15 | 16 | 17 | 18 | 知道點的左右上下關係可以幹嘛? 19 | 20 | 先討論一件事情:有一個凸多邊形,它的頂點已經按逆時針順序排好了,依序是 $p_0p_1p_2 \dots p_{n-1}$,那麼 $p_i$ 和 $p_{i+1}$ 與 $p_{i+2}$ 的關係會是什麼(令 $p_i=p_{i \bmod n}$)?既然是凸多邊形,那麼它的邊應該要是一直往同個方向轉彎的,而如果將邊逆時針排序,這個邊的斜率也應該是一直往逆時針方向轉彎,顯然點也會是這樣,因此: 21 | $$\overrightarrow{p_ip_{i+1}}\times\overrightarrow{p_ip_{i+2}} > 0$$ 22 | 23 | 24 | 25 | 再來,我們把凸包分成上下兩部分:上凸包和下凸包,以極左和極右點分割,如果極左點或極右點有兩個(最多只會有兩個),上面那個屬於上凸包,下面那個屬於下凸包,否則極左點必屬於下凸包,極右點必屬於上凸包。 26 | 27 | 28 | 29 | 顯然,上或下凸包中不會有 $x$ 座標相同的頂點,因此在上或下凸包中,每個頂點都能分別出左右的關係,並且你會發現,如果把點按逆時針排序,在下凸包中的點也是由左而右排序的、在上凸包的點也是由右而左排序的。 30 | 31 | 這樣一來左右關係就有用了,先用由左而右的掃描線把下凸包做出來,再用由右而左的掃描線把上凸包做出來,就可以得到整個凸包。 32 | 33 | 這整個流程可以用一個 stack 來實作,在處理一個點的時候,我們嘗試把它加進凸包裡,此時這個點是 $p_{i+2}$,堆頂的點是 $p_{i+1}$,堆頂再往下一個點是 $p_{i}$,把這些點代入剛剛的式子,符合條件或者 stack 大小小於 $2$ 時就停止,否則就把堆頂 pop,然後繼續重複,結束之後就把目前處理中的點放入堆頂。 34 | 35 | 在做下凸包的時候,先從最左邊且最下面的點開始做上述動作,做到最後,堆頂的點應該會是最右邊且最上面的點,把它 pop 掉,因為它應該屬於上凸包;做上凸包的時候,從最右邊且最上面的點開始做,最後堆頂會是最左且最下的點,把它 pop 掉後,這兩個接起來就是完整的凸包。 36 | 37 | 因為要用到堆頂往下一個點,所以 stack 用 vector 來實作。 38 | 39 | ```cpp 40 | #include 41 | 42 | #define mp(a, b) make_pair(a, b) 43 | #define pb(a) push_back(a) 44 | #define F first 45 | #define S second 46 | 47 | using namespace std; 48 | 49 | template 50 | pair operator-(pair a, pair b){ 51 | return mp(a.F - b.F, a.S - b.S); 52 | } 53 | 54 | template 55 | T cross(pair a, pair b){ 56 | return a.F * b.S - a.S * b.F; 57 | } 58 | 59 | template 60 | vector> getConvexHull(vector>& pnts){ 61 | 62 | int n = pnts.size(); 63 | sort(pnts.begin(), pnts.end()); 64 | 65 | vector> hull; 66 | 67 | for(int i = 0; i < 2; i++){ 68 | int t = hull.size(); 69 | for(pair pnt : pnts){ 70 | while(hull.size() - t >= 2 && cross(hull.back() - hull[hull.size() - 2], pnt - hull[hull.size() - 2]) <= 0) 71 | hull.pop_back(); 72 | hull.pb(pnt); 73 | } 74 | hull.pop_back(); 75 | reverse(pnts.begin(), pnts.end()); 76 | } 77 | 78 | return hull; 79 | } 80 | ``` 81 | 82 | 這個演算法叫 Andrew's monotone chain,另一種比較常聽到的凸包演算法是 Graham's scan,有興趣可以自己查。 83 | 84 | ## 練習題 85 | 86 | - [TIOJ 1178](https://tioj.ck.tp.edu.tw/problems/1178) - 給一堆點,求凸包的頂點數量 87 | - [ZeroJudge a871](https://zerojudge.tw/ShowProblem?problemid=a871) - 求凸多邊形面積(點不一定照順序) 88 | - [ZeroJudge d546](https://zerojudge.tw/ShowProblem?problemid=d546) - 求多邊形面積和凸包面積差 89 | - [TIOJ 1280](https://tioj.ck.tp.edu.tw/problems/1280) - 給你一堆點,求所有點對連成的直線所能圍出的最大面積 90 | 91 | # 旋轉卡尺 92 | 93 | 用旋轉兩條平行線、夾住一堆點,看在線上的點是哪些,就叫旋轉卡尺。 94 | 95 | 96 | 97 | 旋轉線、夾點感覺很麻煩,是不是要用到什麼角度的東西啊?其實不用,先來分析一下問題,用兩條平行線夾一堆點,那麼平行線只會碰到凸包上的點而已,所以不在凸包上的點都可以先忽略: 98 | 99 | 100 | 101 | 過一個點的直線有很多條,但是過一個線段的直線只有一條,所以先枚舉線段,再去找和它平行的直線應該會夾到哪個點,這樣問題就簡單多了。要找平行線會碰到哪個點,顯然離線段最遠的點就是了。 102 | 103 | 不過算距離是另一個問題,聽起來也很麻煩,但其實很簡單。一個點距離一條直線的距離,等同於過該點在直線上作垂線段的長,而一開始選定的線段作為底、垂線段長作為高,那麼就可以得到一個平行四邊形面積了,且底的長是固定的,只要枚舉最遠點,就等同於枚舉高,而得出面積最大的,就是我們要的最遠點了。 104 | 105 | 106 | 107 | 上圖中,選定兩個紅色點所連成的線段為底,然後枚舉各個頂點取高,得出藍色垂線是最長的,因此藍色點就是距離紅色線段最遠的點。 108 | 109 | 這就是旋轉卡尺的基礎應用——最遠點對,找到距離每一線段最遠的點,再取該點與線段兩端點的距離取最大值,這樣就可以得出所有點中最遠的點對為何。 110 | 111 | 硬要這麼做的方式,時間複雜度是 $O(N^2)$,$N$ 是凸包上點的數量(不計蓋凸包的複雜度),枚舉線段是 $O(N)$,再枚舉一個點要再乘上 $O(N)$。 112 | 113 | 這不夠快,我們需要更有效率的方式。 114 | 115 | 仔細觀察一下,點和線段的距離有一個規律——先漸大,到一個最大值,再漸小: 116 | 117 | 118 | 119 | 我們發現它會呈現一個單峰函數,也就是一個先遞增、再遞減的函數,這樣我們就可以用三分搜找到最高點了,這樣三分搜一次的複雜度是 $O(\log N)$,再乘上點的數量,就是 $O(N\log N)$。 120 | 121 | 這樣子還是不夠快,前面提到旋轉卡尺是「旋轉兩條平行線」,剛剛的動作都是旋轉其中一條,再去搜尋另一條,那我們可不可以在旋轉其中一條的同時,把另一條一起旋轉?答案是:可以。 122 | 123 | (以下的轉都是指往同一個方向轉) 124 | 先找到距離第一條邊最遠的點,過前者的線稱為第一條平行線,過後者的稱為第二條平行線,接下來我們轉動第一條平行線,也就是把它轉到第二條線段上,而第二條平行線不要動,會發現,第一條平行線離第二條平行線那個點近了一些,接著再轉第二條平行線,也就是把它轉到下一個點上,那麼距離會變遠。 125 | 126 | 也就是,可以在不重新來過的情況下,找到單峰函數的最高點,會發現這樣就是把兩條平行線繞一圈,因此這樣的複雜度是 $O(N)$。 127 | 128 | ## 最大三角形 129 | 130 | 給你一堆點,請找出這些點所能構成的面積最大的三角形。 131 | 132 | 用類似最遠點對的想法來想,先 $O(N^2)$ 枚舉兩個頂點,然後再用旋轉卡尺去找第三個頂點。首先,先枚舉第一個點,然後在枚舉第二個點的同時,第三個點可以跟著往下轉,這樣就可以 $O(N^2)$ 解決這件事。 133 | 134 | ## 練習題 135 | 136 | - [ZeroJudge b288](https://zerojudge.tw/ShowProblem?problemid=b288) - 最大三角形 137 | - [TIOJ 1105](https://tioj.ck.tp.edu.tw/problems/1105) - 最遠點對 138 | -------------------------------------------------------------------------------- /_posts/note/geometry/vector-application.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: 向量應用 3 | --- 4 | # 向量應用 5 | 6 | ## 三點共線 7 | 8 | 9 | 10 | 有三個點 $P_1$、$P_2$、$P_3$,我們想知道這三個點是否共線,顯而易見,隨便選一個點,作指向另外兩個點的向量,這兩個向量所夾的平行四邊形面積應該要是 $0$,所以,假設我們選的點是 $P_2$: 11 | $$\overrightarrow{P_2P_1}\times\overrightarrow{P_2P_3}=0$$ 12 | 13 | ```cpp= 14 | template 15 | bool collinearity(pair p1, pair p2, pair p3){ 16 | return cross(p1, p2) * cross(p1, p3) == 0; 17 | } 18 | ``` 19 | 20 | ## 點是否在線段上 21 | 22 | 23 | 24 | 有三個點 $A$、$B$ 和 $P$,我們想知道 $P$ 是否在 $\overline{AB}$ 上,顯然三個點必須共線,所以首要條件是: 25 | $$\overrightarrow{PA}\times\overrightarrow{PB}=0$$ 26 | 27 | 在確定共線後,我們還不能確定 $P$ 是否在 $\overline{AB}$ 上,所以我們還要判斷 $A$、$B$ 是否在 $P$ 的兩側,這時可以用內積: 28 | $$\overrightarrow{PA}\cdot\overrightarrow{PB} \leq 0$$ 29 | $=0$ 的狀況會發生在 $P$ 是 $A$ 或 $B$ 的時候。 30 | 31 | ```cpp= 32 | template 33 | bool inLine(pair a, pair b, pair p){ 34 | return collinearity(a, b, p) && dot(a - p, b - p) <= 0; 35 | } 36 | ``` 37 | 38 | ## 線段相交 39 | 40 | 有四個點 $A$、$B$、$C$、$D$,請檢查線段 $\overline{AB}$ 是否和線段 $\overline{CD}$ 相交。 41 | 42 | 分成幾種情況: 43 | 44 | 1. 交點不是任一線段的端點 45 | 2. 交點是其中一線段的端點或兩個線段的端點 46 | 3. 兩個線段重疊 47 | 4. 兩線段不相交且兩線段不共線 48 | 5. 兩線段不相交但兩線段共線 49 | 50 | 先講最間單的,交點不是任一線段的端點: 51 | 52 | 53 | 很顯然地,如果 $\overline{AB}$ 與 $\overline{CD}$ 相交,那麼點 $A$、$B$ 會分別在 $\overline{CD}$ 的兩側,而點 $C$、$D$ 也會分別落在 $\overline{AB}$ 的兩側。要判斷方向,外積就派上用場了: 54 | $$(\overrightarrow{AB}\times\overrightarrow{AC})(\overrightarrow{AB}\times\overrightarrow{AD})<0$$ 55 | $$(\overrightarrow{CD}\times\overrightarrow{CA})(\overrightarrow{CD}\times\overrightarrow{CB})<0$$ 56 | 這兩個條件都符合,就表示兩線段相交且交點不是任一線段的端點。 57 | 58 | 如果 $C$、$D$ 其中一點在 $\overline{AB}$ 上,那麼 $\overrightarrow{AB}\times\overrightarrow{AC}$ 和 $\overrightarrow{AB}\times\overrightarrow{AD}$ 其中一個應該要是 $0$,但是不能簡單地用 $(\overrightarrow{AB}\times\overrightarrow{AC})(\overrightarrow{AB}\times\overrightarrow{AD})=0$ 來判斷,因為會有其中一點和點 $A$、$B$ 共線但沒有交點的狀況,因此最好的方式是判斷有沒有哪個端點落在另一個線段上,這樣可以解決剩下的狀況。 59 | 60 | ```cpp= 61 | template 62 | bool intersect(pair a, pair b, pair c, pair d){ 63 | return (cross(b - a, c - a) * cross(b - a, d - a) < 0 && cross(d - c, a - c) * cross(d - c, b - c) < 0) 64 | || inLine(a, b, c) || inLine(a, b, d) || inLine(c, d, a) || inLine(c, d, b); 65 | } 66 | ``` 67 | 68 | ## 線段交點 69 | 70 | 在知道兩個線段相交後,可能還要進一步求交點(記得先把四點共線,也就是兩線段重疊的狀況判掉)。 71 | 72 | 73 | 74 | $P$ 是兩線段交點,先令 $\overrightarrow{A}+i\overrightarrow{AB}=\overrightarrow{P}$,$i$ 是一個純量,接著我們知道 $\overrightarrow{P}-\overrightarrow{C}=\overrightarrow{A}+i\overrightarrow{AB}-\overrightarrow{C}$,而 $C$、$P$、$D$ 三點共線,因此: 75 | $$\overrightarrow{CP}\times\overrightarrow{CD}=0$$ 76 | $$(\overrightarrow{A}+i\overrightarrow{AB}-\overrightarrow{C}) \times \overrightarrow{CD}=0$$ 77 | $$(\overrightarrow{A}-\overrightarrow{C}+i\overrightarrow{AB}) \times \overrightarrow{CD}=0$$ 78 | $$(\overrightarrow{CA}+i\overrightarrow{AB}) \times \overrightarrow{CD} =0$$ 79 | $$\overrightarrow{CA} \times \overrightarrow{CD} + i\overrightarrow{AB} \times \overrightarrow{CD} = 0$$ 80 | $$\overrightarrow{CA} \times \overrightarrow{CD} = -(i\overrightarrow{AB} \times \overrightarrow{CD})$$ 81 | $$\frac{\overrightarrow{CA}\times\overrightarrow{CD}}{\overrightarrow{CD}\times\overrightarrow{AB}}=i$$ 82 | 83 | 把 $i$ 求出來後,$\overrightarrow{A}+i\overrightarrow{AB}$ 就是 $P$ 了。 84 | 85 | ```cpp= 86 | template 87 | pair intersection(pair a, pair b, pair c, pair d){ 88 | assert(intersect(a, b, c, d)); 89 | return a + cross(a - c, d - c) * (b - a) / cross(d - c, b - a); 90 | } 91 | ``` 92 | 93 | ## 凸多邊形包含測試 94 | 95 | 給一個凸多邊形和一個點,問這個點有沒有在這個凸多邊形裡。 96 | 97 | 先讓問題簡單一些:只考慮三角形就好。令三角形三個點為 $A$、$B$ 和 $C$,先選定 $A$,接著作向量 $\overrightarrow{AB}$、$\overrightarrow{AC}$,如果點 $P$ 在這個三角形內(邊上也算),那麼應該會滿足這個條件: 98 | $$(\overrightarrow{AP} \times \overrightarrow{AB})(\overrightarrow{AP} \times \overrightarrow{AC}) \leq 0$$ 99 | 100 | 也就是說,由 $\overrightarrow{AP}$ 的方向往 $\overrightarrow{AB}$ 和 $\overrightarrow{AC}$ 轉,應該要是不同向。而如果 $P$ 在 $\overline{AB}$ 或 $\overline{AC}$ 上,則上面那個式子會等於 $0$。 101 | 102 | 不過會滿足上面那個式子的 $P$ 範圍其實是下圖紅色部分,等於 $0$ 的話,$P$ 會在紅色邊上: 103 | 104 | 105 | 106 | 但如果選定三個點都做一次這個判定,上述那樣的範圍聯集後就會恰好是這個三角形的範圍了: 107 | 108 | 109 | 110 | 如果滿足 $(\overrightarrow{AP} \times \overrightarrow{AB})(\overrightarrow{AP} \times \overrightarrow{AC}) \leq 0$,那 $P$ 會在紅色區塊,等於 $0$ 是在紅色區塊邊界上; 111 | 如果滿足 $(\overrightarrow{BP} \times \overrightarrow{BA})(\overrightarrow{BP} \times \overrightarrow{BC}) \leq 0$,那 $P$ 會在綠色區塊,等於 $0$ 是在綠色區塊邊界上; 112 | 如果滿足 $(\overrightarrow{CP} \times \overrightarrow{CA})(\overrightarrow{CP} \times \overrightarrow{CB}) \leq 0$,那 $P$ 會在藍色區塊,等於 $0$ 是在藍色區塊邊界上。 113 | 114 | 從圖上可以看出來,滿足以上那三個式的地方只有三角形內而已。如果以上有任何一式等於 $0$,那麼 $P$ 會在這個三角形邊上。 115 | 116 | 把這個想法放到凸多邊形上,會發現:只要這個凸多邊形的每一頂點 $P_0$ 和它相鄰的兩個頂點 $P_1$ 和 $P_2$ 都滿足 $(\overrightarrow{P_0P} \times \overrightarrow{P_0P_1})(\overrightarrow{P_0P} \times \overrightarrow{P_0P_2}) \leq 0$ (相同地,等於 $0$ 就表示在邊界上),那麼 $P$ 就會在這個凸多邊形內。 117 | 118 | ```cpp 119 | template 120 | T inPolygon(vector> polygon, pair p){ 121 | for(int i = 0; i < polygon.size(); i++) 122 | if(cross(p - polygon[i], polygon[(i - 1 + polygon.size()) % polygon.size()] - polygon[i]) * 123 | cross(p - polygon[i], polygon[(i + 1) % polygon.size()] - polygon[i]) > 0) 124 | return false; 125 | return true; 126 | } 127 | ``` 128 | 129 | ## 三角形面積 130 | 131 | 給一個三角形 $\triangle ABC$,求此三角形面積。 132 | 133 | 這有兩種常見作法,第一種是用 Heron's formula($a$、$b$、$c$ 是三邊邊長): 134 | $$s=\frac{a+b+c}{2}$$ 135 | $$\triangle ABC=\sqrt{s(s-a)(s-b)(s-c)}$$ 136 | 137 | 不過要這麼做,就得先把邊長算出來,開根號的過程中也很有可能出現誤差(當然如果題目給的是邊長而非頂點座標就只能這樣做)。 138 | 139 | 用向量的作法是,還記得外積的絕對值等同於兩向量夾的平形四邊形面積嗎?平行四邊形面積的一半就是三角形面積了,所以這樣就可以得到三角形面積: 140 | $$\triangle ABC = \frac{|\overrightarrow{AB} \times \overrightarrow{AC}|}{2}$$ 141 | 142 | ```cpp 143 | template 144 | T triangleArea(pair a, pair b, pair c){ 145 | return abs(cross(b - a, c - a)); 146 | } 147 | ``` 148 | 149 | ## 多邊形面積 150 | 151 | 給一個 $n$ 邊形 $P_0P_1P_2 \dots P_{n-1}$(頂點按逆時針排序),求此多邊形面積。 152 | 153 | 要算多邊形面積,最直觀的方式就是把多邊形切成一堆三角形,這樣面積會好算很多,最簡單的作法是選一個共同頂點,然後以每兩個相鄰頂點為另外兩個點作三角形。在做這件事之前,得先選一個點作這些三角形的共同頂點,不過共同頂點是哪個點其實無所謂,因為外積的結果是「有向」的,只要將頂點逆時針排序,再選隨便一個共同頂點 $P$,那麼這個多邊形的面積就是(令 $P_n=P_0$): 154 | 155 | $$\frac{1}{2} \sum_{i=0}^{n-1} \overrightarrow{PP_i} \times \overrightarrow{PP_{i+1}}$$ 156 | 157 | 舉例來說,以原點為共同頂點,下圖中紅色部分的面積是負的,而藍色是正的(注意有重疊): 158 | 159 | 160 | 不管共同頂點在哪裡,都可以用這個方法算出正確的面積。 161 | 162 | 這個作法有個名字,叫測量師公式: 163 | 164 | $$\frac{1}{2}\sum\left|\begin{array}{cc} 165 | x_i & x_{i+1} \\ 166 | y_i & y_{i+1} \\ 167 | \end{array}\right|=\frac{1}{2}\sum_{i=1}x_iy_{i+1}-y_ix_{i+1}$$ 168 | 169 | ```cpp= 170 | template 171 | T area(vector> p){ 172 | T ans = 0; 173 | for(int i = 0; i < p.size(); i++) ans += cross(p[i], p[(i + 1) % p.size()]); 174 | return ans / 2; 175 | } 176 | ``` 177 | 178 | # 練習題 179 | - [TIOJ 1205](https://tioj.ck.tp.edu.tw/problems/1205) - 給你一堆點,求有幾個直角三角形 180 | - [ZeroJudge d489](https://zerojudge.tw/ShowProblem?problemid=d489) - 給你三角形三邊長,求面積。 181 | - [ZeroJudge d269](https://zerojudge.tw/ShowProblem?problemid=d269)/[UVa 11579](https://onlinejudge.org/index.php?option=com_onlinejudge&Itemid=8&page=show_problem&problem=2626) - 給你一堆邊長,求這些邊長可圍出的最大三角形 182 | - [AtCoder ABC 139 F](https://atcoder.jp/contests/abc139/tasks/abc139_f) - 給你一堆向量,把其中一些加起來使得得到的向量長度最長 -------------------------------------------------------------------------------- /_posts/note/geometry/vector.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: 向量 3 | --- 4 | # 向量 5 | 6 | ## 何謂向量 7 | 8 | 向量(Vector)表示特定的長度和方向,簡單來說,可以想成向量是一個箭頭。例如力、位移都算是向量。 9 | 10 | 向量可以是任何維度的,也就是說,有二維向量、也有三維向量、一維向量……,然後一個 $n$ 維向量可以用 $n$ 個數字來表示,第 $i$ 個數字表示要往第 $i$ 個維度的正向走多少距離(如果是負數,就是往負向走),而這個向量可以被看成是一個從起點指向終點的箭頭,舉例來說: 11 | 12 | 13 | 這是一個二維向量,它表示向著 $x$ 軸正向走 $2$,並向著 $y$ 軸正向走 $3$,所以如果從原點開始走,會走到點 $(2,3)$。 14 | 15 | 向量有幾種表示方式,有把數字放在矩陣裡,寫成 16 | $\left[ \begin{array}{c} 17 | v_1\\ 18 | v_2\\ 19 | \vdots 20 | \end{array} 21 | \right ]$ 22 | 或 23 | $\left[ \begin{array}{ccc} 24 | v1 & v2 & \dots 25 | \end{array} 26 | \right ]$ 27 | 這種形式的,也有放在 tuple 裡,寫成 $(v_1, v_2, \dots)$ 的,如果沒有特別需要用到矩陣,通常會用 tuple 來表示。 28 | 29 | 一個 $n$ 維向量 $(v_1,v_2,\dots,v_n)$ 的長度可以用畢氏定理來算: 30 | $$\sqrt{\sum_{i=1}^n v_i^2}$$ 31 | 32 | 長度為 $0$ 的向量稱為「零向量」,零向量可以是任意方向的。為了方便,除非特別註明,接下來提到的向量都不包含零向量。 33 | 34 | 基本符號: 35 | 36 | - $\overrightarrow{AB}$,也就是 $\overrightarrow{B}-\overrightarrow{A}$,$A$、$B$ 是點,表示由 $A$ 指向 $B$ 的向量。(注意向量沒有特定的起終點,只能規定它的方向和長度) 37 | - $\overrightarrow{A}$,$A$ 是點,等同於 $\overrightarrow{OA}$,$O$ 是原點。 38 | - $|\overrightarrow{v}|$,向量 $\overrightarrow{v}$ 的長度。 39 | 40 | 41 | ## 基本運算 42 | 43 | 可以發現到,由原點指向某個點 $(x, y)$ 的向量,就是 $(x, y)$,所以也可以把點想成是向量、也可以把向量想成是點,這會有助於理解接下來的東西。 44 | 45 | ### 加減 46 | 47 | 兩個向量可以相加得到新的向量,就把所有維度的分量分別相加就好: 48 | $$(u_1,u_2,u_3,\dots) + (v_1,v_2,v_3,\dots) = (u_1 + v_1, u_2 + v_2, u_3 + v_3, \dots)$$ 49 | 50 | 例如,$(2,3)+(3,1)=(5,4)$: 51 | 52 | 53 | 54 | 兩個向量相加等同於兩個力的合力,可以發現到這張圖是個平行四邊形。 55 | 56 | 至於減法,就移項一下就可以暸解要怎麼做了,也可以想成是一個向量加上另一個向量的反向,或者是把兩個向量想成點,$\overrightarrow{A}-\overrightarrow{B}=\overrightarrow{BA}$,也就是一個從點 $B$ 指向點 $A$ 的向量,例如, $(2, 3)-(3, 1)=(-1, 2)$: 57 | 58 | 59 | 60 | ### 純量乘法 61 | 62 | 沒有方向的量稱為純量,像是 $1$、$\frac{1}{2}$、$\pi$ 這些數字都是純量。向量可以乘上一個純量,得到一個新的向量,也就是將這個向量的長度乘上某一個數字,得到一個新的向量,這個動作非常簡單,就把每個維度的分量都乘上這個純量就好: 63 | $$i(v_1, v_2, v_3, \dots)=(iv_1, iv_2, iv_3, \dots)$$ 64 | 65 | 例如,$2 \times (2,3)=(4,6)$: 66 | 67 | 68 | 69 | ### 內積(點積) 70 | 71 | 這是向量特有的運算,兩個夾角是 $\theta$ 的 $n$ 維向量 $\overrightarrow{u}=(u_1,u_2,\dots,u_n)$ 和 $\overrightarrow{v}=(v_1,v_2,\dots,v_n)$ 的內積記作 $\overrightarrow{u} \cdot \overrightarrow{v}$,結果是一個純量: 72 | $$\overrightarrow{u} \cdot \overrightarrow{v} = |\overrightarrow{u}||\overrightarrow{v}|\cos\theta = \sum_{i=1}^{n} u_iv_i$$ 73 | 74 | 其實就是把每一個維度的分量長相乘後相加,例如二維向量 $(x_1, y_1) \cdot (x_2, y_2) = x_1x_2 + y_1y_2$。意義是作一個 $\overrightarrow{u}$ 在 $\overrightarrow{v}$ 上垂直投影的向量,然後將這個向量的長和 $\overrightarrow{v}$ 的長相乘。 75 | 76 | 77 | 78 | (內積是 $|\overrightarrow{u}| \cos \theta$ 再乘上 $|\overrightarrow{v}|$,你想要 $\theta$ 是裡面那個角還是外面那個角都沒差,畢竟 $\cos \theta=\cos(2\pi-\theta)$。如果夾角超過直角,那麼投影會在另一邊,此時 $|\overrightarrow{u}|\cos\theta$ 會是負數。) 79 | 80 | $\overrightarrow{F} \cdot \overrightarrow{S}$ 在力學上的意義是,對一個物體作一個力 $\overrightarrow{F}$,而物體的位移是 $\overrightarrow{S}$,則 $\overrightarrow{F} \cdot \overrightarrow{S}$ 是 $\overrightarrow{F}$ 對這個物體所作的功。 81 | 82 | 既然內積只是多維版本的乘法,那內積也和乘法一樣,有交換律、結合律且對加法有分配律。 83 | 84 | 接下來針對兩個向量的夾角 $\theta$ 大小來討論內積的結果為何: 85 | 86 | - $\theta = 90^\circ = \frac{1}{2}\pi$ 87 | 顯而易見地,如果作用在一個物體上的力和物體位移方向垂直,那麼這個力對這個物體不作功,且 $\cos 90^\circ=\cos \frac{1}{2}\pi$ 也確實為 $0$,因此我們知道,兩個垂直的向量內積是 $0$。 88 | 89 | - $\theta = 0^\circ = 0$ 90 | 如果作用在一個物體上的力和物體位移方向相同,那麼這個力作的功等同於力的大小乘上位移距離,同樣 $\cos 0^\circ = \cos 0$ 也是 $1$,因此兩個方向相同的向量 $\overrightarrow{u}$、$\overrightarrow{v}$ 的內積是 $|\overrightarrow{u}||\overrightarrow{v}|$。 91 | 92 | - $\theta = 180^\circ = \pi$ 93 | 如果作用在一個物體上的力和物體位移方向相反,那麼這個力作的會是負功,且這個負功的大小等於力的大小乘上位移距離,同樣 $\cos 180^\circ=\cos\pi$ 是 $-1$,因此兩個方向相反的向量 $\overrightarrow{u}$、$\overrightarrow{v}$ 內積是 $-|\overrightarrow{u}||\overrightarrow{v}|$。 94 | 95 | - $0^\circ=0 < \theta < 90^\circ=\frac{1}{2}\pi$ 96 | 也就是兩個向量方向在同一邊的時候,顯然如果作用在一個物體上的力和物體位移方向在同一邊,那麼這個力作的會是正功,同樣 $\cos \theta$ 的範圍會是 $(0,1)$,因此兩個夾角是 $\theta$、方向在同一邊的向量 $\overrightarrow{u}$、$\overrightarrow{v}$ 內積會是一個正數且 $0 < \overrightarrow{u} \cdot \overrightarrow{v} < |\overrightarrow{u}||\overrightarrow{v}|$。 97 | 98 | - $90^\circ=\frac{1}{2}\pi < \theta < 180^\circ=\pi$ 99 | 也就是兩個向量方向在不同邊的時候,如果作用在一個物體上的力和物體位移方向在不同邊,那麼這個力作的會是負功,同樣 $\cos \theta$ 的範圍會是 $(-1,0)$,因此兩個夾角是 $\theta$、方向在不同邊的向量 $\overrightarrow{u}$、$\overrightarrow{v}$ 內積會是一個負數且 $-|\overrightarrow{u}||\overrightarrow{v}| < \overrightarrow{u} \cdot \overrightarrow{v} < 0$。 100 | 101 | 兩向量在不同夾角時的內積: 102 | 103 | 104 | 105 | 兩個長度為 $1$ 的向量在不同夾角時的內積($x$ 軸為夾角(弧度)、$y$ 軸為內積): 106 | 107 | 108 | 內積常被用來判斷兩個向量是否垂直,也可以用來判斷兩個向量的方向是否在同一邊、是否相同或者相反。 109 | 110 | ### 外積(叉積) 111 | 112 | 向量特有的運算,在定義上,它只能用在三維,兩個三維向量 $\overrightarrow{u}=(x_1,y_1,z_1)$ 和 $\overrightarrow{v}=(x_2,y_2,z_2)$ 的外積記作 $\overrightarrow{u} \times \overrightarrow{v}$,是一個新的二維向量 ($x_3,y_3,z_3$): 113 | $$\overrightarrow{u} \times \overrightarrow{v}=\left|\begin{array}{ccc} 114 | \bf{i} & \bf{j} & \bf{k} \\ 115 | x_1 & y_1 & z_1 \\ 116 | x_2 & y_2 & z_2 117 | \end{array}\right| = (y_1z_2-z_1y_2)\textbf{i}-(x_1z_2-z_1x_2)\textbf{j}+(x_1y_2-y_1x_2)\textbf{k}$$ 118 | 119 | 其中,$\bf{i}$、$\bf{j}$、$\bf{k}$ 是三個維度的「基向量」,也就是 $(1,0,0)$、$(0,1,0)$、$(0,0,1)$。外積得到的向量會垂直於 $\overrightarrow{u}$ 和 $\overrightarrow{v}$ 構成的平面,且當 $\overrightarrow{u}$ 和 $\overrightarrow{v}$ 的夾角是 $\theta$,外積得到的向量長度是: 120 | $$||\overrightarrow{u}||\overrightarrow{v}|\sin\theta|$$ 121 | 122 | 它的長度等同於兩個向量所夾的平行四邊形面積。 123 | 124 | 至於它的方向,會依據右手定則,右手食指指向 $\overrightarrow{u}$,中指指向 $\overrightarrow{v}$,大姆指的方向就會是 $\overrightarrow{u} \times \overrightarrow{v}$ 的方向。 125 | 126 | 以上不重要 (?)。二維向量的外積比較常用,但剛剛不是說只能用在三維?你會發現把第三維全部代 $0$,就會變成: 127 | $$\overrightarrow{u} \times \overrightarrow{v}=(x_1y_2-y_1x_2)\textbf{k}$$ 128 | 129 | $\textbf{k}$ 的部分我們不要管它,把二維外積定義為三維外積的長度(純量),如果三維外積的結果向量向上,那二維外積是正的,否則就是負的,可以得出: 130 | $$\overrightarrow{u} \times \overrightarrow{v}=\left|\begin{array}{cc} 131 | x_1 & y_1 \\ 132 | x_2 & y_2 \\ 133 | \end{array}\right|=x_1y_2-y_1x_2=|\overrightarrow{u}||\overrightarrow{v}|\sin\theta$$ 134 | 135 | 相同地,這個值等同於兩個向量所夾的平行四邊形面積,但它是「有向」的,如果 $\overrightarrow{u}$ 轉向 $\overrightarrow{v}$ 是逆時針,那會是正的,反之就會是負的(這邊的轉指往較近那邊轉),至於夾角 $\theta$ 是指 $\overrightarrow{u}$ 往逆時針轉到 $\overrightarrow{v}$ 的角度。 136 | 137 | 138 | 139 | 外積沒有交換律,也就是說 $\overrightarrow{u}\times\overrightarrow{v}\neq\overrightarrow{v}\times\overrightarrow{u}$,但它滿足「負交換律」,也就是 $\overrightarrow{u}\times\overrightarrow{v}=-(\overrightarrow{v}\times\overrightarrow{u})$,因為: 140 | $$(x_1,y_1)\times(x_2,y_2)=x_1y_2-x_2y_1\\ 141 | =-(x_2y_1-x_1y_2)=-(x_2,y_2)\times(x_1,y_1)$$ 142 | 143 | 外積沒有結合律(甚至二維的外積算出來也不是向量),但對加法有分配律。 144 | 145 | 接下來我們針對各種夾角 $\theta$ 討論它的值: 146 | 147 | - $\theta = 90^\circ=\frac{1}{2}\pi$ 148 | 這樣兩個向量會夾一個長方形,且 $\sin 90^\circ=\sin \frac{1}{2}\pi=1$,因此兩個夾角為直角的向量 $\overrightarrow{u}$、$\overrightarrow{v}$ 外積為 $|\overrightarrow{u}||\overrightarrow{v}|$。 149 | 150 | - $\theta = 270^\circ=\frac{3}{2}\pi$ 151 | 這樣兩個向量會夾一個長方形,且 $\sin 270^\circ=\sin \frac{3}{2}\pi=-1$,但 $\overrightarrow{u}$ 轉向 $\overrightarrow{v}$ 是順時針,因此$\overrightarrow{u} \times \overrightarrow{v} = -|\overrightarrow{u}||\overrightarrow{v}|$。 152 | 153 | - $\theta = 0^\circ = 0$ 154 | 兩個向量同向,那麼它們夾的平行四邊形面積為 $0$,且 $\sin 0^\circ=\sin 0=0$,因此此時外積值為 $0$。 155 | 156 | - $\theta = 180^\circ = \pi$ 157 | 兩個向量反向,此時它們夾的平行四邊形面積也為 $0$,且 $\sin 180^\circ=\sin \pi = 0$,因此此時外積值為 $0$。 158 | 159 | - $0^\circ=0 <\theta < 180^\circ = \pi$ 160 | $\overrightarrow{u}$ 往 $\overrightarrow{v}$ 轉是逆時針,且 $\sin \theta \in (0, 1]$,則 $\overrightarrow{u} \times \overrightarrow{v} > 0$。除非 $\theta=90^\circ=\frac{1}{2}\pi$,不然 $\sin \theta < 1$,則 $0 < \overrightarrow{u} \times \overrightarrow{v} < |\overrightarrow{u}||\overrightarrow{v}|$。 161 | 162 | - $180^\circ=\pi <\theta < 360^\circ = 2\pi$ 163 | $\overrightarrow{u}$ 往 $\overrightarrow{v}$ 轉是順時針,且 $\sin \theta \in [-1, 0)$,則 $\overrightarrow{u} \times \overrightarrow{v} < 0$。除非 $\theta=270^\circ=\frac{1}{2}\pi$,不然 $\sin \theta > -1$,則 $-|\overrightarrow{u}||\overrightarrow{v}| < \overrightarrow{u} \times \overrightarrow{v} < 0$。 164 | 165 | 兩向量在不同夾角時的外積: 166 | 167 | 168 | 兩個長度為 $1$ 的向量在不同夾角時的外積($x$ 軸為夾角(弧度)、$y$ 軸為外積): 169 | 170 | 171 | 172 | ## 實作 173 | 174 | 有些人會把向量(或點)做成一個 class,不過我自己是習慣直接用 `pair`,然後再做運算子重載。 175 | 176 | ```cpp= 177 | #define F first 178 | #define S second 179 | #define mp make_pair 180 | 181 | template 182 | pair operator+(pair a, pair b){ 183 | return mp(a.F + b.F, a.S + b.S); 184 | } 185 | 186 | template 187 | pair operator-(pair a, pair b){ 188 | return mp(a.F - b.F, a.S - b.S); 189 | } 190 | 191 | template 192 | pair operator*(pair a, T b){ 193 | return mp(a.F * b, a.S * b); 194 | } 195 | 196 | template 197 | pair operator/(pair a, T b){ 198 | return mp(a.F / b, a.S / b); 199 | } 200 | 201 | template 202 | T dot(pair a, pair b){ 203 | return a.F * b.F + a.S * b.S; 204 | } 205 | 206 | template 207 | T cross(pair a, pair b){ 208 | return a.F * b.S - a.S * b.F; 209 | } 210 | 211 | template 212 | T abs2(pair a){ 213 | return a.F * a.F + a.S * a.S; 214 | } 215 | ``` 216 | 217 | -------------------------------------------------------------------------------- /_posts/note/math/gcd.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: 歐幾里得演算法 3 | --- 4 | # 歐幾里得演算法 5 | 6 | 歐幾里德演算法也叫輾轉相除法,用來求 $\gcd(a,b)$,它運用了一個性質: 7 | 8 | > 對於 $a \geq b$,$\gcd(a,b)=\gcd(a-b,b)$($\gcd(a,0)=a$)。 9 | > 10 | > Proof. 11 | > $d \mid a \land d \mid b \iff d \mid (a-b) \land d \mid b$,所以 $a$、$b$ 的公因數和 $a$、$a-b$ 是一樣的。 12 | 13 | 從這個性質可以推出 $\gcd(a,b)=\gcd(a-b,b)=\gcd(a-2,b)=\dots=\gcd(b, a \bmod b)$,遞迴直到 $\gcd(a,0)=a$。 14 | 15 | 因為 $a \bmod b \leq \frac{a}{2}$,所以每過兩次 $a$ 會變小至少一半($\gcd(a,b)=\gcd(b, a \bmod b) = \gcd(a \bmod b, b \bmod (a \bmod b))$),得出這個演算法的時間複雜度是 $O(\log \max(a,b))$。 16 | 17 | ```cpp 18 | int gcd(int a, int b){ 19 | while(b > 0){ 20 | int t = a % b; 21 | a = b; 22 | b = t; 23 | } 24 | return a; 25 | } 26 | ``` 27 | 28 | 實作上可以用迴圈,常數比較小,不過在 C++ 裡不用自己寫,可以用 `__gcd(a,b)`。 29 | 30 | ## 擴展歐幾里德演算法 31 | 32 | 擴展歐幾里德演算法用來求 $ax+by=c$ 的整數解。 33 | 34 | > 貝祖定理: 35 | > 36 | > 對於整數 $a,b,c$,$ax+by=c$ 有解若且唯若 $\gcd(a,b) \mid c$。 37 | 38 | 由此可知只要 $c$ 不是 $\gcd(a,b)$ 的倍數就無解。有解的話,令 $g=gcd(a,b)$,可以先找 $ax+by=g$ 的解,擴展歐幾里德演算法的作法是嘗試從輾轉相除法的遞迴式,做出一個長得差不多的式子: 39 | 40 | $$\begin{align*} 41 | ax+by & =\gcd(a,b) \\ 42 | & = \gcd(b, a \bmod b) \\ 43 | & = \gcd(b, a - \lfloor \frac{a}{b} \rfloor b) \\ 44 | & = bx' + (a - \lfloor \frac{a}{b} \rfloor b)y' \\ 45 | & = ay' + b(x' - \lfloor \frac{a}{b} \rfloor y') 46 | \end{align*}$$ 47 | 48 | 得出 $x=y'$、$y=x' - \lfloor \frac{a}{b} \rfloor y'$,所以先求 $x',y'$ 就可以算出 $x,y$,終止條件一樣是當 $b=0$ 時,回傳 $(1,0)$ 即可。 49 | 50 | ```cpp 51 | pii exgcd(int a, int b){ 52 | if(b == 0) return mp(1, 0); 53 | pii ans = exgcd(b, a % b); 54 | return mp(ans.S, ans.F - a / b * ans.S); 55 | } 56 | ``` 57 | 58 | 最後得到的答案只要乘上 $\frac{c}{g}$ 倍,就可以變成 $ax+by=c$ 的答案。 59 | 60 | ### 其他的解 61 | 62 | 擴展歐幾里德演算法只會拿到其中一組解。先來看看兩組解會有什麼關係:有兩組整數解 $(x_1,y_1)$、$(x_2,y_2)$,我們知道 63 | 64 | $$ax_1+by_1=ax_2+by_2 \implies \frac{a}{g}(x_1-x_2) = \frac{b}{g}(y_2-y_1)$$ 65 | 66 | 因為 $\frac{a}{g}$ 和 $\frac{b}{g}$ 互質,因此 $x_1-x_2 = k\frac{b}{g}$、$y_2-y_1 = k\frac{a}{g}$,也就是說有了一組解 $(x,y)$ 後,其他解都可以寫成 $(x+k\frac{b}{g}, y-k\frac{a}{g})$,$k \in \mathbb{Z}$。 67 | 68 | ### 負數的狀況 69 | 70 | 這裡有一些小小細節,因為係數常常有可能是負的,所以特別討論一下負數的狀況。在上面的式子裡面,寫的都是 $a \bmod b = a - \lfloor\frac{a}{b}\rfloor b$,但是程式碼裡的 `a % b` 實際上是 `a - a / b * b`,而 `a / b` 是向零取整而非向下取整。在不同的除法(向下、向下、向零)定義下,`gcd(a,b)` 給出的結果正負會不一樣。 71 | 72 | 要避免這個問題發生,就要確保 `gcd` 和 `exgcd` 的遞迴過程完全一樣。如果好好照上面那樣寫,並且算 $\gcd$ 用的是 `__gcd` 就不會出任何問題,但是如果你在 `exgcd` 不小心把 `a / b` 變成向下取整,還繼續用 `__gcd` 就會出事了。 73 | 74 | 如果不喜歡有負數的話,可以把負號移到變數上,例如 $-3x+y=1$ 可以變成 $3(-x)+y=1$。 75 | 76 | ### 解的範圍 77 | 78 | 另外還有溢位的問題,事實上可以不用擔心運算過程發生溢位。因為貝祖定理其實還告訴我們: 79 | 80 | > 一定存在恰兩組整數解 $(x,y)$ 滿足 $\lvert x \rvert \leq \lvert \frac{b}{g} \rvert$、$\lvert y \rvert \leq \lvert \frac{a}{g} \rvert$,而且擴展歐幾里德演算法得出的是其中一組。 81 | 82 | 所以不只可以知道運算過程數字都會保持在一個範圍內,而且還可以知道如果你想要拿到最小正數解之類的,只要把得到的解移動幾次就可以了。 83 | -------------------------------------------------------------------------------- /_posts/note/math/pow.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: 快速冪 3 | --- 4 | # 快速冪 5 | 6 | 要算 $a^n$,最暴力的方法就是乘 $n$ 次,不過這樣的時間複雜度是 $O(n)$,太慢了。 7 | 8 | 利用 $a^b \times a^c = a^{b+c}$ 的性質,我們可以把指數拆開,例如: 9 | $$ 10 | \begin{align*} 11 | 13 &= 2^0 + 2^2 + 2^3 \\ 12 | a^{13} &= a^{2^0} \times a^{2^2} \times a^{2^3} 13 | \end{align*} 14 | $$ 15 | 16 | $a=a^{2^0}$ 乘上自己會得到 $a^{2^1}$、再乘自己會得到 $a^{2^2}$,每次指數都會變兩倍,只要做 $i$ 次就可以拿到 $a^{2^i}$ 了。因此,可以用這樣的方式在 $O(\log n)$ 的時間算出答案: 17 | 18 | ```cpp 19 | const ll MOD = 1000000007; 20 | ll fp(ll a, ll b){ 21 | int ans = 1; 22 | while(b > 0){ 23 | if(b & 1) ans = ans * a % MOD; 24 | a = a * a % MOD; 25 | b >>= 1; 26 | } 27 | return ans; 28 | } 29 | ``` -------------------------------------------------------------------------------- /_posts/note/math/prime-and-factor.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: 質數與因數 3 | --- 4 | # 質數與因數 5 | 6 | ## 質數判斷 7 | 8 | 要判斷一個數 $x$ 是否是質數,最暴力的方式是檢查它有沒有除了 1 和 $x$ 以外的因數,時間複雜度是 $O(x)$。 9 | 10 | 因為一個合數 $x$ 一定會有一個 $\leq \sqrt{x}$ 的因數,所以其實只要檢查到 $\sqrt{x}$ 就好,時間複雜度會是 $O(\sqrt{x})$。 11 | 12 | ## 質數篩 13 | 14 | 找 $n$ 以內的質數,暴力找的話時間會是 $O(n \sqrt{n})$,要更快的話就要用到質數篩。 15 | 16 | 如果發現了一個質數 $p$,就會知道 $p$ 的倍數都不是質數,這個時候就可以把它們刪掉。記錄每一個數被刪掉了沒,從 2 開始看,如果沒被刪掉就表示它是質數,然後去刪掉它的所有倍數,這麼做的時間複雜度是 17 | 18 | $$O(\frac{n}{2} + \frac{n}{3} + \frac{n}{5} + \dots) = O(n \log \log n)$$ 19 | 20 | (見[素數的倒數之和](https://zh.wikipedia.org/zh-tw/%E7%B4%A0%E6%95%B0%E7%9A%84%E5%80%92%E6%95%B0%E4%B9%8B%E5%92%8C)) 21 | 22 | 注意到如果目前數字是 $i$,對於 $j isprime(n + 1, true); 26 | vector prime; 27 | for(int i = 2; i <= n; i++){ 28 | if(!isprime[i]) continue; 29 | prime.eb(i); 30 | for(int j = i; (ll) i * j <= n; j++){ 31 | isprime[i * j] = false; 32 | } 33 | } 34 | 35 | ``` 36 | 37 | 每一個數被刪到的次數會是「平方不超過它本身的質因數」數量,那有沒有辦法讓每個數都只刪到一次呢? 38 | 39 | 做法是對於**每一個**數,刪掉它的「不超過它的最小質因數的質數」倍數。假設 $x$ 的最小質因數是 $p$,$t$ 是一個 $\leq p$ 的質數,那麼 $tx$ 的最小質因數肯定是 $t$,也就是說,一個合數只會被「它除以它的最小質因數」刪掉,得出每個合數只會被刪到一次,複雜度就變成 $O(n)$,因為複雜度是線性,稱為線性篩。 40 | 41 | ```cpp 42 | vector lpf(n, 1); //每個數的最小質因數 43 | vector prime; //找到的質數 44 | 45 | for(int i = 2; i <= n; i++){ 46 | if(lpf[i] == 1){ 47 | lpf[i] = i; 48 | prime.eb(i); 49 | } 50 | for(int j : prime){ 51 | if(i * j > n) break; 52 | lpf[i * j] = j; 53 | if(j == lpf[i]) break; 54 | } 55 | } 56 | ``` 57 | 58 | ## 質因數分解 59 | 60 | 暴力找一個數的質因數,複雜度會是 $O(n)$。事實上一個數 $n$ 頂多只會有一個 $\geq \sqrt{n}$ 的質因數,所以可以只枚舉 $2$ 到 $\sqrt{n}$,檢查它們是不是 $n$ 的因數,從 $n$ 除掉之後,剩下如果 $>1$,那剩下來的數字也是它的質因數,這樣複雜度就是 $O(\sqrt n)$。 61 | 62 | 如果要找很多個數的質因數,那也可以利用剛剛線性篩得到的最小質因數。$n$ 質因數分解後的數字數量(重複也算)頂多只會有 $\lfloor \log_2 n \rfloor$ 個,因為所有的質數都 $\geq 2$,因此只要不斷把 $n$ 的最小質因數除掉,就可以在 $O(\log n)$ 做好一個數的質因數分解。 63 | 64 | ```cpp 65 | vector pf; //質因數分解的結果,每一項記為 (底數, 指數) 66 | 67 | while(x > 1){ 68 | int d = lpf[x]; 69 | pf.eb(mp(d, 0)); 70 | while(x % d == 0){ 71 | pf.back().second++; 72 | x /= d; 73 | } 74 | } 75 | ``` -------------------------------------------------------------------------------- /_posts/note/misc/complexity.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: 複雜度 3 | --- 4 | # 複雜度 5 | 6 | ## 什麼是複雜度 7 | 8 | 複雜度是用來形容某個函數和它的參數的關係的東西,最常用的表示方法是 Big-O notation,正式的定義是: 9 | 10 | > 對於兩個函數 $f(n)$ 和 $g(n)$,如果存在正數 $c$ 和 $n_0$,對於所有的 $n \geq n_0$ 都滿足 $f(n) \leq cg(n)$,那麼 $f(n) \in O(g(n))$。 11 | 12 | 用圖形來看,就是把 $g(n)$ 乘上某個數字 $c$,使得 $cg(n)$ 的圖形在某個點之後就會一直在 $f(n)$ 上方。 13 | 14 | 複雜度表示函數的值如何隨著它的參數成長,換句話說,一個函數的複雜度是它的「成長率」,當一個函數的複雜度越大,就表示它成長得越快。一種簡單的算法是,把 $f(n)$ 中的所有低次項和係數去掉,就是它的複雜度了,因為當 $n$ 變得很大,非最高次的項和係數對函數值的影響就不大了。例如 $f(n)=2n^2+n-1$ 的複雜度就是 $O(n^2)$,因為當 $n$ 逐漸變大,$n$ 和 $-1$ 就對 $f(n)$ 的值影響不大了,複雜度給我們一種方式來表示「這個函數的結果大概是多少」。 15 | 16 | 而時間複雜度和空間複雜度就是表示程式消耗的時間和空間如何成長,也是一種用來比較演算法好壞的方式,像是同樣的問題,有作法 A 和作法 B,它們的時間複雜度分別是 $O(n^2)$ 和 $O(n)$,我們就可以透過它們的時間複雜度,知道當 $n$ 到達一定的大小時,B 就會比 A 快得多,所以 B 是比 A 好的作法。 17 | 18 | 既然要比大小,就要知道怎麼比。如果 $f(n) \in O(g(n))$ 的話就知道 $O(f(n)) \leq O(g(n))$(也就是 $O(f(n)) \subseteq O(g(n))$),如果 $O(f(n)) \leq O(g(n))$ 而且 $O(g(n)) \leq O(f(n))$ 的話,就代表 $O(f(n))=O(g(n))$。 19 | 20 | 順帶一提,根據定義,$f(n) \in O(f(n))$ 是成立的,例如 $2n^2+n-1 \in O(2n^2+n-1)$ 是成立的,而且 $2n^2+n-1 \in O(n^3)$ 也是成立的,所以複雜度的表示不是唯一的,但我們會選擇最小的那一個,然後用最簡單的方法把它表示出來,這樣做才是有意義的。沿用剛剛的例子,因為 $2n^2+n-1 \in O(2n^2+n-1) = O(n^2) \in O(n^3)$,所以我們會說 $2n^2+n-1$ 的複雜度是 $O(n^2)$ 而不是另外兩個。 21 | 22 | 常見用語: 23 | 24 | - 線性複雜度:$O(n)$ 稱為線性複雜度,$f(n) \in O(n)$ 可以說成 $f(n)$ 對 $n$ 呈線性($f(n)$ is linear to $n$)。 25 | - 常數複雜度:$O(1)$。 26 | 27 | ## 時間複雜度 28 | 29 | 時間複雜度就是用複雜度來描述程式執行的時間如何隨輸入的數字成長,例如: 30 | 31 | ```cpp 32 | int n; 33 | cin >> n; 34 | vector v(n); 35 | for(int i = 0; i < n; i++) cin >> v[i]; 36 | for(int i = 0; i < n - 1; i++){ 37 | for(int j = 0; j < n - i - 1; j++){ 38 | if(v[j] > v[j + 1]) swap(v[j], v[j + 1]); 39 | } 40 | } 41 | for(int i = 0; i < n; i++) cout << v[i] << " "; 42 | cout << "\n"; 43 | ``` 44 | 45 | 這是用 Bubble sort 排序一個陣列,輸入或輸出一個數字是時間複雜度是 $O(1)$,所以輸入的部分需要花 $O(n)$ 的時間,而第 5 到 9 行的巢狀迴圈,最裡面的第 7 行總執行次數是 $1+2+\dots+(n-1) \in O(n^2)$,最後輸出也要花 $O(n)$ 的時間。這個 $O(n^2)$ 是所有部分當中花費時間最久的,也稱為**瓶頸**,所以這個程式的總時間複雜度是 $O(n^2)$。 46 | 47 | 像這種迴圈執行次數很好算的,時間複雜度的算法很簡單,就是巢狀的迴圈執行次數相乘、非巢狀的取最大,不過也會有次數不好算的,像是: 48 | 49 | ```cpp 50 | int n; 51 | cin >> n; 52 | 53 | stack st; 54 | for(int i = 0; i < n; i++){ 55 | int t; 56 | cin >> t; 57 | while(!st.empty() && st.top() <= t) st.pop(); 58 | if(st.empty()) cout << "-1 "; 59 | else cout << st.top() << " ", st.pop(); 60 | st.push(t); 61 | } 62 | cout << "\n"; 63 | ``` 64 | 65 | 這個是輸入一個陣列,然後對每一個元素輸出它左邊第一個比它大的數字(用單調隊列),沒有就輸出 `-1`。第 8 行的 `while` 執行次數是不固定的,所以不能用巢狀迴圈次數相乘的方法。注意到每執行一次都會從 stack pop 掉一個東西,所以我們可以知道,執行次數也就是 pop 次數,一定不超過 push 次數,而 push 次數顯然是 $n$,得出這個 `while` 的總執行次數是 $O(n)$,總時間複雜度是 $O(n)$。 66 | 67 | ### 真正的執行時間 68 | 69 | 程式執行的時間會受到很多其他因素影響,所以是不能從時間複雜度知道實際的執行時間確切是多少的,但可以估算。估算的方法是,把數字都代進複雜度後,得到的數字大約 $10^8 \sim 10^9$ 是一秒。 70 | 71 | ### 常數 72 | 73 | 前面提到複雜度的估算方法是把低次項和常數全部拿掉,而且不管把複雜度乘上任何常數,都還會是一樣的東西,因為它對函數值影響不大,但是,這個常數真的不重要嗎? 74 | 75 | 舉個例子: 76 | 77 | ```cpp= 78 | for(int i = 0; i < n; i++){ 79 | for(int j = 0; j < n; j++){ 80 | for(int k = 0; k < 10; k++) /*...*/; 81 | } 82 | } 83 | ``` 84 | 85 | 這裡有三層的巢狀迴圈,總時間複雜度是 $O(n) \times O(n) \times O(10)=O(10n^2)=O(n^2)$,我們在計算複雜度時把 $10$ 丟掉了,但那個 $10$ 不重要嗎?這要依 $n$ 的大小而定,這和我們在想複雜度時的概念相反,算複雜度丟掉常數是因為數字大的時候它不重要,但實際上是數字越大時它越重要:如果 $n \leq 10000$,那 $n^2=10^8$,已經接近極限了,再乘上一個 $10$,就會變 $10^9$,如果時限是 1 秒,這就有點緊了。 86 | 87 | 這個例子是屬於「看得到」的常數,還有的常數是「看不到」的,像是雖然加、減、乘、除、模的複雜度都是 $O(1)$,但除和模的所需時間其實是比其他的略久的,模 $10^8$ 次沒有在 1 秒內跑完也是有可能發生,還有像 unordered\_map、unordered\_set 這種 hash table 雖然查詢、修改的複雜度都是 $O(1)$,但常數是很大的,有時候也會發生用 $O(\log n)$ 的 map 或 set 比 unordered 版快的狀況。 88 | 89 | 總而言之,常數是很捉摸不定的,有時候一個看起來久到爆的東西,也可能會因為編譯器優化而變得超快,這就只能慢慢累積經驗了。 90 | 91 | ## 空間複雜度 92 | 93 | 空間的狀況比時間單純許多,因為空間用了多少通常很好算,也可以用 `sizeof()` 來看用了多少空間。只是需要記得一些容器的空間常數比較大,例如空 deque 的大小比空 vector 大。 -------------------------------------------------------------------------------- /_posts/note/misc/io-optimize.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: I/O 優化 3 | --- 4 | # I/O 優化 5 | 6 | 有的時候會在題目中看到這麼一句:「建議使用 `scanf`/`printf`,使用 `cin` 和 `cout` 可能造成 TLE。」在輸入、輸出量比較大的題目,應該都可以明顯感受到 `stdio` 和 `iostream` 兩個的差異,`scanf` 和 `printf` 的效率顯然高得多,但是事情並不是這樣。 7 | 8 | `cin` 和 `cout` 的好處很明顯,像是它們保證類型安全,不會像 `printf` 和 `scanf` 有打錯類型的風險,且 `<<` 和 `>>` 這兩個運算子可以重載,換句話說,你可以在定義一個類型的物件 `a` 如何輸入輸出後,方便的使用 `cin >> a` 和 `cout << a`,就可以做到輸入輸出,既方便又直觀。 9 | 10 | 再加上 `cout` 像是補 0、控制位數等等都比 `printf` 好寫得多,所以事實上 `cin` 和 `cout` 是比較方便的,然而,速度似乎慢很多?先來看看為什麼: 11 | 12 | 在文字被輸出前,它會先到緩衝區,不是馬上就全部輸出,而是到一個特定的時間再一次輸出出去,因為分次輸出其實算是很耗資源的事。 13 | 14 | 緩衝區的 flush 是決定效率的關鍵因素,看是一次 flush、還是多次 flush,速度就會大有不同。`printf` 是不會自動 flush 的,`scanf` 也不會強制 flush,但 `cin` 就會了,因為在要求使用者輸入之前,應該先讓使用者看見先前輸出的文字,這是介面設計上的考量,但顯然不符合競程的需要,因此我們要取消強制 flush: 15 | ```cpp 16 | cin.tie(0); 17 | ``` 18 | `tie` 是 `ios` 的一個方法,也就是說,`cin`、`cerr`、`cout` 都有這個方法,`cin` 和 `cerr` 預設是有 tie `cout` 的,所以在使用 `cin`、`cerr` 時,`cout` 的緩衝區會先被釋放,`tie(0)` 就可以解除。所以如果你不想看到 `cerr` 和 `cout` 的訊息全部卡在一起,可以再加上 `cerr.tie(0)`。 19 | 20 | 接下來,還有一個地方會自動 flush,就是 `endl`,也就是說,`cout << endl` 其實是 `cout << flush << '\n'`,解決方法很簡單,就是不要用,都用 `cout << '\n'` 就可以了。 21 | 22 | 最後,還有一個跟緩衝區無關的問題,C++ 中有兩種輸入輸出系統:`stdio` 和 `iostream`,因為一些歷史因素,C++ 會讓 `iostream` 和 `stdio` 同步,來避免混用時發生無法預期的問題,所以這個額外的動作會造成效率降低,不過打競程時不會有混用的狀況,把同步解除就好: 23 | ```cpp 24 | ios_base::sync_with_stdio(false); 25 | ``` 26 | (記得這個用了之後,`iostream` 和 `stdio` 不能混用。) -------------------------------------------------------------------------------- /_posts/note/misc/iostream.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: I/O Stream 3 | --- 4 | # I/O Stream 5 | 6 | ## 簡介 7 | 8 | 你可能有聽過「輸入流」和「輸出流」,但是流是什麼呢? 9 | 10 | 流(stream)就像河流一樣,先從上游流進去的東西會先從下游流出來,而輸入流和輸出流顧名思議,就是用來控制輸入和輸出的東西。簡單來講,你輸入的東西會進入輸入流,然後再依序流往它流向的地方,像是被程式讀取,輸出流也是,程式輸出的東西會先到輸出流,再到它流向的地方,可能是你的 terminal、或是某些檔案,分成多個輸入、輸出流的目的就是為了控管不同的來源和去向。流會有「緩衝區」,就是流裡的東西會暫時停在那裡,不會馬上跑到流向的地方。 11 | 12 | 非常簡略地講解一下 C++ 裡輸入輸出的物件關係(詳細可以看[這裡](http://www.cplusplus.com/reference/ios/))。有兩個類型分別叫 ostream 和 istream,分別是輸出流和輸入流,繼承 ostream 的都是輸出流、繼承 istream 的都是輸入流,然後有另一個類型叫 iostream,它同時繼承 ostream 和 istream,所以它是雙向的,一個例子是 stringstream。 13 | 14 | 所有 istream 的用法都和 cin 一樣、所有 ostream 的用法都和 cout 一樣。 15 | 16 | ### 標準流 17 | 18 | 標準流是指三種流:標準輸入流、標準輸出流和標準錯誤流,標準輸入和輸出流就如同其名,分別是輸入流和輸出流,至於標準錯誤流,它是一個輸出流,它們預設都是在 terminal 上做事的,從 terminal 輸入和輸出到 terminal,在 C++ 中,它們三個分別由 cin(繼承 istream)、cout(繼承 ostream)和 cerr(繼承 ostream)控制,也就是大家最常用的輸入輸出流啦。 19 | 20 | 標準輸出流和標準錯誤流都是輸出流,那它們有什麼分別呢?首先,judge 是只會讀標準輸出流的資訊的,所以你可以輸出一堆 debug 用的訊息到標準錯誤流,忘記刪掉也不會怎麼樣,只要不要造成 TLE 就好了(像是輸出量過大)。而且它們可以被重新導向到不同的地方。 21 | 22 | 這裡提到了一個東西,叫做「重新導向(redirect)」,標準流不一定要在 terminal 做事,也可以把它導到別的地方去,如果直接在 C++ 寫,用法是這樣的: 23 | 24 | ```cpp 25 | freopen("filename", "w", stdout); 26 | freopen("filename", "w", stderr); 27 | freopen("filename", "r", stdin); 28 | ``` 29 | 30 | 這三個分別是把標準輸出、錯誤和輸入流導向到名為 filename 的檔案,在本機測試的時候,輸入或輸出量大時,把標準流導到檔案去是很實用的方法,也很方便,只要加幾行重新導向的程式碼就可以了,而上傳到 judge 前也只要移除這幾行就好。有些比賽例如 USACO,會要求你從特定的檔案輸入輸出。 31 | 32 | 也可以在 terminal 上做重新導向,在 Linux 的 bash 和 Windows 的 CMD 都是像這樣寫: 33 | ```bash 34 | command < in 35 | command > out 36 | command 2> err 37 | command < in > out 2> err 38 | command < in > out 39 | ``` 40 | 41 | command 是執行檔名稱,in 是輸入流導向的檔案,out 是輸出流導向的檔案,err 是錯誤流導向的檔案,可以像前三行一樣只重新導向其中一個、也可以像第四行一樣重新導向全部三個,或是像第五行只重新導向其中兩個。 42 | 43 | 對檔案輸入輸出也可以用 fstream,但比賽理論上用不到,所以有興趣可以自己查。 44 | 45 | cin 和 cout 都有緩衝區,所以 cin 從來源讀取的東西會留在緩衝區裡等你用程式讀取,而你讓 cout 輸出的東西也會先留在緩衝區,不會馬上出現在它導向到的地方,讓輸出流真的輸出緩衝區裡的東西的動作叫 flush,手動 flush 的方法是 `cout << flush`,`cout` 也可以換成其他有緩衝區的輸出流,自動 flush 的條件後面會提到。而 cerr 不緩衝,所以讓 cerr 輸出的東西就會馬上輸出。 46 | 47 | ## 使用 48 | 49 | ### 運算子重載 50 | 51 | 重載 `<<` 和 `>>` 就能讓原本不能直接輸入輸出的東西可以直接 cin/cout 了,例如: 52 | 53 | ```cpp= 54 | ostream& operator<<(ostream& o, pair p){ 55 | return o << p.first << " " << p.second; 56 | } 57 | 58 | istream& operator>>(istream& i, pair p){ 59 | return i >> p.first >> p.second; 60 | } 61 | ``` 62 | 63 | ### 輸入格式 64 | 65 | #### EOF 66 | 67 | EOF 是 End of File 的縮寫。有些題目會在一個檔案放多筆測資,比較現代的題目會先輸入一個數字告訴你接下來有幾筆,但比較古老的題目~~或心理比較古老的人~~出的題目就會跟你說「以 EOF 結束」,所以你必須一直讀到檔案結束為止,做法很簡單: 68 | 69 | ```cpp= 70 | while(cin >> /*...*/){ 71 | //... 72 | } 73 | ``` 74 | 75 | `cin >> ...` 除了把東西輸入到變數裡,也會回傳 cin 本身(這就是為什麼可以像 `cin >> a >> b >> c >> ...` 串一大串),而 cin 可以轉型成 bool,如果遇到 EOF(或其他原因導致輸入失敗)了它就是 false,否則它就是 true。 76 | 77 | EOF 這東西不是一個字元,所以你可能會想,terminal 不是 file,不就沒有 EOF 了嗎?在 Windows 可以用 ctrl+z 製造出 EOF,其他系統可以用 ctrl+d。 78 | 79 | #### 輸入一整行 80 | 81 | 用像是 `cin >> ...` 的方式輸入的話,會先忽略緩衝區前面的所有空白字元(空格、換行等等),然後讀取非空白字元,直到遇到空白字元再停下來。 82 | 83 | 但是,當遇到這種情況,就會需要一次輸入一整行:你需要知道哪些東西在同一行,而且又不確定一行有幾個,方法是這樣: 84 | 85 | ```cpp= 86 | string s; 87 | getline(in, s); //in 是輸入流 88 | ``` 89 | 90 | getline 也可以放進 while 裡判 EOF。 91 | 92 | getline 跟 `>>` 混用的話會發生一個問題,假設有兩行輸入,第一行有一個數字,下一行有未知數量的以空格隔開的數字,首先它們會全部進入緩衝區,如果你先用 `>>` 輸入了一個數字,那麼在緩衝區內的這個數字會被移除,但它後面的換行不會,所以你這個時候用 getline,就得到一個空字串,解決方法也很簡單,就是 getline 一次把空白行清掉後,再 getline 一次。 93 | 94 | #### 忽略輸入 95 | 96 | 直接看題目:[ZeroJudge c268](https://zerojudge.tw/ShowProblem?problemid=c268),這題表面上是要你找三個數字當三角形的邊長,可以 $O(n \log n)$ 做完,可是第一 $n$ 超大,把數字排序好後就 TLE 了,就算不排序,雖然輸入的複雜度是 $O(n)$,但輸入常數很大,輸入這麼多東西也會 TLE 的。 97 | 98 | 可以發現到若輸入的數字都不能構成三角形,那麼在數字都盡量小的情況下,把它們排序後會變成費氏數列,而輸字的最大值又是 $10^9$,費氏數列還沒到第 50 項就會超過了,所以可以得出 $n \geq 50$ 的時候保證有解。 99 | 100 | 現在排序的問題解決了,那輸入的問題呢?它是一個檔案多筆測資,所以也不能讀到 $n \geq 50$ 就直接 return,解決方法是: 101 | 102 | ```cpp 103 | cin.ignore(100000000, '\n'); 104 | ``` 105 | 106 | 這麼做可以忽略掉到一部分的輸入,第一個參數是忽略的字元上限,打一個很大的數字就行了,第二個是停下來的字元,到下一個這個字元之前和它本身的字元都會被忽略掉。 107 | 108 | 這也是 getline 解決空行的解法之一。 109 | 110 | ### 輸出格式 111 | 112 | #### 小數位數 113 | 114 | 需要輸出浮點數的題目,要嘛就是要求跟答案的差距要在某個範圍內,不然就是要求你四捨五入到第幾位,所以我們要能夠控制輸出的小數點後位數。方法是這樣: 115 | 116 | ```cpp 117 | cout << fixed << setprecision(10) << ...; 118 | ``` 119 | 120 | 沒加 fixed 的話,會變成是指定總共的位數,如果小數點前的位數超過就會變科學記號,而加上 fixed 就是指定小數點後的位數了,然後它也會自己四捨五入,非常方便。還有它只作用在浮點數,像是 int 不會跑出 .0000,字元也還是以字元的形式輸出。 121 | 122 | #### 數字寬度 123 | 124 | 數字寬度和數字位數不一樣,輸字寬度是指輸出的總字元數不到寬度的時候,就會在它之前補上數個某固定字元。指定寬度是 `setw(n)`,補上的字元是 `setfill(c)`,預設空格,用法就一樣是 `cout << setw(n) << setfill(c) << ...`。要特別注意的是,`setw` 只作用在下一個輸出的數字(可以是浮點數或整數)上,而 setfill 不會失效。 125 | 126 | -------------------------------------------------------------------------------- /_posts/note/misc/prefix-sum.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: 前綴和與差分 3 | --- 4 | # 前綴和與差分 5 | 6 | 序列 $A_1,A_2,\dots,A_n$ 的長度為 $i$ 的前綴和記作 $S_i$,而 $S_0=0$,可以用來計算區間和: 7 | 8 | $$ \sum_{i=l}^r A_i = S_r - S_{l-1} $$ 9 | 10 | 而差分 $D_i$ 是 $A_i-A_{i-1}$,特別地令 $A_0=D_0=0$。差分的前綴和就是原序列,區間和就是原序列某兩項的差。 11 | 12 | > 給一個序列 $A_1,A_2,\dots,A_n$,有 $q$ 筆操作,第 $i$ 筆操作為將序列 $A$ 的區間 $[l_i, r_i]$ 加上 $v_i$。求做完所有操作後,序列 $A$ 的樣子。 13 | 14 | 注意到區間 $[l,r]$ 加 $v$ 的時候,$D_l$ 會多出 $v$、$D_{r+1}$ 會少 $v$,其他都不會變。既然不需要中途詢問,那就可以在操作的過程中維護 $D$,最後再算前綴和就能得到 $A$。 15 | 16 | 前綴和跟差分如果要帶修改,可以搭配 [BIT](/fenwick-tree)。 -------------------------------------------------------------------------------- /_posts/note/misc/preprocessor.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: 前置處理器 3 | --- 4 | # 前置處理器 5 | 6 | ## 簡介 7 | 8 | C++ 是一個編譯式語言,也就是程式碼要先經過編譯,變成執行檔才能執行。不過「編譯」的過程其實不光只有編譯這麼簡單,例如這個程式碼: 9 | 10 | ```cpp 11 | #define test 123 12 | #include 13 | ``` 14 | 15 | 這種以 `#` 為開頭的程式碼是**前置處理器**的指令,在編譯之前有一個階段就叫作前置處理,前置處理做的事情就是根據你的指令修改程式碼,然後再拿去編譯。 16 | 17 | 舉例來說,`#include` 會在編譯前將指定檔案裡的文字完全複製貼上到你 include 的那個地方,它可以用在任何地方,例如: 18 | 19 | 你開了一個檔案叫做 `hello.txt`,內容只有 `"hello"`,然後你在同目錄有另一個檔案 `test.cpp`,內容是: 20 | ```cpp 21 | #include 22 | using namespace std; 23 | 24 | int main(){ 25 | cout << 26 | #include "hello.txt" 27 | << "\n"; 28 | return 0; 29 | } 30 | ``` 31 | 那麼會輸出 `hello`,在編譯之前,`#include "hello.txt"` 這一行就會被替換成 `"hello"`。 32 | 33 | 還有一個常見的指令是 `#define`,寫法是 `#define identifier 替換字串`,也被稱為「巨集」或「宏」(macro),有些人可能會說這是「常數」,然後你就會開始懷疑它跟 `const` 的差別,其實 `#define` 並不是在宣告一個常數,而是它會把整份程式碼中的一段特定文字替換,例如: 34 | 35 | ```cpp 36 | #include 37 | #define hello "hello" 38 | using namespace std; 39 | 40 | int main(){ 41 | cout << hello << "\n"; 42 | return 0; 43 | } 44 | ``` 45 | 在編譯之前,`hello` 就會被替換成 `"hello"`,接著才進行編譯,所以輸出會是 `hello`。它也有 function 的用法: 46 | 47 | ```cpp 48 | #inlcude 49 | #define say(a) cout << a << "\n" 50 | using namespace std; 51 | 52 | int main(){ 53 | say("hello"); 54 | return 0; 55 | } 56 | ``` 57 | 這樣會輸出 `hello`,在編譯前,`say("hello")` 會被替換成 `cout << a << "\n"`。 58 | 59 | 要特別注意的一點是,因為這是把文字原封不動貼上,所以 `#define` 替換的部分**不會**先做運算,而是在執行期按照前置處理器處理完的程式碼運算,例如: 60 | ```cpp 61 | #include 62 | #define plus(a, b) a + b 63 | using namespace std; 64 | 65 | int main(){ 66 | cout << (plus(1, 2) * 3) << "\n"; 67 | return 0; 68 | } 69 | ``` 70 | 這樣的結果並不是 `9`,而是 `7`,因為替換完後的程式碼會是 `1 + 2 * 3`,`2 * 3` 會先被計算。所以建議都用個括號把它包起來。 71 | 72 | 以上是 `#define` 的「巨集」用法,而 `#define` 還有另一個功用,前置處理器也有 if 的語法,它們都和 `#define` 有關。 73 | 74 | 最單純的是 `#if`、`#elif`、`#else`、`#endif`,用法例如: 75 | ```cpp 76 | #include 77 | #define A 3 78 | using namespace std; 79 | 80 | int main(){ 81 | #if A == 3 82 | cout << "test\n"; 83 | #elif A == 2 84 | cout << "hello\n"; 85 | #else 86 | cout << "QQ\n"; 87 | #endif 88 | 89 | return 0; 90 | } 91 | ``` 92 | 這就像是一般的 if 控制語法,只是以 `#endif` 結束區塊,要巢狀結構也可以。如果判斷結果是 `true`,那個區塊才會被留下來,否則會被刪掉,上述的範例中,只有 `A==3` 這個判斷會是 `true`,因此只有第 7 行會留下來,第 9 和 11 行不會。前置處理器的判斷只能判 `#define` 定義的東西,可以判等於、比大小等等。如果把第 2 行改成 `#define A 2`,那被留下的就會是第 9 行,如果 `A` 不是 `2` 也不是 `3`,那被留下的就會是第 11 行。 93 | 94 | 除了可以判斷值之外,也可以判斷一個 identifier 有無被定義,用 `defined(identifier)` 就可以得到指定的 identifier 有沒有 define 過,例如: 95 | ```cpp 96 | #include 97 | #define A 98 | using namespace std; 99 | 100 | int main(){ 101 | #if defined(A) 102 | cout << "test\n"; 103 | #endif 104 | #if defined(B) 105 | cout << "hi\n"; 106 | #endif 107 | 108 | return 0; 109 | } 110 | ``` 111 | 第 7 行會被留下來,而第 10 行不會。`#if defined(A)` 可以簡寫為 `#ifdef A`,而 `#if !defined(A)` 可以簡寫為 `#ifndef A`。至於已經 define 過的東西可以用 `#undef` 移除。 112 | 113 | 這通常會用來避免標頭檔被重複 include,至於競賽用途,如果你打了一大段測試用的程式碼,覺得要在 submit 前註解掉、WA 了又要取消註解很麻煩,那就可以用個 `#ifdef` 來處理,會方便許多。 114 | 115 | ## define 花式用法 116 | 117 | 其實通常也只會用到 define 而已 (?),define 在競賽中最大的功能就是把常用的東西變得比較好寫,例如這是一個常見的 pair 模板: 118 | 119 | ```cpp 120 | #define mp make_pair 121 | #define F first 122 | #define S second 123 | using pii = pair; 124 | ``` 125 | 126 | 除了普通的把東西替換掉之外,function macro 還有一種特別的寫法: 127 | 128 | ```cpp 129 | #define err(a) cerr << #a << ": " << a << "\n" 130 | 131 | int num = 123; 132 | err(num); // 輸出 num: 123 133 | ``` 134 | 135 | `#a` 就是把你在 `a` 打的東西變成字串替換上去,所以 `err(num);` 會被替換成: 136 | 137 | ```cpp 138 | cerr << "num" << ": " << a << "\n"; 139 | ``` -------------------------------------------------------------------------------- /_posts/note/misc/stringstream.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: StringStream 3 | --- 4 | # StringStream 5 | 6 | 有一個類型叫 iostream,它同時是輸入流也是輸出流,而 stringstream 正是一種 iostream。它不像標準流,有固定的輸出去向或輸入來源,簡單來講,它就是一個流水線,你可以從任何地方放東西進去、然後再把裡面的東西拿出來到任何地方。像是你就無法在程式中把標準輸出流的緩衝區裡的東西拿出來,你只能在 terminal 或它導向的檔案裡見到你放進標準輸出流的東西(就是你輸出的東西)。 7 | 8 | 直接來看個範例就可以暸解用法了: 9 | ```cpp= 10 | stringstream ss; 11 | ss << 5; 12 | int t; 13 | ss >> t; 14 | cout << t << "\n"; 15 | ``` 16 | 17 | 最後的輸出是 5,可以看到它既可以 `<<` 也可以 `>>`,在第 2 行做的事情是把 5 丟進 ss 裡面,只要是可以給 cout 輸出的類型都可以這麼做,而第 4 行做的事是把 ss 裡的東西拿出來,然後放到變數 t,於是 t 就變成 56 了,同樣地,可以用 cin 輸入到的類型都可以這麼做,而它的行為和 cin 也一模一樣,像是: 18 | 19 | ```cpp= 20 | stringstream ss; 21 | ss << 5 << " " << 6; 22 | int t; 23 | ss >> t; 24 | cout << t << "\n"; 25 | ``` 26 | 27 | 第 2 行改成了把 5 丟進去、把含有一個空白的字串丟進去、再把 6 丟進去,這個時候流裡面會是 \texttt{5 6},中間有一個空白,所以在第 4 行輸出東西到 t 的時候,和 cin 一樣會根據空白分割字串,只有 5 進到了 t 且離開了流,空白和 6 還留在其中。 28 | 29 | 至於 stringstream 有什麼用處呢?除了可以把各種類型互相轉換外(有實作輸入或輸出方法的類型),還有像是有些題目的輸入格式比較討厭,一個檔案多筆測資、一筆測資占一行,但又不告訴你一筆測資有幾個數字,這個時候你就可以先用 getline 把整行讀到一個 string、然後再用 stringstream 做分割。 -------------------------------------------------------------------------------- /_posts/note/sqrt/mo-algorithm.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: 莫隊算法 3 | --- 4 | # 莫隊算法 5 | 6 | 莫隊算法是一種用來處理區間問題的離線算法,也就是說,它會在把所有詢問讀進來之後,再利用詢問之間的關聯性來降低運算時間,所以它在像是 IOI 這種一定要先回傳答案才可以讀下一筆詢問的比賽不能用,但大多數的比賽都可以。 7 | 8 | 莫隊算法需要用到分塊法,令一塊大小為 $k$,序列長為 $n$,總塊數為 $b=\lceil \frac{n}{k} \rceil$,塊的編號從 $0$ 開始,第 $i$ 個元素所在的塊是 $\lfloor \frac{i}{k} \rfloor$,詢問有 $q$ 筆。 9 | 10 | 要使用莫隊算法有一個先決條件:如果已經知道 $[l,r]$ 的答案,那必須要可以快速地知道 $[l,r+1]$、$[l,r-1]$、$[l+1,r]$、$[l-1,r]$ 的答案,也就是把左或右界移動一個位置,令算出新答案的複雜度為 $O(P)$,有時候它可能是 $O(\log n)$,有時候會是 $O(1)$,視題目要你算什麼而定,至於算不算快就自己評估。 11 | 12 | 接下來是莫隊算法的過程,首先,讀入每一筆詢問,然後對每一筆詢問依**左界所在的塊**由小到大排序,相同的話依**右界位置**(非塊編號)由小到大排序,接著算出第一筆詢問的答案後,用移動邊界的方式來算出下一筆詢問的答案。 13 | 14 | pseudo code: 15 | ```cpp 16 | array queries = 詢問們; 17 | type ans; //目前答案 18 | void add(type v){/*...*/} //增加一個數字,算新答案 19 | void sub(type v){/*...*/} //移除一個數字,算新答案 20 | 21 | int l = 0, r = -1; 22 | for(Query q : queries){ //Query是一筆詢問的資料 23 | while(r < q.右界) add(++r); 24 | while(r > q.右界) sub(r--); 25 | while(l < q.左界) sub(l++); 26 | while(l > q.左界) add(--l); 27 | q的答案 = ans; 28 | } 29 | ``` 30 | 31 | 只要把詢問的順序記下來,得出全部詢問的答案後依序輸出就好了。 32 | 33 | 再來是最重要的複雜度,乍看之下會覺得這樣一直加減加減的,一次還只移動一格,會不會太慢啊? 34 | 35 | 移動一次邊界算新答案是 $O(P)$,然後左界在相同的塊中,每次詢問最多會移動 $k$,因為已經按照左界所在的塊排序了,排序後連續的幾個詢問左界會在同一個塊中,而換塊的話,往右換一塊最多就移動大約 $2k$ 次左界(從一塊的開始移到下一塊的結尾),所以總次數大約是 $2k\lceil \frac{n}{k} \rceil$,約等於 $n$。然後左界在相同的塊時,右界會不斷往右移,因為我們剛剛是照右界位置排序,因此右界最多移動 $n$ 次,總次數是 $bn$。 36 | 37 | 左界在同一塊時,每次詢問左界最多移動 $k$ 次,換塊的總移動次數是 $n$,因此左界移動複雜度是 $O(kq+n)$,而右界移動複雜度會是 $O(bn)$,乘上算出新答案的複雜度 $O(P)$ 再相加,就是 $O((kq+n+bn)P)=O((kq+bn)P)$。 38 | 39 | $b$ 約為 $\frac{n}{k}$,根據算幾不等式,如果要讓 $kq+bn$ 最小,那麼就要 $kq=\frac{n}{k}n$,解出來會得到 $k=\frac{n}{\sqrt{q}}$,代回去後 $kq+bn$ 會變成 $n\sqrt{q}$,複雜度變成 $O(Pn\sqrt{q})$,如果 $O(P)=O(1)$,那複雜度甚至只有 $n\sqrt{q}$。 40 | 41 | ## 帶修改 42 | 43 | TODO 44 | 45 | ## 練習題 46 | 47 | - [ZeroJudge b417](https://zerojudge.tw/ShowProblem?problemid=b417) - 區間眾數 48 | - [ZeroJudge d539](https://zerojudge.tw/ShowProblem?problemid=d539) - 區間最大值 -------------------------------------------------------------------------------- /_posts/note/sqrt/sqrt-decomposition.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: 分塊法 3 | --- 4 | # 分塊法 5 | 6 | 先來考慮一個單點修改、區間查詢的問題,然後來個簡單的想法:如果把數列切成好幾塊,然後在修改之後更新那一塊的答案,如果區間查詢的範圍涵蓋一整塊,那麼這一塊的答案就不必重新算,如果有包含到一塊中的一小段,那這段就暴搜,這樣會不會比較好? 7 | 8 | 數列 $A$ 長度是 $n$,每塊長度是 $k$,那麼分塊會變成類似這樣: 9 | 10 | 11 | 格子中的數字是那一塊的編號,如果數列中的元素是從 $0$ 開始編號的,那麼 $A_i$ 所在的塊編號會是 $\lfloor \frac{i}{k} \rfloor$,總塊數會是 $\lceil \frac{n}{k} \rceil$,注意 $n$ 不一定是 $k$ 的倍數,所以最後會有 $n \bmod k$ 個數字剩下來,它們也算一塊。 12 | 13 | 在初始化時,計算出每一塊的答案,例如如果查詢求的是區間和,那就儲存每一塊的數字和、求的是最大值,就儲存每一塊裡的最大值。 14 | 15 | 單點修改的時候,在修改完後,更新那一塊的答案,如果求的答案是區間和,那麼只要把那一塊的和扣掉原來數字,再加上新的數字,這種「可返回」的東西,可以做 $O(1)$ 修改,至於最大、最小值這種「不可返回」的,就把那一塊重新掃一遍,找出最大或最小值,這樣是 $O(k)$ 修改。 16 | 17 | 區間查詢的時候,如果有完整的一塊被涵蓋,那就直接 $O(1)$ 取那一塊的答案就好,然後暴搜剩下不完整的部分。總塊數有 $\lceil \frac{n}{k} \rceil$ 個,不完整的部分,在查詢區間的左右兩端分別最多 $k - 1$ 個,因此查詢複雜度是 $O(\frac{n}{k}+k)$。 18 | 19 | 接下來來討論一下 $k$ 應該要取多少好,要讓 $\frac{n}{k}+k$ 盡量小一點,那麼算幾不等式可以派上用場: 20 | $$\frac{\frac{n}{k}+k}{2} \geq \sqrt{\frac{n}{k} \times k}$$ 21 | 22 | 要讓等號成立的話,就必須要 $\frac{n}{k}=k$,解出來可以得到 $k=\sqrt{n}$,這樣查詢和修改複雜度就都是 $O(\sqrt{n})$ 了,如果詢問有 $q$ 筆,再加上預處理的時間,整體複雜度就是 $O(n + q\sqrt{n})$,比暴力破解快了很多。 23 | 24 | ## 懶人標記 25 | 26 | 如果要做區間修改的話,用剛剛的方式會變成要對每個單點修改一遍、再去更新每一塊的答案,這樣複雜度是 $O(n)$,太久了。 27 | 28 | 觀察一下,如果整塊都一起被修改會發生什麼事? 29 | 30 | - 如果整塊同時加上 $x$,那麼最大或最小值會加上 $x$,數字和會加上 $x$ 乘上塊大小。 31 | - 如果整塊同時變成 $x$,那麼最大或最小值會變成 $x$,數字和會是 $x$ 乘上塊大小。 32 | 33 | 由此可知,如果整塊一起變動,那我們可能可以推算出我們想要的答案會如何改變,此時我們就不要浪費時間去修改全部的數字,多開一個變數來儲存這一塊都被改成什麼數字或被多加上多少,這就稱為**懶人標記**。至於邊邊不到一整塊的部分,就一樣暴力處理。 34 | 35 | 如果某一塊已經有懶人標記,也就是已經被整塊一起修改過,那麼如果之後要再變動這一整塊,只要修改懶人標記就好;但如果之後是只變動部分呢?如果修改是加值的話,硬加上去,不理會懶人標記是可以的,但如果修改是改值,而不理會懶人標記,那麼在查詢時,這個元素的值還是會被覆蓋掉,所以就必須要依懶人標記所記錄的修改,去修改那一塊裡的每一個元素,再去修改剛剛想改的那一部分了。 36 | 37 | 打一塊的懶人標記是 $O(1)$,塊數最多是 $\frac{n}{k}$,邊邊不到完整一塊的部分,最多就是兩塊,改其中一塊的複雜度是 $O(k)$,因此整體複雜度是 $O(\frac{n}{k}+k)$,把 $k=\sqrt{n}$ 代進去就是 $O(\sqrt{n})$,根本沒變。 38 | 39 | ## 練習題 40 | 41 | - [ZeroJudge d539](https://zerojudge.tw/ShowProblem?problemid=d539) - 區間查詢最大值 42 | - [ZeroJudge d799](https://zerojudge.tw/ShowProblem?problemid=d799) - 區間修改、區間查詢和 43 | -------------------------------------------------------------------------------- /_posts/note/string/longest-palindromic-substring.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: 最長迴文子字串 3 | --- 4 | # 最長迴文子字串(Longest Palindromic Substring, LPS) 5 | 6 | 給一個字串 $S$,求 $S$ 中最長的為迴文的子字串長度。 7 | 8 | ## Manacher's Algorithm 9 | 10 | 為了方便,接下來的索引值都是從 0 開始,也就是說,$S$ 的第一個字元是 $S_0$。 11 | 12 | 首先,先求出以每個字元為中心點的最長迴文長度,但偶數長度的迴文沒有中心點,所以這邊做一個操作:在每個字元中間和 $S$ 兩端加上一個 $S$ 中沒有的字元 $c$,結果會是:$c S_0 c S_1 c S_2 c S_3 \dots S_{|S|-1} c$。 13 | 14 | 這個加工過的字串為 $T$,長度是 $2 \times |S| + 1$,可以用一個簡單的方式來得到 $T_i$: 15 | $$ 16 | T_i = 17 | \begin{cases} 18 | S_{\lfloor \frac{i}{2} \rfloor} &\quad \text{if }i \bmod 2 = 1 \\ 19 | c &\quad \text{else} 20 | \end{cases} 21 | $$ 22 | 接下來的程式碼會用 $c$ 為 `.` 作為範例。 23 | ```cpp 24 | #define T(x) ((x) % 2 ? s[(x) / 2] : '.') 25 | ``` 26 | 27 | 接下來,我們用 $R_i$ 來表示以 $T_i$ 為中心點的最長迴文半徑(含中心點)。再來的重點是要如何快速求出 $R_i$,先從 $T$ 的左邊讀到右邊,並且一邊計算 $R_i$ 為何。 28 | 29 | 令 $\text{mx}$ 為目前為止,我們找到的迴文子字串的**最右邊的結尾**,也就是:$\max \{i+R_i-1\}$,而 $\text{center}$ 為這個**結尾在最右邊**的迴文子字串的中心點,也就是:$\text{mx}=\text{center}+R_\text{center}-1$。 30 | 31 | 先寫一個 function 來方便等等的計算,$\text{ex}(l,r)$ 表示從 $l$ 往左和 $r$ 往右的最長對稱字串長度,也就是說:令 $t=\text{ex}(l,r)$,$T_{(l-t+1)..l}=T_{r..(r+t-1)}$,且 $t$ 盡可能地大。 32 | ```cpp 33 | int ex(int l, int r){ 34 | int i = 0; 35 | while(l - i >= 0 && r + i < n && T(l - i) == T(r + i)) i++; 36 | return i; 37 | } 38 | ``` 39 | 40 | 如果我們已經算好了前 $i-1$ 項,現在要算第 $i$ 項,我們會遇到幾種狀況: 41 | 42 | - $T_i$ 沒有被任何中心點在前面的迴文覆蓋($i > \text{mx}$) 43 | 在這樣的情況下,我們不能用先前算好的東西來得到 $R_i$,所以只能硬算: 44 | ```cpp 45 | r[i] = ex(i, i); 46 | center = i; 47 | mx = i + r[i] - 1; 48 | ``` 49 | - $T_i$ 有被中心點在前面的迴文覆蓋($i \leq \text{mx}$) 50 | 如果這樣的話,我們可以利用覆蓋 $T_i$ 的迴文另一邊的對稱字串來得到 $R_i$。令 $i'$ 為迴文另一邊的對稱點,也就是說:$i'=\text{center}-(i-\text{center})$,接著我們令 $\text{len}$ 為 $T_i$ 到這個迴文結尾的長,也就是 $\text{len}=\text{mx}-i+1$。再來又分成了幾種情況: 51 | - 以 $T_{i'}$ 為中心的最長迴文剛好貼齊以 $T_{\text{center}}$ 為中心的迴文邊界($R_{i'}=\text{len}$) 52 | 這樣子我們只能確保 $R_i \geq R_{i'}$,因為我們並不知道 $T_i+R_{i'}$ 是否等於 $T_i-R_{i'}$,所以只好接著算:$R_i=R_{i'}+\text{ex}(i-R_{i'}, i+R_{i'})$。 53 | - 以 $T_{i'}$ 為中心的最長迴文在 $T_{\text{center}}$ 為中心的迴文範圍以內($R_{i'}<\text{len}$) 54 | 這樣我們直接確定 $R_i=R_{i'}$ 了,因為以 $T_{\text{center}}$ 為中心的迴文兩邊是對稱的。 55 | - 以 $T_{i'}$ 為中心的最長迴文超出 $T_{\text{center}}$ 為中心的迴文範圍($R_{i'}>\text{len}$) 56 | 仔細想一下會發現: 57 | $$ 58 | T_{\text{center}-R_\text{center}} \neq T_{\text{center}+R_\text{center}}\\ 59 | T_{\text{center}-R_\text{center}} = T_{i'-\text{len}} = T_{i'+\text{len}} = T_{i-\text{len}} \\ 60 | T_{\text{center}+R_\text{center}} = T_{i+\text{len}} \\ 61 | T_{i-\text{len}} \neq T_{i+\text{len}} 62 | $$ 63 | 由此可知,$R_i$ 不會超過 $\text{len}$,而我們知道以 $T_{i'}$ 為中心的最長迴文是 $R_{i'}$,而 $R_{i'} > \text{len}$也就是說,存在以 $T_i$ 為中心,長度為 $\text{len}$ 的迴文,因此,$R_{i}=\text{len}$。 64 | ```cpp 65 | int ii = center - (i - center); 66 | int len = mx - i + 1; 67 | if(i > mx){ 68 | r[i] = ex(i, i); 69 | center = i; 70 | mx = i + r[i] - 1; 71 | } 72 | else if(r[ii] == len){ 73 | r[i] = len + ex(i - len, i + len); 74 | center = i; 75 | mx = i + r[i] - 1; 76 | } 77 | else{ 78 | r[i] = min(r[ii], len); 79 | } 80 | ``` 81 | 82 | 最後,只要算怎麼把最大的 $R_i$ 轉成 $S$ 中最大迴文子字串的長度就好了。我們知道 $S$ 的每一個字元在 $T$ 中,前方都會有一個字元 $c$,而 $T$ 最後也有一個 $c$,剛好以 $T_i$ 為中心的最長迴文,兩端也會是 $c$,所以我們可以用一樣的想法來轉回去:令以 $T_i$ 為中心的最長迴文長度為 $t=R_i \times 2 - 1$,轉回去 $S$ 的長度就是 $\frac{(t-1)}{2}$(注意 $t$ 必為奇數),化簡後得到 $R_i-1$。 83 | 84 | ```cpp 85 | #define T(x) ((x) % 2 ? s[(x) / 2] : '.') 86 | 87 | string s; 88 | int n; 89 | 90 | int ex(int l, int r){ 91 | int i = 0; 92 | while(l - i >= 0 && r + i < n && T(l - i) == T(r + i)) i++; 93 | return i; 94 | } 95 | 96 | int main(){ 97 | cin >> s; 98 | n = 2 * s.size() + 1; 99 | 100 | int mx = 0; 101 | int center = 0; 102 | vector r(n); 103 | int ans = 1; 104 | r[0] = 1; 105 | for(int i = 1; i < n; i++){ 106 | int ii = center - (i - center); 107 | int len = mx - i + 1; 108 | if(i > mx){ 109 | r[i] = ex(i, i); 110 | center = i; 111 | mx = i + r[i] - 1; 112 | } 113 | else if(r[ii] == len){ 114 | r[i] = len + ex(i - len, i + len); 115 | center = i; 116 | mx = i + r[i] - 1; 117 | } 118 | else{ 119 | r[i] = min(r[ii], len); 120 | } 121 | ans = max(ans, r[i]); 122 | } 123 | 124 | cout << ans - 1 << "\n"; 125 | return 0; 126 | } 127 | ``` 128 | 129 | 仔細觀察會發現,這個過程只是不斷把 $\text{mx}$ 往右移,因此複雜度是漂亮的 $O(|S|)$。 -------------------------------------------------------------------------------- /home.md: -------------------------------------------------------------------------------- 1 | # WiwiHo 的競程筆記 2 | 3 | 歡迎來到 WiwiHo 的~~唬爛專區~~競程筆記,這裡會整理我的筆記和上過課的內容,希望對你有幫助。這裡是一個最近沒什麼在更新和維護的地方,最近我大部分的時間都在寫 [NTUCPC Guide](https://guide.ntucpc.org/),歡迎去看看。 4 | 5 | 文章原檔都在 [GitHub](https://github.com/wiwiho/cpnote-source) 上,如果有任何疑問,請至 [issue](https://github.com/wiwiho/cpnote-source/issues),~~如果你想幫我寫,請至 [pull request](https://github.com/wiwiho/cpnote-source/pulls)~~。 6 | 7 | ## 關於我 8 | 9 | [我的個人網站](https://www.wiwiho.me/) 10 | 11 | - 不會數學的競程選手 12 | - 常用 handle:wiwiho/WiwiHo 13 | - IOI 2021 銀牌、IOI 2022 金牌 14 | - 師大附中資訊培訓講師 15 | 16 | ## 目錄 17 | 18 | - [我教過的課](/lessons) 19 | - 雜項 20 | - [複雜度](/complexity) 21 | - [前置處理器](/preprocessor) 22 | - [前綴和與差分](/prefix-sum) 23 | - 輸入輸出 24 | - [I/O Stream](/iostream) 25 | - [String Stream](/stringstream) 26 | - [輸入輸出優化](/io-optimize) 27 | - 數字與位元 28 | - [進位制](/radix) 29 | - [整數與浮點數](/int-and-float) 30 | - [位元運算](/bitwise-operator) 31 | - 基礎資料結構 32 | - [類陣列容器](/array-like-ds) 33 | - [Linked List](/linked-list) 34 | - 動態規劃 35 | - DP 優化 36 | - [Aliens 優化](/aliens) 37 | - 數學 38 | - [歐幾里得演算法](/gcd) 39 | - [快速冪](/pow) 40 | - [質數與因數](/prime-and-factor) 41 | - 幾何 42 | - [向量](/vector) 43 | - [向量應用](/vector-application) 44 | - [凸包](/convex-hull) 45 | - 進階資料結構 46 | - [Sparse Table](/sparse-table) 47 | - [線段樹](/segment-tree) 48 | - [樹狀數組](/fenwick-tree) 49 | - 根號 50 | - [根號分塊](/sqrt-decomposition) 51 | - [莫隊算法](/mo-algorithm) 52 | - 字串 53 | - [最長迴文子字串](/longest-palindromic-substring) 54 | -------------------------------------------------------------------------------- /images/advanced-ds/fenwick-tree/bit.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wiwiho/cpnote-source/743b13cc4c58c308b72aaba7d4ad18c17de232ee/images/advanced-ds/fenwick-tree/bit.png -------------------------------------------------------------------------------- /images/advanced-ds/fenwick-tree/bit.tex: -------------------------------------------------------------------------------- 1 | \documentclass[12pt, tikz, margin=1cm]{standalone} 2 | \usepackage{xeCJK} 3 | \usepackage{fontspec} 4 | % \usepackage[a4paper,top=2.8cm,bottom=2.8cm,left=2.5cm,right=2.5cm, landscape]{geometry} 5 | \usepackage{graphicx} 6 | \usepackage{listings} 7 | \usepackage{xcolor} 8 | \usepackage{indentfirst} 9 | \usepackage{tikz} 10 | \usepackage{amssymb} 11 | \usepackage{amsthm} 12 | \usepackage{amsmath} 13 | \usepackage{tabularx} 14 | \usepackage{hyperref} 15 | \usepackage{ulem} 16 | \usepackage{version} 17 | \usepackage{thmtools} 18 | \usepackage{qtree} 19 | \usepackage{algpseudocode} 20 | \usepackage{mathtools} 21 | % \usepackage[active,tightpage]{preview} 22 | % \usepackage{varwidth} 23 | 24 | \XeTeXlinebreaklocale "zh" 25 | \XeTeXlinebreakskip = 0pt plus 1pt 26 | 27 | \setCJKmainfont[BoldFont={SourceHanSerifTW-SemiBold.otf},AutoFakeSlant]{SourceHanSerifTW-Regular.otf} 28 | \usetikzlibrary{arrows,decorations.markings,decorations.pathreplacing} 29 | 30 | \tikzstyle {graph node} = [circle, draw, minimum width=1cm] 31 | \tikzset{edge/.style = {decoration={markings,mark=at position 1 with % 32 | {\arrow[scale=2,>=stealth]{>}}},postaction={decorate}}} 33 | 34 | \begin{document} 35 | 36 | % \begin{preview} 37 | % \begin{varwidth}{\linewidth} 38 | 39 | \begin{tikzpicture}[scale=1.5] 40 | \node[circle, draw, minimum width=1cm] (v0) at (0, 5) {0}; 41 | \node[circle, draw, minimum width=1cm] (v1) at (1, 4) {1}; 42 | \node[circle, draw, minimum width=1cm] (v2) at (2, 4) {2}; 43 | \node[circle, draw, minimum width=1cm] (v4) at (4, 4) {4}; 44 | \node[circle, draw, minimum width=1cm] (v8) at (8, 4) {8}; 45 | \node[circle, draw, minimum width=1cm] (v3) at (3, 3) {3}; 46 | \node[circle, draw, minimum width=1cm] (v5) at (5, 3) {5}; 47 | \node[circle, draw, minimum width=1cm] (v6) at (6, 3) {6}; 48 | \node[circle, draw, minimum width=1cm] (v9) at (9, 3) {9}; 49 | \node[circle, draw, minimum width=1cm] (v10) at (10, 3) {10}; 50 | \node[circle, draw, minimum width=1cm] (v12) at (12, 3) {12}; 51 | \node[circle, draw, minimum width=1cm] (v7) at (7, 2) {7}; 52 | \node[circle, draw, minimum width=1cm] (v11) at (11, 2) {11}; 53 | \node[circle, draw, minimum width=1cm] (v13) at (13, 2) {13}; 54 | \node[circle, draw, minimum width=1cm] (v14) at (14, 2) {14}; 55 | \node[circle, draw, minimum width=1cm] (v15) at (15, 1) {15}; 56 | 57 | \node at (0, 5-0.5) {0000}; 58 | \node at (1, 4-0.5) {0001}; 59 | \node at (2, 4-0.5) {0010}; 60 | \node at (4, 4-0.5) {0100}; 61 | \node at (8, 4-0.5) {1000}; 62 | \node at (3, 3-0.5) {0011}; 63 | \node at (5, 3-0.5) {0101}; 64 | \node at (6, 3-0.5) {0110}; 65 | \node at (9, 3-0.5) {1001}; 66 | \node at (10, 3-0.5) {1010}; 67 | \node at (12, 3-0.5) {1100}; 68 | \node at (7, 2-0.5) {0111}; 69 | \node at (11, 2-0.5) {1011}; 70 | \node at (13, 2-0.5) {1101}; 71 | \node at (14, 2-0.5) {1110}; 72 | \node at (15, 1-0.5) {1111}; 73 | 74 | \draw (v0.east) -- (v1.north); 75 | \draw (v0.east) -- (v2.north); 76 | \draw (v0.east) -- (v4.north); 77 | \draw (v0.east) -- (v8.north); 78 | \draw (v2.east) -- (v3.north); 79 | \draw (v4.east) -- (v5.north); 80 | \draw (v4.east) -- (v6.north); 81 | \draw (v6.east) -- (v7.north); 82 | \draw (v8.east) -- (v9.north); 83 | \draw (v8.east) -- (v10.north); 84 | \draw (v8.east) -- (v12.north); 85 | \draw (v10.east) -- (v11.north); 86 | \draw (v12.east) -- (v13.north); 87 | \draw (v12.east) -- (v14.north); 88 | \draw (v14.east) -- (v15.north); 89 | \end{tikzpicture} 90 | 91 | % \end{varwidth} 92 | % \end{preview} 93 | 94 | \end{document} -------------------------------------------------------------------------------- /images/advanced-ds/segment-tree/segtree.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wiwiho/cpnote-source/743b13cc4c58c308b72aaba7d4ad18c17de232ee/images/advanced-ds/segment-tree/segtree.png -------------------------------------------------------------------------------- /images/advanced-ds/segment-tree/segtree.py: -------------------------------------------------------------------------------- 1 | def draw(l, r, L, R, dpt, ty, num, pr): 2 | if l <= L and R <= r and ty == 'pass node': 3 | ty = 'end node' 4 | elif ty == 'end node': 5 | ty = 'more node' 6 | elif max(l, L) > min(r, R): 7 | ty = 'normal node' 8 | print('\\node[', ty,'] (v', num, ') at (', (L + R) / 2, ',', -dpt, ') {};', sep='') 9 | if pr != -1: 10 | print('\\draw (v', pr, ') -- (v', num, ');', sep='') 11 | if L == R: 12 | return 13 | M = (L + R) // 2 14 | draw(l, r, L, M, dpt + 1, ty, 2 * num, num) 15 | draw(l, r, M + 1, R, dpt + 1, ty, 2 * num + 1, num) 16 | 17 | draw(10, 26, 1, 32, 0, 'pass node', 1, -1) 18 | -------------------------------------------------------------------------------- /images/advanced-ds/segment-tree/segtree.tex: -------------------------------------------------------------------------------- 1 | \documentclass[10pt]{article} 2 | \usepackage[paperwidth=40cm, paperheight=10cm, margin=1cm]{geometry} 3 | \usepackage{xeCJK} 4 | \usepackage{fontspec} 5 | \usepackage{graphicx} 6 | \usepackage{xcolor} 7 | \usepackage{indentfirst} 8 | \usepackage{tikz} 9 | \usepackage{pgfplots} 10 | \usepackage{amssymb} 11 | \usepackage{amsthm} 12 | \usepackage{amsmath} 13 | \usepackage{fancyhdr} 14 | \usepackage{tabularx} 15 | \usepackage{hyperref} 16 | \usepackage{xurl} 17 | \usepackage{ulem} 18 | \usepackage{version} 19 | \usepackage{thmtools} 20 | \usepackage{qtree} 21 | \usepackage{algorithm} 22 | \usepackage{algpseudocode} 23 | \usepackage{mathtools} 24 | \usepackage[shortlabels]{enumitem} 25 | \usepackage{minted} 26 | \usepackage{mdframed} 27 | \usepackage{lipsum} 28 | 29 | \XeTeXlinebreaklocale "zh" 30 | \XeTeXlinebreakskip = 0pt plus 1pt 31 | 32 | \setCJKmainfont{Noto Serif CJK TC} 33 | \setmonofont{Source Code Pro} 34 | \usetikzlibrary{arrows,decorations.markings,decorations.pathreplacing} 35 | \pagestyle{empty} 36 | 37 | \tikzstyle {graph node} = [circle, draw, minimum width=1cm] 38 | \tikzset{edge/.style = {decoration={markings,mark=at position 1 with % 39 | {\arrow[scale=2,>=stealth]{>}}},postaction={decorate}}} 40 | 41 | \renewcommand{\baselinestretch}{1.3} 42 | 43 | \newtheoremstyle{owo}% name of the style to be used 44 | {10pt}% measure of space to leave above the theorem. E.g.: 3pt 45 | {10pt}% measure of space to leave below the theorem. E.g.: 3pt 46 | {}% name of font to use in the body of the theorem 47 | {}% measure of space to indent 48 | {\bfseries}% name of head font 49 | { }% punctuation between head and body 50 | { }% space after theorem head; " " = normal interword space 51 | {\thmname{#1}\thmnumber{ #2}.\thmnote{ (#3)}} 52 | 53 | \theoremstyle{owo} 54 | \newtheorem{Theorem}{Theorem} 55 | \newtheorem{Lemma}{Lemma} 56 | \newtheorem{Observation}{Observation} 57 | \newtheorem{Corollary}{Corollary} 58 | \newtheorem*{Proof}{Proof} 59 | 60 | \DeclareMathOperator*{\argmin}{argmin} 61 | 62 | \hypersetup{ 63 | %colorlinks=true, 64 | } 65 | 66 | \usemintedstyle{vs} 67 | \renewcommand{\theFancyVerbLine}{\ttfamily\normalsize\arabic{FancyVerbLine}} 68 | \newenvironment{Code}[1]{~\vspace{-15pt}\VerbatimEnvironment\begin{minted}[bgcolor=black!5!white, breaklines, linenos]{#1}}{\end{minted}} 69 | 70 | \newcommand{\Inline}{\mintinline[bgcolor=black!5!white, breaklines]{text}} 71 | 72 | \newmdenv[topline=false, bottomline=false, rightline=false, linewidth=3pt, linecolor=black!10!white]{Quotemd} 73 | \newenvironment{Quote}{~\vspace{-15pt}\begin{Quotemd}}{\end{Quotemd}} 74 | 75 | \begin{document} 76 | 77 | \setlength\parindent{20pt} 78 | \setlength\parskip{10pt} 79 | 80 | \setlist[enumerate]{itemsep=0pt, parsep=0pt, topsep=0pt} 81 | \setlist[itemize]{itemsep=0pt, parsep=0pt, topsep=0pt} 82 | 83 | \tikzstyle {normal node} = [circle, draw, minimum width=0.8cm] 84 | \tikzstyle {pass node} = [circle, draw, fill, minimum width=0.8cm, red!70!white] 85 | \tikzstyle {end node} = [circle, draw, fill, minimum width=0.8cm, blue!70!white] 86 | \tikzstyle {more node} = [circle, draw, fill, minimum width=0.8cm, black!20!white] 87 | 88 | \centering 89 | \begin{tikzpicture} 90 | \node[pass node] (v1) at (16.5,0) {}; 91 | \node[pass node] (v2) at (8.5,-1) {}; 92 | \draw (v1) -- (v2); 93 | \node[normal node] (v4) at (4.5,-2) {}; 94 | \draw (v2) -- (v4); 95 | \node[normal node] (v8) at (2.5,-3) {}; 96 | \draw (v4) -- (v8); 97 | \node[normal node] (v16) at (1.5,-4) {}; 98 | \draw (v8) -- (v16); 99 | \node[normal node] (v32) at (1.0,-5) {}; 100 | \draw (v16) -- (v32); 101 | \node[normal node] (v33) at (2.0,-5) {}; 102 | \draw (v16) -- (v33); 103 | \node[normal node] (v17) at (3.5,-4) {}; 104 | \draw (v8) -- (v17); 105 | \node[normal node] (v34) at (3.0,-5) {}; 106 | \draw (v17) -- (v34); 107 | \node[normal node] (v35) at (4.0,-5) {}; 108 | \draw (v17) -- (v35); 109 | \node[normal node] (v9) at (6.5,-3) {}; 110 | \draw (v4) -- (v9); 111 | \node[normal node] (v18) at (5.5,-4) {}; 112 | \draw (v9) -- (v18); 113 | \node[normal node] (v36) at (5.0,-5) {}; 114 | \draw (v18) -- (v36); 115 | \node[normal node] (v37) at (6.0,-5) {}; 116 | \draw (v18) -- (v37); 117 | \node[normal node] (v19) at (7.5,-4) {}; 118 | \draw (v9) -- (v19); 119 | \node[normal node] (v38) at (7.0,-5) {}; 120 | \draw (v19) -- (v38); 121 | \node[normal node] (v39) at (8.0,-5) {}; 122 | \draw (v19) -- (v39); 123 | \node[pass node] (v5) at (12.5,-2) {}; 124 | \draw (v2) -- (v5); 125 | \node[pass node] (v10) at (10.5,-3) {}; 126 | \draw (v5) -- (v10); 127 | \node[pass node] (v20) at (9.5,-4) {}; 128 | \draw (v10) -- (v20); 129 | \node[normal node] (v40) at (9.0,-5) {}; 130 | \draw (v20) -- (v40); 131 | \node[end node] (v41) at (10.0,-5) {}; 132 | \draw (v20) -- (v41); 133 | \node[end node] (v21) at (11.5,-4) {}; 134 | \draw (v10) -- (v21); 135 | \node[more node] (v42) at (11.0,-5) {}; 136 | \draw (v21) -- (v42); 137 | \node[more node] (v43) at (12.0,-5) {}; 138 | \draw (v21) -- (v43); 139 | \node[end node] (v11) at (14.5,-3) {}; 140 | \draw (v5) -- (v11); 141 | \node[more node] (v22) at (13.5,-4) {}; 142 | \draw (v11) -- (v22); 143 | \node[more node] (v44) at (13.0,-5) {}; 144 | \draw (v22) -- (v44); 145 | \node[more node] (v45) at (14.0,-5) {}; 146 | \draw (v22) -- (v45); 147 | \node[more node] (v23) at (15.5,-4) {}; 148 | \draw (v11) -- (v23); 149 | \node[more node] (v46) at (15.0,-5) {}; 150 | \draw (v23) -- (v46); 151 | \node[more node] (v47) at (16.0,-5) {}; 152 | \draw (v23) -- (v47); 153 | \node[pass node] (v3) at (24.5,-1) {}; 154 | \draw (v1) -- (v3); 155 | \node[end node] (v6) at (20.5,-2) {}; 156 | \draw (v3) -- (v6); 157 | \node[more node] (v12) at (18.5,-3) {}; 158 | \draw (v6) -- (v12); 159 | \node[more node] (v24) at (17.5,-4) {}; 160 | \draw (v12) -- (v24); 161 | \node[more node] (v48) at (17.0,-5) {}; 162 | \draw (v24) -- (v48); 163 | \node[more node] (v49) at (18.0,-5) {}; 164 | \draw (v24) -- (v49); 165 | \node[more node] (v25) at (19.5,-4) {}; 166 | \draw (v12) -- (v25); 167 | \node[more node] (v50) at (19.0,-5) {}; 168 | \draw (v25) -- (v50); 169 | \node[more node] (v51) at (20.0,-5) {}; 170 | \draw (v25) -- (v51); 171 | \node[more node] (v13) at (22.5,-3) {}; 172 | \draw (v6) -- (v13); 173 | \node[more node] (v26) at (21.5,-4) {}; 174 | \draw (v13) -- (v26); 175 | \node[more node] (v52) at (21.0,-5) {}; 176 | \draw (v26) -- (v52); 177 | \node[more node] (v53) at (22.0,-5) {}; 178 | \draw (v26) -- (v53); 179 | \node[more node] (v27) at (23.5,-4) {}; 180 | \draw (v13) -- (v27); 181 | \node[more node] (v54) at (23.0,-5) {}; 182 | \draw (v27) -- (v54); 183 | \node[more node] (v55) at (24.0,-5) {}; 184 | \draw (v27) -- (v55); 185 | \node[pass node] (v7) at (28.5,-2) {}; 186 | \draw (v3) -- (v7); 187 | \node[pass node] (v14) at (26.5,-3) {}; 188 | \draw (v7) -- (v14); 189 | \node[end node] (v28) at (25.5,-4) {}; 190 | \draw (v14) -- (v28); 191 | \node[more node] (v56) at (25.0,-5) {}; 192 | \draw (v28) -- (v56); 193 | \node[more node] (v57) at (26.0,-5) {}; 194 | \draw (v28) -- (v57); 195 | \node[normal node] (v29) at (27.5,-4) {}; 196 | \draw (v14) -- (v29); 197 | \node[normal node] (v58) at (27.0,-5) {}; 198 | \draw (v29) -- (v58); 199 | \node[normal node] (v59) at (28.0,-5) {}; 200 | \draw (v29) -- (v59); 201 | \node[normal node] (v15) at (30.5,-3) {}; 202 | \draw (v7) -- (v15); 203 | \node[normal node] (v30) at (29.5,-4) {}; 204 | \draw (v15) -- (v30); 205 | \node[normal node] (v60) at (29.0,-5) {}; 206 | \draw (v30) -- (v60); 207 | \node[normal node] (v61) at (30.0,-5) {}; 208 | \draw (v30) -- (v61); 209 | \node[normal node] (v31) at (31.5,-4) {}; 210 | \draw (v15) -- (v31); 211 | \node[normal node] (v62) at (31.0,-5) {}; 212 | \draw (v31) -- (v62); 213 | \node[normal node] (v63) at (32.0,-5) {}; 214 | \draw (v31) -- (v63); 215 | \end{tikzpicture} 216 | 217 | \end{document} 218 | -------------------------------------------------------------------------------- /images/advanced-ds/sparse-table/sparse-table.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wiwiho/cpnote-source/743b13cc4c58c308b72aaba7d4ad18c17de232ee/images/advanced-ds/sparse-table/sparse-table.png -------------------------------------------------------------------------------- /images/dp/aliens/animation.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wiwiho/cpnote-source/743b13cc4c58c308b72aaba7d4ad18c17de232ee/images/dp/aliens/animation.gif -------------------------------------------------------------------------------- /images/geometry/convex-hull/build.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wiwiho/cpnote-source/743b13cc4c58c308b72aaba7d4ad18c17de232ee/images/geometry/convex-hull/build.png -------------------------------------------------------------------------------- /images/geometry/convex-hull/build2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wiwiho/cpnote-source/743b13cc4c58c308b72aaba7d4ad18c17de232ee/images/geometry/convex-hull/build2.png -------------------------------------------------------------------------------- /images/geometry/convex-hull/convex-hull.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wiwiho/cpnote-source/743b13cc4c58c308b72aaba7d4ad18c17de232ee/images/geometry/convex-hull/convex-hull.png -------------------------------------------------------------------------------- /images/geometry/convex-hull/rotating-calipers1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wiwiho/cpnote-source/743b13cc4c58c308b72aaba7d4ad18c17de232ee/images/geometry/convex-hull/rotating-calipers1.png -------------------------------------------------------------------------------- /images/geometry/convex-hull/rotating-calipers2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wiwiho/cpnote-source/743b13cc4c58c308b72aaba7d4ad18c17de232ee/images/geometry/convex-hull/rotating-calipers2.png -------------------------------------------------------------------------------- /images/geometry/convex-hull/rotating-calipers3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wiwiho/cpnote-source/743b13cc4c58c308b72aaba7d4ad18c17de232ee/images/geometry/convex-hull/rotating-calipers3.png -------------------------------------------------------------------------------- /images/geometry/convex-hull/rotating-calipers4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wiwiho/cpnote-source/743b13cc4c58c308b72aaba7d4ad18c17de232ee/images/geometry/convex-hull/rotating-calipers4.png -------------------------------------------------------------------------------- /images/geometry/convex-hull/sweep-line.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wiwiho/cpnote-source/743b13cc4c58c308b72aaba7d4ad18c17de232ee/images/geometry/convex-hull/sweep-line.png -------------------------------------------------------------------------------- /images/geometry/vector-application/area.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wiwiho/cpnote-source/743b13cc4c58c308b72aaba7d4ad18c17de232ee/images/geometry/vector-application/area.png -------------------------------------------------------------------------------- /images/geometry/vector-application/collinearity.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wiwiho/cpnote-source/743b13cc4c58c308b72aaba7d4ad18c17de232ee/images/geometry/vector-application/collinearity.png -------------------------------------------------------------------------------- /images/geometry/vector-application/inline.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wiwiho/cpnote-source/743b13cc4c58c308b72aaba7d4ad18c17de232ee/images/geometry/vector-application/inline.png -------------------------------------------------------------------------------- /images/geometry/vector-application/inpoly1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wiwiho/cpnote-source/743b13cc4c58c308b72aaba7d4ad18c17de232ee/images/geometry/vector-application/inpoly1.png -------------------------------------------------------------------------------- /images/geometry/vector-application/inpoly2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wiwiho/cpnote-source/743b13cc4c58c308b72aaba7d4ad18c17de232ee/images/geometry/vector-application/inpoly2.png -------------------------------------------------------------------------------- /images/geometry/vector-application/intersect.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wiwiho/cpnote-source/743b13cc4c58c308b72aaba7d4ad18c17de232ee/images/geometry/vector-application/intersect.png -------------------------------------------------------------------------------- /images/geometry/vector-application/intersection.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wiwiho/cpnote-source/743b13cc4c58c308b72aaba7d4ad18c17de232ee/images/geometry/vector-application/intersection.png -------------------------------------------------------------------------------- /images/geometry/vector/cross.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wiwiho/cpnote-source/743b13cc4c58c308b72aaba7d4ad18c17de232ee/images/geometry/vector/cross.png -------------------------------------------------------------------------------- /images/geometry/vector/cross1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wiwiho/cpnote-source/743b13cc4c58c308b72aaba7d4ad18c17de232ee/images/geometry/vector/cross1.png -------------------------------------------------------------------------------- /images/geometry/vector/cross2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wiwiho/cpnote-source/743b13cc4c58c308b72aaba7d4ad18c17de232ee/images/geometry/vector/cross2.png -------------------------------------------------------------------------------- /images/geometry/vector/dot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wiwiho/cpnote-source/743b13cc4c58c308b72aaba7d4ad18c17de232ee/images/geometry/vector/dot.png -------------------------------------------------------------------------------- /images/geometry/vector/dot1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wiwiho/cpnote-source/743b13cc4c58c308b72aaba7d4ad18c17de232ee/images/geometry/vector/dot1.png -------------------------------------------------------------------------------- /images/geometry/vector/dot2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wiwiho/cpnote-source/743b13cc4c58c308b72aaba7d4ad18c17de232ee/images/geometry/vector/dot2.png -------------------------------------------------------------------------------- /images/geometry/vector/minus.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wiwiho/cpnote-source/743b13cc4c58c308b72aaba7d4ad18c17de232ee/images/geometry/vector/minus.png -------------------------------------------------------------------------------- /images/geometry/vector/multiply.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wiwiho/cpnote-source/743b13cc4c58c308b72aaba7d4ad18c17de232ee/images/geometry/vector/multiply.png -------------------------------------------------------------------------------- /images/geometry/vector/plus.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wiwiho/cpnote-source/743b13cc4c58c308b72aaba7d4ad18c17de232ee/images/geometry/vector/plus.png -------------------------------------------------------------------------------- /images/geometry/vector/vector.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wiwiho/cpnote-source/743b13cc4c58c308b72aaba7d4ad18c17de232ee/images/geometry/vector/vector.png -------------------------------------------------------------------------------- /images/sqrt/sqrt-decomposition/sqrt-decomposition.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wiwiho/cpnote-source/743b13cc4c58c308b72aaba7d4ad18c17de232ee/images/sqrt/sqrt-decomposition/sqrt-decomposition.png -------------------------------------------------------------------------------- /menu.md: -------------------------------------------------------------------------------- 1 | - [我教過的課](/lessons) 2 | - 雜項 3 | - [複雜度](/complexity) 4 | - [前置處理器](/preprocessor) 5 | - [前綴和與差分](/prefix-sum) 6 | - 輸入輸出 7 | - [I/O Stream](/iostream) 8 | - [String Stream](/stringstream) 9 | - [輸入輸出優化](/io-optimize) 10 | - 數字與位元 11 | - [進位制](/radix) 12 | - [整數與浮點數](/int-and-float) 13 | - [位元運算](/bitwise-operator) 14 | - 基礎資料結構 15 | - [類陣列容器](/array-like-ds) 16 | - [Linked List](/linked-list) 17 | - 動態規劃 18 | - DP 優化 19 | - [Aliens 優化](/aliens) 20 | - 數學 21 | - [歐幾里得演算法](/gcd) 22 | - [快速冪](/pow) 23 | - [質數與因數](/prime-and-factor) 24 | - 幾何 25 | - [向量](/vector) 26 | - [向量應用](/vector-application) 27 | - [凸包](/convex-hull) 28 | - 進階資料結構 29 | - [Sparse Table](/sparse-table) 30 | - [線段樹](/segment-tree) 31 | - [樹狀數組](/fenwick-tree) 32 | - 根號 33 | - [根號分塊](/sqrt-decomposition) 34 | - [莫隊算法](/mo-algorithm) 35 | - 字串 36 | - [最長迴文子字串](/longest-palindromic-substring) 37 | 38 | --------------------------------------------------------------------------------