<style> .reveal .slides { text-align: left; font-size:30px; } </style> # pretrain 2 2023 Introduction to Competitive Programming 2022/12/08 ---- * 矩陣快速冪 * 位元運算 * 前綴和與差分 --- ## 矩陣快速冪 ---- ### 矩陣快速冪 與快速冪類似,差別只有快速冪是對數字,而矩陣快速冪是對矩陣做。 ---- ### 例題: 費式數列 $F_0 = 0, F_1 = 1$ $F_n = F_{n-1} + F_{n-2}$ * subtask 1: $n \le 20$ * subtask 2: $n \le 10^5$ * subtask 3: $n \le 10^{18}$ ---- ### subtask 1:$n \le 20$ trivial 直接遞迴即可 ---- 程式碼 ```cpp= int fib(int n) { if(n <= 1) return n; return fib(n-1) + fib(n-2); } ``` ---- 分析: 藉由下圖可以發現,fib 產生出的遞迴樹的節點會比完全二元樹(*)還少,花費的時間會小於 $O(2^n)$ 在$n \le 20$的情況下,時間複雜度會是好的 ![](https://i.imgur.com/EKjypzE.jpg =600x) ---- ### subtask 2:$n \le 10^5$ dp 可以發現直接遞迴的做法會算到很多次重複的值,因此我們可以通過將值記錄下來,之後跑到就不需要重新計算了。 ---- 程式碼 ```cpp= int dp[] = {0}; int fib(int n) { if(n <= 1) return n; if(dp[i]) { return dp[i]; } return dp[n] = fib(n-1) + fib(n-2); } ``` ---- 分析: 可以發現上面每個 dp[i] 都只會計算到一次 複雜度 $O(n)$ ---- ### subtask 3:$n \le 10^{18}$ 矩陣快速冪 將 $F_n = F_{n-1} + F_{n-2}$ 的計算推導成矩陣形式 ![](https://i.imgur.com/Wg1PgdA.jpg =700x) ---- 套用上一張投影片的結論,我們可以發現,如果可以將左邊的矩陣多乘幾次,再乘上右邊 2 * 1 的矩陣,就可以找出我們要的答案了! ${\begin{bmatrix} 1&1\\1&0 \end{bmatrix}}^{n-1}\quad \times \quad \begin{bmatrix} f[1]\\f[0] \end{bmatrix}\quad = \quad\begin{bmatrix} f[n]\\f[n-1] \end{bmatrix}\quad$ 大概是像這樣 ---- ### 程式碼: ```cpp= int base[2][2] = { int ans[2][2] = { {1, 1}, {1, 0}, {1, 0} {0, 1} }; }; int mypow(int y){ while(y){ if( y&1 ) { ans = mul(ans, base); } //實作矩陣乘法 base = mul(base, base);//實作矩陣乘法 y >>= 1; } return ans[0][0]; } ``` ---- 矩陣乘法程式碼: ```cpp= for(int i = 0; i < n; i++){ for(int j = 0; j < n; j++){ for(int k = 0; k < n; k++){ ret[i][j] += a[i][k] * b[k][j]; } } } ``` ---- 分析: 快速冪 $O(lgN)$ 矩陣乘法 $O(矩陣大小^3)$ 複雜度 $O(2^3 lg N)$ ---- 例題: 環塗色 題序: 給兩個整數 n、$\lambda$, $\lambda$ 是有多少種顏色,n 是指有幾個頂點圍成的環,問你有幾種塗色方式可以讓環上連在一起的頂點兩兩顏色都不同 範圍:n、$\lambda \le 10^9$ ![](https://i.imgur.com/USW3Dzj.png) n = 3, $\lambda$ = 3 以上為 6 種塗色方法 (1,2,3) (1,3,2) (2,1,3) (2,3,1) (3,1,2) (3,2,1) <!-- ![](https://i.imgur.com/xcppgM6.png =700x) --> <!-- https://icpc2022.ntub.edu.tw/wp-content/uploads/2022/11/ICPC2022-main.pdf --> ---- 可以發現如果直接推的話,會長左下這樣,但是因為這樣很明顯無法知道最後一個的顏色是什麼,因此這個推法是錯的。 ![](https://i.imgur.com/2EJ2fTf.png =500x) ---- 如果要正確推出多邊形有幾種塗色的方式,我們需要知道最後一個跟第一個不一樣的塗法有幾種,因此我們需要記錄每個節點跟第一個一樣及跟第一個不一樣的圖法 color[i][0] 是指第 i 個節點與第一個不一樣顏色的話有幾種塗法 color[i][1] 是指第 i 個節點與第一個一樣顏色的話有幾種塗法 ---- 轉移就會變成以下這樣 color[i][0] = color[i - 1][0] * ($\lambda$ - 2) + color[i - 1][1] * ($\lambda$ - 1) color[i][1] = color[i - 1][0] 而初始值是 color[0][0] = 0 color[0][1] = $\lambda$ (在第一個的時候顏色會跟自己一樣,因此是 $\lambda$) ---- 這樣就做完了嗎? No,他的 k 的範圍最多到 $10^9$,除了沒辦法開那麼大的陣列以外,時間也不足以跑完 $10^9$,這時候就需要矩陣快速冪加速了。 ---- 推導過程: ${\begin{bmatrix} \lambda - 2 &\lambda - 1\\1&0 \end{bmatrix}}\quad \times \quad \begin{bmatrix} \text{color[0][0]}\\\text{color[0][1]}\end{bmatrix}$ <!-- ![](https://i.imgur.com/GzzulEF.jpg) --> ---- 通過以上的推導,我們就可以對求出的矩陣做快速冪,在 $O(log_2N)$ 的時間找出我們要的多邊形塗色的答案了,也就是 color[n - 1][0] (第 n 個跟第一個顏色不同的塗法)。 --- ## 位元運算 ---- ### 位元運算 位元運算是對二進位的 0 和 1 做一些處理,然後會出現一些性質和用途。 ---- 二進位理所當然就只有 true(1) 跟 false(0),以下是位元運算示範操作 ``` AND運算: OR運算: 0 & 0 0 0 | 0 0 0 & 1 0 0 | 1 1 1 & 0 0 1 | 0 1 1 & 1 1 1 | 1 1 XOR運算(⊕): NOT運算: 0 ^ 0 0 !0 1 0 ^ 1 1 1 ^ 0 1 !1 0 1 ^ 1 0 ``` ---- ### XOR運算 因為 XOR 太多常用性質,所以特別拿出來講。 先來幾個基本原則(bitwise xor 的符號一樣用 ⊕): A ⊕ B = B ⊕ A,(A ⊕ B) ⊕ C = A ⊕ (B ⊕ C),也就是運算順序不影響結果(有交換律和結合律)。 A ⊕ A=0 若 A ⊕ B=C,則 A ⊕ C=B A ⊕ 0 = A ---- ### 補數運算 C++程式裡使用 "~" 來表示,補數運算為逐位元取反 顛倒一個變數的每個位元的 0 和 1 ```= ~0=1 ~1=0 常見用法(位元優化dp 總有一天會遇到) 15 & ~(1 << 1) = 13 //將第2個bit設為0 8 |(1 << 2) = 12 //將第3個bit設為1 ``` ---- ### 左移 右移 運算 C++程式裡使用"<<"與">>"來表示,表示為位元左移(右移)一格 左移時最左邊會補上 0 而右移時最右邊位元會被移除 ```= 9=1001 9<<1 = 10010 = 18 15=1111 15>>1 = 111 = 7 常見用法 (x>>1) 快速除2 以及 (x<<1) 和 (x<<1)|1 左子節點和右子節點 ``` ---- ### 位元性質 if (status) while (status) for ( ; status ; ) 以上的 status 都是非 0 就是 true,也就是裡面都是布林值 所以可以常常看到這種寫法 ```cpp= if(array[x]){ //若array[x]的值非零即會進入"True" cout<<"True"; } else{ //只有再array[x]==0的時候才會進入這邊 cout<<"False"; } while(x){ //當x不是0的時候就不會跳出迴圈 x--; cout<<x<<endl; } ``` ---- 規律性質 例題: [[CPE例題](https://cpe.cse.nsysu.edu.tw/cpe/file/attendance/problemPdf/12208.pdf)] 詢問 x~y 中所有的數字轉換成二進制後有幾個 1 而當你把位元攤開來之後 0 : 0 0 0 0 1 : 0 0 0 1 2 : 0 0 1 0 3 : 0 0 1 1 4 : 0 1 0 0 5 : 0 1 0 1 6 : 0 1 1 0 7 : 0 1 1 1 8 : 1 0 0 0 || <span class="spoiler-text"> 每個bit拆開來即可發現規律 </span> || ---- 1 最低的'1'位元 ```cpp= x & -x ``` 根據二的補數,-x 為 ~x + 1。因此在這先將 -x 拆開為 ~x + 1 來討論。 x 中最低位的位元 1 經過 not 運算後,將在 ~x 中變為最低位的位元 0。 因此對 ~x 加上 1 後,最低位的位元 0 重新變為位元 1,且其後的位元都將被設定為 0。 2 檢查冪次 是否為二的冪次 ```cpp= (x & -x) == x ``` 由於若你為二的冪次,則該數字只會有一個位元(也是最高位元) ---- ## 練習 練習 1 --- XOR 練習 ```cpp= x^=y^=x^=y ``` 請問 x,y 各為多少?(用 x 跟 y 表示) ---- 拆開來看即為 ```cpp= x = x ^ y; y = y ^ x; x = x ^ y; ``` 答案為 x 為 y ,y 為 x (相等於 swap ( x , y )) ---- 練習 2 --- 優先級的練習 ```cpp= cout<<(15>>1+2); ``` ---- ```cpp= cout<< ( 15 >> 1 + 2 ); ``` ans : 1 大多數的位元運算子的運算優先級都比較低, 因此此題的計算過程為 1 + 2 = 3 , 15(1111) >> 3 = 1. 所以你不確定優先級的時候最好都要加括號 ---- 練習 3 --- 常用的技巧 ```cpp= if(x & 1) return 1; else return 0; ``` ```cpp= return (x & 1); ``` ---- ```cpp= if(x&1) return 1;//(奇數) else return 0;//(偶數) ``` 判斷 x 的奇偶性 也是常見用法 (比較快) 由於一個數字的位元組成都是 2 的冪次方 因此 $2^n$ 只有在 n==1 的時候會影響到數字的奇偶性, 故只需要判斷最後一個位元 --- ## 前綴和、差分 #### 一維前綴和 序列 $A_1, A_2,...,A_n$ 的長度為i的前綴和記為 $S_i$ 而 $S_0$ =0,可以用來計算區間和。 $\sum\limits_{i = l}^r {A_i = S_r -S_{l-1}}$ 可以簡單理解為前 $i$ 項的和,而求 $l ~ r$ 的區間和自然就是$S_r-S_{l-1}$ ---- #### 二維前綴和 多維前綴和的構造方式與一維相同,主要是求區間和時要利用容斥原理 如何構造 矩陣 sum? $sum_{x,y}=\sum\limits_{i = 1}^x \sum\limits_{j = 1}^y {arr_{i,j}}$ 遞推求sum的過程為(容斥原理): $sum_{i,j} = sum_{i-1,j} + sum_{i,j-1} - sum_{i-1,j-1} + arr_{i,j}$ ---- ### 如何應用 求子矩陣 ($x_1 , y_1$)~($x_2 , y_2$) 的和。 由遞推的算式可得 ($x_1 , y_1$)~($x_2 , y_2$) 的區間和為: $ans = sum_{x2,y2} - sum_{x1-1,y2} - sum_{x2,y1-1} + sum_{x1-1,y1-1}$ ---- 圖例: $sum_{x2,y2} - sum_{x1-1,y2} - sum_{x2,y1-1} + sum_{x1-1,y1-1}$ ![](https://i.imgur.com/d5S6g5j.png =700x) ---- ### 例題: 對一段長度為 $n$ 的區間,做出 $q$ 筆詢問 * subtask 1: $n*q \le 10^8$ * subtask 2: $n \le 10^2,\ q \le 10^8$ * subtask 3: $n \le 10^6,\ q \le 10^8$ ---- ### subtask 1: $n*q \le 10^8$ 每次詢問就遍例一次,直接暴力加法 ```cpp= for(int i=1;i<=q;i++){ int l,r,ans=0; cin>>l>>r; for(int j=l;j<=r;j++){ ans+=arr[j]; } cout<<ans<<"\n"; } ``` 最差情況 : $q$ 次詢問都是求 $sum_{1,n}$ 因此你每次都需要遍例 $1~n$,則時間複雜度為 $O(q\cdot n)$ ---- ### subtask 2: $n \le 10^2,\ q \le 10^8$ 開 $10^2*10^2$ 的陣列,sum[i][j]紀錄 $i \sim j$ 的總和是多少 $q$ 筆詢問即可 $O(1)$ 解決掉 因此時間複雜度為 $O(n^2+q)$ ---- ### subtask 3: $n \le 10^6,\ q \le 10^8$ 開 $10^6$ 的陣列,sum[i] 紀錄 i 的前綴和是多少 q 筆詢問即可 $O(1)$ 解決掉 因此時間複雜度為O(n + q) ```cpp= int n,l,r,q; int arr[N]={},sum[N]={}; cin>>n; for(int i=1;i<=n;i++){ cin>>arr[i]; sum[i]=sum[i-1]+arr[i]; } cin>>q; for(int i=1;i<=q;i++){ cin>>l>>r; cout<<sum[r]-sum[l-1]<<"\n"; } ``` --- ## 差分 ---- ### 差分的構造 差分是一種和前綴和相對的策略,可以當做是求和的逆運算。 $f_{i} = arr_{i} - arr_{i-1}$ , $f_{1} = arr_{1}$ ```cpp= ex: arr[] = 1 10 5 -3 9 f[] = 1 9 -5 -8 12 ``` 同時不難發現,$arr_{i} 的值是 f_{i} 的前綴和$ ---- ## 計算區間和 若你要利用f計算arr的前綴和 $sum_{1,n} = \sum\limits_{i = 1}^n \sum\limits_{j = 1}^i {f_j}=\sum\limits_{i = 1}^n (n-i+1)*f_{i}$ ## 以及修改區間值 若你要再$arr_l$ ~ $arr_r$ 都加上 x 只需要修改 $f_l$ 和 $f_{r+1}$ 的值 ```cpp= f[l] += x; f[r+1] -= x; ``` ---- ### 例題: 對一段長度為 $n$ 的區間,提出 $k$ 次區間修改後詢問你一次結果 * subtask 1: $n*k \le 10^8$ * subtask 2: $n \le 10^6,k \le 10^8$ ---- ### subtask 1: $n*k \le 10^8$ 一樣直接暴力做,這邊就不贅述 ---- ### subtask 2: $n \le 10^6,\ k \le 10^8$ ```cpp= int n,l,r,k,x; int arr[N]={},f[N]={}; cin>>n; for(int i=1;i<=n;i++){ cin>>arr[i]; f[i]=arr[i]-arr[i-1]; } cin>>k; for(int i=1;i<=k;i++){ cin>>l>>r>>x; f[r+1]-=x,f[l]+=x; } ``` --- 今天教的三個內容都是關於優化,都很常用,回家請好好練習 :p ---- # 練習時間 :) 問啥都行 題單:https://vjudge.net/contest/532352 回饋表單(想檢討上週題目可以進來反應,全匿名) https://forms.gle/7ZY16mxu6BzaFsHaA ![](https://i.imgur.com/D030GBX.png)
{"metaMigratedAt":"2023-06-17T15:10:38.926Z","metaMigratedFrom":"YAML","title":"pretrain 2","breaks":true,"contributors":"[{\"id\":\"5df14ba0-4dd2-4172-b309-8b5af9ede7a1\",\"add\":3393,\"del\":462},{\"id\":\"e4a3af8b-860c-46a0-96f1-558667fdcf41\",\"add\":5832,\"del\":625},{\"id\":\"19f09ccf-6b99-452f-971f-955cfc1657f3\",\"add\":875,\"del\":424}]"}
    941 views
   Owned this note