# APCSCAMP week 1 ### 重點:時間複雜度、資料結構、枚舉、二分搜尋法、貪婪演算法(greedy) --- ## 時間複雜度 #### 時間複雜度是一個估程式運行時間的方法,在解一些難題時,某些方法往往會因為時間問題而導致TLE,因此,估出一個好的時間複雜度是我們在優化程式的時候,不可或缺的判斷因素。 ### 定義O(K)其中K為此程式在最壞情況下所運行的時間,且將其寫成一個函式並將最高次項(或影響最大的項次令為K),係數可以省略。 對於每個程式,其中基本操作如:算術運算、比較、賦值等皆為O(1)的操作,大部分時間都會落在迴圈、遞迴、及資料結構的操作上,如單層迴圈的複雜度為O(N),雙層為O($N^2$) 等,若一個程式的運行時間為$3n^2 * 2^n * 2$,其最高項為 3^n ,係數忽略後我們可以將其複雜度寫成O($n^2$ )。此外,一般judge一秒可以跑10^8 筆整數運算,在寫的時候可以稍微估一下時間。 ### 雙指針(two pointer,又名slide windows) 雙指針是一個能將O(n^2)的程式改寫為O(n)的技巧,使用時機為當左指針遞增時,右指針只會遞增或遞減時使用。 --- EX: 給定一串字串,求最短的子字串包含所有小寫英文字母。 如果有多個最短的子字串,請輸出第一次出現的。如果這種子字串不存在,請輸出 QQ。 --- 那這題我們可以發現,當我們定住左指針時,右指針一定會遞增,否則一定不會包含26個字母,因此這題就是一個典型的雙指針題型。 --- ```cpp= #include<bits/stdc++.h> using namespace std; #define potato ios_base::sync_with_stdio(0); cin.tie(0); cout.tie(0) #define pb emplace_back #define mp make_pair #define pii pair<int,int> int arr[26]={0}; int l=0,r=0,cnt;//cnt紀錄目前英文字母相異的個數 int ans[2]={0,1000000}; string n; int main() { cin>>n; //字串長度 cnt=1; arr[n[l]-'a']+=1;//記錄左指針的英文字母 for (l=0;l<n.length();l++) { while(r+1<n.length() && cnt<26) { r++; if(arr[n[r]-'a']+1==1) cnt+=1;//如果未出現過就將cnt+1 arr[n[r]-'a']+=1; } if (cnt<26)//如果跳出while迴圈但cnt仍<26代表r跑到底且l到r不存在26個相異字母 { if (ans[1]-ans[0]+1!=1000001)//如果之前有找到存在26個相異字母的子字串則跳出while迴圈且輸出 break; cout<<"QQ"<<endl;// 否則cout<<QQ並結束程式 return 0; } if(r-l<ans[1]-ans[0])//判斷當前字串的長度有沒有比上一個字串短並更新l跟r的數值 { ans[1]=r; ans[0]=l; } arr[n[l]-'a']-=1;//將目前l所在字母的出現次數-1,如果移除後當前字串不存在26個字母,就將cnt-1 if (arr[n[l]-'a']==0) { cnt-=1; } } # for (int i=ans[0];i<=ans[1];i++)//輸出字串 { cout<<n[i]; } cout<<endl; return 0; } ``` --- ## 資料結構 #### 資料結構可以有效的幫我們存取資料,不同的題目需要不同的資料結構來存取,以下介紹幾個資料結構:stack、queue、deque、priority_queue、set、map等。c++有STL資料庫可以供我們使用這些資料結構,不需要自己手刻。 ### stack stack(堆疊)是一種後進先出的資料結構(LIFO),需要支援以下操作: 1. stk.top()回傳stack中最上面的元素 2. stk.pop()將最上面的元素移除 3. stk.push()將元素移入stack裡 4. stk.size()回傳stack當前的大小 5. stk.empty()回傳一個bool值,如果當前stack為空回傳true,反之回傳false。 --- EX: 給定一個字串,若有任何相鄰的字元相同,就會被消掉。舉例來說,假設一開始的字串是 cabbab: cabbab -> caab -> cb 第一步中中間有兩個 b 相鄰,消掉後又有兩個 a 相鄰,最後剩下 cb。 --- 輸入只有一行S長度不會超過200000,只由小寫英文字母組成。 輸出字串運算後的結果。 --- 只要相鄰兩個就刪除,乍看之下可以O(n^2)爆搜,可是這樣複雜度會爛掉,再仔細看一下,其實就是括號匹配的弱化版! --- 從字串由由左往右掃,每次把元素丟到stack裡,遇到相同的就pop掉 這樣就達到O(n)操作了! --- 以下是程式碼: ```cpp= #include<bits/stdc++.h> using namespace std; #define potato ios_base::sync_with_stdio(0); cin.tie(0); cout.tie(0) #define pb emplace_back #define mp make_pair #define pii pair<int,int> stack<int> stk; string s,l=""; int main() { potato; cin>>s;//輸入字串 stk.push(s[0]); for(int i=1;i<s.length();i++) { if(!stk.empty() && stk.top() == s[i])//如果stack不為空且當目前輸入與stack最上層相同時,把上層元素pop掉。 stk.pop(); else stk.push(s[i]);//否則把目前元素push進去。 } while(!stk.empty()) { l+=stk.top();//將stack字元加入l字串 stk.pop(); } reverse(l.begin(),l.end());//由於stack pop出來的是反過來的,所以要reverse。 cout<<l<<'\n';//輸出並換行 } ``` --- ### queue queue(佇列)是一種先進先出的資料結構(FIFO),需要支援以下操作: 1. que.front()回傳stack中最前面的元素 2. que.pop()將最上面的元素移除 3. que.push()將元素移入queue裡 4. que.size()回傳queue當前的大小 5. que.empty()回傳一個bool值,如果當前queue為空回傳true,反之回傳false。 --- EX: 每次進行:(1)將最上面的一張牌捨棄掉 (2)將現在牌組最上面的一張牌移動至牌組最下方 --- 假設第i次操作的時候捨棄掉的牌的編號為Ai,那草原戰士們想要你回答Q個問題,每次會問一個m,問你Am的值為多少? --- 輸入將有2行。第一行將有兩個正整數N,Q,代表有幾張牌和有幾個問題要回答。第二行有Q個數字,第i個是Mi,代表一次詢問。 請輸出Q行,每一行代表A Mi。 --- 這題主要是將一堆卡牌進行以上的操作之後問第m次操作後所取出的卡牌,由於每次會取出最上面的卡牌並將下一張卡牌塞到下面,因此,我們可以使用queue的資料結構來處理這題。 ```cpp= #include<bits/stdc++.h> using namespace std; #define potato ios_base::sync_with_stdio(0); cin.tie(0); cout.tie(0) #define pb push_back #define mp make_pair #define pii pair<int,int> int n,m,k; queue<int> que; vector<int> vec; int main() { potato; cin>>n>>m; for (int i=1;i<=n;i++) { que.push(i); }//初始化一組有n張的卡牌 for (int i=0;i<n;i++) { vec.pb(que.front());//紀錄第i次操作所取出的卡牌 que.pop(); que.push(que.front());//取出後將下一張卡牌塞進queue的最後面 que.pop(); } for (int i=0;i<m;i++) { cin>>k; cout<<vec[k-1]<<'\n';//由於記錄第i次操作的vector是0 base,而題目的要求是1 base,所以將題目要求-1後輸出並換行 } } } ``` ### deque deque(雙端佇列)是一種結合vector跟stack跟queue特性的一種容器,一個deque可以支援以下操作: 1. 新增、存取、刪除deque後端的資料 2. 新增、存取、刪除deque前端的資料 --- 給定一串長度為n的序列,求i|i<=n-k+1中,所有[i,i+k]的最大值。 其中1<=k<=n<10^6 --- 對於這題,雖然我們可以從0~n-k做一次max_element,但顯然這複雜度是糟的,因為max_element的複雜度為O(K),整體複雜度為O(NK),顯然是糟的。我們重複比較過多次相同的元素,即使他不能是我要的,因此我們需要一個資料結構來維持其"單調性",那因為它是一個區間,我們沒有辦法透過priority_queue(後面會提到)來維護,因此我們可以透過頭尾控制這個區間,當目前的最大值超過我要的區間就pop掉,即為"隊列",即為'單調對列'。 --- ```cpp= #include<bits/stdc++.h> using namespace std; #define potato ios_base::sync_with_stdio(0); cin.tie(0); cout.tie(0) #define pb emplace_back #define mp make_pair #define pii pair<int,int> #define MOD 1000000009 int n,k; int pep,tar,pivot; deque<pii> vec; int main() { potato; cin>>n>>k; for(int i=0;i<n;i++) { if(!vec.empty() && vec.front().second+k<=i) vec.pop_front();//如果當前最大值已經超過目前的範圍就pop掉 cin>>pep; while(!vec.empty() && vec.back().first<pep)vec.pop_back();//維護deque單調性(即使其保持大到小的排序) vec.push_back(mp(pep,i)); if(i>=k-1) cout<<vec.front().first<<' ';//輸出前最大值 } } ``` ### 堆疊(heap、priority_queue) heap又稱priority_queue(簡稱pq),是一種能維護極值的資料結構,支援新增、刪除元素到pq頂端,其中每項操作皆為O(logn),可以自定義比較函數或是使用內建函式達到維護最大值(或最小值)的作用。 通常pq都會跟著greedy演算法使用,因此例題到後面再講www。 ### set、map set(集合),map(映射)皆為唯一性且排序過的資料結構,若不想要其唯一性也可使用multi_set,multi_map等(但multi_map很雞肋就是了)。 #### set 顧名思義,可在裡面存取元素且不重複,支援以下操作: 1. s.insert(value)、s.erase(value) 新增、刪除資料 2. s.find(value) 回傳value所在的iterator,若不存在則回傳s.end() 3. s.count(value) 回傳value的個數,在set只回傳0 or 1 #### map 如同數學上的函數定義,由一個key映照到value上,其中一個key只會映照到一個value上,支援以下操作: 1. mp[key]=value 將key映照到value上 2. mp.erase(value) 同set 3. mp.find(value) mp.count(value)同set 一樣沒有例題(我真的找不到好的例題嗚嗚嗚) ## 枚舉 #### 顧名思義,就是將所有情況一一列舉出來,由於時間複雜度的關係,枚舉往往不是一個好的寫法,但如果透過剪枝、加速等動作,也許可以撈到部分分數或者是AC。 ## 二分搜尋法 #### 一般搜尋有兩種方法:線性搜尋跟二分搜尋。線性搜尋就是很直覺的跑O(n)次找到我們要的目標,但配合其他程式碼,時間複雜度往往會來到O(n^2)甚至更高,因此我們需要一種新的搜尋法──二分搜尋法。 二分搜尋法透過以下操作: 1. 建立中心點 2. 如果比他大往右邊找,反之 3. 以此遞迴下去 透過分治的概念,達成二分搜的想法,完成O(logn)的搜尋演算法。 C++有內建函數 lower_bound(),upper_bound()等函數可以達成二分搜。 --- APCS 1月第三題:切割費用 有一根長度為 L 的棍子,你會把這個棍子切割 n 次。 假設一開始棍子左端放在數線上 0 的位置,棍子的右端放在數線上 L 的位置,每次的切割會給定一個介於 0 到 L 的數字表示要切個的位置,你要把穿過個這位置的棍子切成兩段,而所需的花費就等於所切割的棍子的長度。 第一行有兩個整數 n,L。 接下來 n 行每行有兩個整數 x,i,表示 x 位置被切過一刀,而這刀是全部的切割中的第 i 刀,保證 i 是介於 [1,n] 的整數且不會重複。 輸出一個整數表示總共的切割費用,答案可能超過 2^31 但不會超過2^60。 --- 想法是透過set存取每一刀的位置,並二分搜上一刀的位置找出這一到所位於的線段,再將答案存起來 ```cpp= #include<bits/stdc++.h> using namespace std; #define potato ios_base::sync_with_stdio(0); cin.tie(0); cout.tie(0) #define pb emplace_back #define mp make_pair #define pii pair<int,int> set<int> s; vector<pii> vec; int n,len; int x,y; int r,l,ans,key; int main() { potato; cin>>n>>len; for(int i=0;i<n;i++) { cin>>y>>x;//存取每一刀的位置 vec.pb(x,y); } sort(vec.begin(),vec.end());//依刀的順序排序 for(int i=0;i<n;i++) { key=vec[i].second; auto it = s.lower_bound(key);//找到上一刀的位置 if(it != s.end()) r = *it;//如果有找到就將右界設在這一刀 else r = len;//否則右界設在末端 if(it != s.begin()) l = *--it;//如果有找到就將左界設在這一刀前面的位置 else l = 0;//否則設在最前端 ans += r - l ;//兩邊相減求線段長度 s.insert(key);//把現在這刀存起來 } cout<<ans; } ```