<style> .reveal .slides { text-align: left; font-size:36px; } .cpp { font-size: 32px !important; line-height: 36px !important; } </style> # pretrain 1 2023 Introduction to Competitive Programming 2022/12/01 ---- ## 自我介紹 李欣祐 LeeShoW <div style="font-size: 30px"> * APCS 5/5 * CPE 七題 * 校內海洋盃第二 * 2022北區大專初級組第一名 </div> ---- ## 自我介紹 賴柏勛 可撥呆呆獸 <div style="font-size: 30px"> * 2022 ICPC 國際大學生程式設計競賽 臺北新竹賽區 銀牌 * CPE 七題 * 校內海洋盃第一 </div> ---- ## 上課模式 <div style="font-size: 30px"> 第一節課上課,大概介紹一下之後上課的模式 ### 課堂上 * 講解演算法 * 實作練習 ### 課後 * 回家練習 </div> ---- <div style="font-size: 30px"> 沒意外這學期固定星期四,地點 212 教室 因為借不到電腦教室所以沒筆電的回去再練習 </div> ---- ## 今天要教的內容 * 時間複雜度 * 質數判斷 * 快速冪 --- # 時間複雜度 - 與**輸入大小**有關的函數 - 用來估計程式執行大約的時間 - 用程式執行次數來去估算執行時間 - 通常以大 $O$ 符號表式,ex: $O(N)$、$O(NlgM)、O(k^3)$ ---- ### 計算大概的執行次數 執行次數無法完全精準的計算 可能受到編譯器優化影響 使得執行次數不是我們所計算的 而只需要計算大概的次數就可以去估算時間 ---- ### 表達時間複雜度 - 以大$O$符號表示,其中大$O$符號代表"上限"的意思 - 以$T(N)$代表時間複雜度函數($N$是輸入大小) - 舉例:$T(N)=O(N^2), T(N,Q)=O(Q\times2^N),\\T(S,w)=O(Slgw), T(N)=O(1)$ ![](https://i.imgur.com/brR3xmg.png) ---- ### 計算時間複雜度 1. 估計程式的運算執行次數 2. 將得到的函數最高階項以外的項全部刪掉 3. 把係數拿掉 ---- ### 迴圈的時間複雜度 迴圈的執行次數\*每次迴圈的時間複雜度 ---- ```cpp for(int i=0;i<n;i++) { arr[i]=arr[i-1]*a+b; //O(1) swap(a,b); //O(1) } ``` $O(n)$ ---- ```cpp for(int i=0;i<n;i++) { for(int j=i+1;j<n;j++) { if(arr[i]>arr[j]) swap(arr[i],arr[j]); //O(1) } } ``` 執行次數大約是$(n-1)+(n-2)+...+1+0=\frac{1}{2}n(n-1)=\frac{1}{2}n^2+\frac{1}{2}n$ 保留最高階項,省略常數 因此時間複雜度是$O(n^2)$ ---- ```cpp for(int i=1;i<n;i*=2) s+=i; ``` $2^i\lt n\Rightarrow i\lt lgn$ 因此是$O(lgn)$ 在算複雜度時,通常 $lg$、$log$ 代表以 $2$ 為底取 $log$ ---- ### 遞迴函數的時間複雜度 - 簡單的你們應該會算(?) - 比較難的等教分治法(Divide&Conquer)時再說 - 很複雜的我也不會 ---- ### 內建函數的時間複雜度 其實從函數的功能應該可以猜出其複雜度 不然的話就把它記起來(至少常用的函數要知道) 例:memset O(N)、 sort O(NlgN)、 __gcd O(lgN)、lower_bound O(lgN) ... ---- ### 練習 1. ```cpp for(int i=100000;i<n;i++) { for(int j=i-100;j>=1000;j--) cout<<i<<" "<<j<<'\n'; } ``` ---- 1. ```cpp for(int i=10;i<n;i++) { for(int j=i-100;j>=1000;j--) cout<<i<<" "<<j<<'\n'; } ``` $O(n^2)$ ---- 2. ```cpp for(int i=0;i<100000000;i++) cin>>n,cout<<n*n<<'\n'; ``` ---- 2. ```cpp for(int i=0;i<100000000;i++) cin>>n,cout<<n*n<<'\n'; ``` $O(1)$ ---- 3. ```cpp for(int i=1;i<=n;i++){ sort(arr,arr+i); } ``` ---- 3. ```cpp for(int i=1;i<=n;i++){ sort(arr,arr+i); } ``` $O(n^2lgn)$ $(\sum\limits_{k=1}^{n}klgk\approx n^2lgn)$ ---- 4. ```cpp for(int i=1;i<n;i=i*2){ for(int j=i;j<n;j+=i) arr[j]+=arr[j-i]; } ``` ---- 4. ```cpp for(int i=1;i<n;i=i*2){ for(int j=i;j<n;j+=i) arr[j]+=arr[j-i]; } ``` $O(n)$ $(n+\frac{n}{2}+\frac{n}{4}+...\approx 2n)$ ---- 5. ```cpp void f(int n) { if(n==0) return 1; return f(n-1)*n; } ``` ---- 5. ```cpp void f(int n) { if(n==0) return 1; return f(n-1)*n; } ``` $O(n)$ ---- 6. ```cpp void f(int i){ if(i==n) return; v[i]=0,f(i+1);v[i]=1,f(i+1);v[i]=2,f(i+1); } ``` ---- 6. ```cpp void f(int i){ if(i==n) return; v[i]=0,f(i+1);v[i]=1,f(i+1);v[i]=2,f(i+1); } ``` $O(3^n)$ --- ## 為什麼要學時間複雜度 - **判斷一個程式(做法)會不會TLE** - 可以用側資的範圍去反推演算法 ---- 1. 把時間複雜度的大$O$符號拿掉,並將題目給定的輸入範圍**上限**帶入函數,令得到的數值為 $T$ 2. 一般而言假設c++每秒能跑 $10^8$ 的數量級 3. 假設題目的時限是TL秒,那麼 - 若$T\lt TL\times 10^8$則通常不會TLE - 若$T\gt TL\times 5\times 10^8$則極有可能拿到TLE ---- ### 時間複雜度的好處 - 當你想到一個做法後,在開始寫之前就能先用時間複雜度來判斷是否會TLE,以避免浪費時間在寫必然會TLE的程式 - 在比賽中更容易作時間分配、難度分析 --- # 判斷質數 ---- <div style="font-size: 30px"> $Q$ 筆詢問 每筆詢問給你一個數字 $N$,判斷是不是質數? 質數: 除了 $1$ 跟 $N$ 都不是 $N$ 的因數 TimeLimit : 1 second * subtask 1: $Q=1, N=10^6$ * subtask 2: $Q=1, N=10^{12}$ * subtask 3: $Q=10^6, N=10^{6}$ * ~~subtask 4: $Q=10^5, N=10^{18}$~~ </div> ---- ### subtask 1: $Q=1, N=10^6$ <div style="font-size: 30px"> 窮舉每個小於 $N$ 的數字,判斷有沒有整除 $N$ 整除代表是 $N$ 的因數 7 -> 2, 3, 4, 5, 6 無其他因數 25 -> 2, 3, 4, 5 為因數 120 -> 2 為因數 最差情況 : 判斷的數字是質數,因此要跑 $N-2$ 個數字 (除了 $1$ 跟 $N$ ) </div> ---- ### subtask 1: $Q=1, N=10^6$ <div style="font-size: 30px"> 可以再檢查更少次? 最差情況跑到 $N/2$ 就好 在小於 $N$ 的數字中,如果數字 $x$ 大於 N/2 則不可能是 $N$ 的因數 所以只需要檢查區間 $[2,N/2]$ 的數字就好 </div> ---- ### subtask 2: $Q=1, N=10^{12}$ <div style="font-size: 30px"> 如果用剛剛說的方法做,最差情況要跑到 $N/2$ 但 1 秒跑得完嗎? </div> ---- ### subtask 2: $Q=1, N=10^{12}$ <div style="font-size: 30px"> 1 秒電腦可以跑的運算量約為 $5 \cdot 10^8$ 如果用前面說的方法 運算量最差約為 $\frac{10^{12}}{2} = 5 \cdot 10^{11}$ $5 \cdot 10^{11} > 5 \cdot 10^8 \to$ TLE ( Time Limit Exceed ) 還不夠快 QQ </div> ---- ### subtask 2: $Q=1, N=10^{12}$ <div style="font-size: 30px"> 先來觀察以下數字 $12 = 1 \times 12 = 2 \times 6 = 3 \times 4 = 4 \times 3 = 6 \times 2 = 12 \times 1$ $9 = 1 \times 9 = 3 \times 3 = 9 \times 1$ $7 = 1 \times 7 = 7 \times 1$ 如果一個數字 $x$ 是 $N$ 的 因數則 $x$ 或者 $N/x$ 必定有一個 $\le \sqrt{N}$ </div> ---- ### subtask 2: $Q=1, N=10^{12}$ <div style="font-size: 30px"> 因此我們只需要跑到 $\sqrt{N}$ 就好 如果在$\le \sqrt{N}$ 的範圍沒檢查到因數 則後面不會有 $N$ 的因數 $\sqrt{10^{12}} = 10^6 < 5 \cdot 10^8(1秒)$ </div> ---- ### subtask 3: $Q=10^6, N=10^{6}$ <div style="font-size: 30px"> 用剛剛的方法? 每筆詢問最差跑 $10^3$ 的運算量 有 $10^6$ 筆詢問 $Q \cdot \sqrt{N} = 10^{9} > 5 \cdot 10^8$ ( 1秒 ) Time Limit Exceed </div> ---- ## Sieve of Eratosthenes ## 質數篩法 ---- ## Sieve of Eratosthenes <div style="font-size: 30px"> 建立質數表 想法: 最小的質數開始,所有質數的倍數都一定不是質數 因此我們就先用一個陣列,儲存每個數字是不是質數 一開始先把所有大於1的數字當成質數 再從2依序把所有質數的倍數設成非質數 </div> ---- ## Sieve of Eratosthenes ![](https://i.imgur.com/wZuoyk4.gif) ---- <div style="font-size: 30px"> ## 演算法步驟 從 2 開始跑到 N 判斷當前數字是不是質數 如果是質數把所有質數的倍數設成非質數 </div> ---- ## 核心程式碼 ```cpp= bool isprime[1000005]; int primeSize = 0, prime[200005]; //先將所有大於1數字設為質數 for(int i=2;i<=n;i++) isprime[i] = 1; for(int i=2;i<=n;i++){ if(isprime[i]){ //如果為質數 prime[primeSize++] = i; for(long long j=2;i*j<=n;j++){ //所有質數的倍數設成非質數 isprime[i*j] = 0; } } } ``` ---- ## 執行次數 <div style="font-size: 30px"> 將所有數字設成 質數 ```cpp= for(int i=2;i<=n;i++) isprime[i] = 1; ``` 約n次 ```cpp= for(int i=2;i<=n;i++){ if(isprime[i]){ prime[primeSize++] = i; for(long long j=2;i*j<=n;j++){ isprime[i*j] = 0; } } } ``` 次數為 $\frac{n}{2} + \frac{n}{3} + ... + \frac{n}{n}$ 為調和級數 $= n lg n$ </div> ---- ## 優化 <div style="font-size: 30px"> 在內迴圈裡的倍數可以直接從 i 倍開始 因為小於 i 的倍數之前都跑過了 ```cpp= for(int i=2;i<=n;i++){ if(isprime[i]){ prime[primeSize++] = i; for(long long j=i; i*j<=n; j++){ isprime[i*j] = 0; } } } ``` 複雜度為 $O(NloglogN)$ ---- <div style="font-size: 30px"> 回到原本問題 * subtask 3: $Q=10^6, N=10^{6}$ 如果我們建好質數表,每次只要查質數表 就可以直接判斷是不是質數 執行次數約為 $Q \cdot 1 = 10^6 < 5 \cdot 10^8$ </div> ---- ### ~~subtask 4: $Q=10^5, N=10^{18}$~~ miller-rabin 有興趣再自己去看 --- ## 補充 因數的數量最多只有 $n^{\frac{1}{3}}$ 個 --- # 快速冪 ---- <div style="font-size: 30px"> 先把 y 換成二進位 以 $2^{13}$ 為例 $13$ $= 1101$ $= 1\cdot 2^3 + 1\cdot 2^2 + 0\cdot 2^1 + 1\cdot 2^0$ $= 8 + 4 + 1$ $\to 2^{13} = 2^8 * 2^4 * 2^1$ </div> ---- 因此,我們只要求出 $2^8, 2^4, 2^1$ 就好 也就是 $lgN$ 次 ---- ## 實作 <div style="font-size: 30px"> 一樣以2^13為例 1101 就從最右邊的開始做 判斷最右邊的bit是不是1如果是1則把答案乘上去 並且每次把 x 平方 接著把整個右移一個,變成 110 因此一樣變成判斷最右邊的bit即可 $再右移\to 11$ $再右移\to 1$ 一直做到 y 右移到 0 為止 </div> ---- ## 過程 <div style="font-size: 25px"> | x | y | 原本答案 | 計算後答案 | | -------- | -------- | -------- | -------- | | $2^1$ | 1101 | $2^0$ | $2^1$ | | $2^2$ | 110 | $2^1$ | $2^1$ | | $2^4$ | 11 | $2^1$ | $2^5$ | | $2^8$ | 1 | $2^5$ | $2^{13}$ | </div> ---- ## 程式碼 ```cpp= long long mypow(long long x,long long y){ long long ans = 1; while(y){ if( y&1 ) ans = ans * x % p; x = x * x % p; //每次把自己平方 y >>= 1; //每次右移一格 } return ans; } ``` ---- 換成2進位,只需要計算 $lgN$ 次 $lg10^{18} = 60 < 5 \cdot 10^8$ --- ## 練習時間 :) 問啥都行 題單:https://vjudge.net/group/ntou_cse107?r=wG5DBzKmcopmEWn7sqQn ![](https://i.imgur.com/LMtJ6Zq.png)
{"metaMigratedAt":"2023-06-17T15:00:20.976Z","metaMigratedFrom":"YAML","title":"pretrain 1","breaks":true,"contributors":"[{\"id\":\"5df14ba0-4dd2-4172-b309-8b5af9ede7a1\",\"add\":8007,\"del\":382},{\"id\":\"e4a3af8b-860c-46a0-96f1-558667fdcf41\",\"add\":1021,\"del\":470}]"}
    1202 views
   Owned this note