# SCIST S4 演算法課程_預處理&排序 :::info 時間:03/24 9:00 ~ 18:00 主題:預處理&排序 直播連結:https://www.youtube.com/watch?v=kUQkSaIK000 ::: ## 課程內容 - 預處裡 - 質數篩 - 前綴和 - 排序 - 講義連結:https://hackmd.io/@sa072686/cp/%2F%40sa072686%2FrJpY0i7PS # 題解 [TOC] # 預處理 ## [AtCoder typical90_d](https://hackmd.io/@sa072686/AtCoder_typical90_d) 撰寫者:百鬼真香 解法:預處理每個直排和橫排的和 $S_i, S_j$,在計算答案 $B_{i, j}$ 時就可以直接用 $S_i + S_j - A_{i, j}$ 了。 :::spoiler 參考程式 ```cpp= #include <bits/stdc++.h> using namespace std; #define ll long long #define N 2005 int a[N][N]; int main() { ios::sync_with_stdio(0); cin.tie(0); int h, w; cin >> h >> w; for(int i=0; i<h; i++) for(int j=0; j<w; j++) cin >> a[i][j]; int si[N], sj[N]; for(int i=0; i<h; i++){ int sum = 0; for(int j=0; j<w; j++) sum += a[i][j]; si[i] = sum; } for(int j=0; j<w; j++){ int sum = 0; for(int i=0; i<h; i++) sum += a[i][j]; sj[j] = sum; } for(int i=0; i<h; i++){ for(int j=0; j<w; j++) cout << si[i] + sj[j] - a[i][j] << " "; cout << "\n"; } return 0; } ``` ::: ## [AtCoder typical90_at](https://hackmd.io/@sa072686/AtCoder_typical90_at) 撰寫者:fishhh 因為想要求的是 $A_i+B_i+C_i$ 是否為 $46$ 的倍數。 根據模的性質,$A_i+B_i+C_i$ 等價於 $(A_i \% 46)+(B_i \% 46)+(C_i \% 46)$ 我們就可以知道,$46、46 \times2、46\times3....$ 之類的數字在這一題裡面可以看成是一樣的。 同樣的,$46+i、46 \times 2 +i、46\times 3 +i.....$ 也可以視為相同。 所以數列中的數只有 46 種($0 \sim 45$),根據一點點枚舉的想法,我們可以透過枚舉數列 $A$ 裡面拿出餘數為 $x$ 的數,再枚舉數列 $B$ 內餘數為 $y$ 的數。 這時候我們就可以知道,從數列 $C$ 裡面拿出來的數字 $z$,應該要滿足 $x+y+z$ 是 $46$ 的倍數 稍微透過數學算一下,$z = 46 - (x+y)\%46$,就可以知道我們只要要從數列 $C$ 中,拿出餘數為 $z$ 的數就可以滿足題目要的。 :::spoiler ```cpp= int a[100010],b[100010],c[100010]; int cnt_a[50],cnt_b[50],cnt_c[50]; void solve(){ int n; cin>>n; for(int i=1;i<=n;i++)cin>>a[i]; for(int i=1;i<=n;i++)cin>>b[i]; for(int i=1;i<=n;i++)cin>>c[i]; for(int i=1;i<=n;i++){ cnt_a[a[i]%46]++; cnt_b[b[i]%46]++; cnt_c[c[i]%46]++; } int ans = 0; for(int x=0;x<46;x++){ for(int y = 0;y<46;y++){ int z = (46-(x+y)%46)%46; ans += cnt_a[x]*cnt_b[y]*cnt_c[z]; } } cout<<ans<<"\n"; } ``` ::: ## [TOJ 120](https://toj.tfcis.org/oj/pro/120/) 撰寫者:百鬼真香 解法:題目要求多個區間和,所以可以用前綴和。 注意: 1.因為前綴和需要把所有數加起來,所以經常需要 long long 2.計算區間和需要保證是由後面減前面,否則會出錯 :::spoiler 參考程式 ```cpp= #include <bits/stdc++.h> using namespace std; #define ll long long #define N 200005 int main() { ios::sync_with_stdio(0); cin.tie(0); int n; cin >> n; ll a[N]; for (int i = 0; i < n; i++) cin >> a[i]; ll pre_sum[N] = {0}; for (int i = 0; i < n; i++) pre_sum[i + 1] = pre_sum[i] + a[i]; int q; cin >> q; while(q--){ int a, b; cin >> a >> b; if(a > b) swap(a, b); cout << pre_sum[b] - pre_sum[a - 1] << '\n'; } return 0; } ``` ::: ## [AtCoder abc048_b](https://atcoder.jp/contests/abc048/tasks/abc048_b) 撰寫者:fishhh 題目要問的是,在 $[a \sim b]$ 之間有幾個數可以被 $x$ 整除。 我們可以知道一件事情,在一個連續的自然數數列中,可以被 $x$ 整除的數字的間隔會是 $x$。 也就是,當目前這個數模 $x$ 是 $0$ 那麼下一個出現可以被 $x$ 整除的數字會是 $x$ 個後,且一直保持這個規則。 前面看不懂沒關係(x 我們只需要這樣做,找到第一個 $\geq a$ 的 $x$ 的倍數,在找到第一個 $\leq b$ 的 $x$ 的倍數。 然後計算說中間的間距並且除以 $x$ 就可以了! :::spoiler 參考程式 ```cpp= void solve(){ int a,b,x; cin>>a>>b>>x; a += (x-a%x)%x; b -= b%x; if(a<=b){ cout<<((b-a)/x)+1<<"\n"; } else{ cout<<"0\n"; } } ``` ::: ## [CSES 1662](https://cses.fi/problemset/task/1662/) 撰寫者 : Paul 因為要算被 $n$ 整除的連續子區間數量,我們可以對前綴和 $mod$ $n$ 。可以把題目想成:前綴和 $mod$ $n$ 相同的組數,加上 $mod$ $n = 0$ 的前綴和序列數量。假設有 $k$ 個前綴和 $mod$ $n$ 有相同餘數,則這 $k$ 個前綴和可創造出的答案數量為 $k*(k-1)/2$ ( $k$ 個裡面選兩個且不排順序,因為前綴和只能夠後面的減前面的)。其中如果 $mod$ $n = 0$ 的前綴和序列也符合題目要求,要加進答案裡面。 因為 $a_i$ 可能出現負數,而且對負數取 $mod$ 會跑出負數,所以要加上 $n$ 再記錄下來。 :::spoiler 參考程式 ```cpp= #include<bits/stdc++.h> using namespace std; #define int long long #define endl "\n" int mod[200005]; signed main() { ios_base::sync_with_stdio(0); cin.tie(0); int n, a, sum=0, ans=0; cin>>n; for(int i=1;i<=n;i++) { cin>>a; sum+=a; sum%=n; if(sum<0) sum+=n; mod[sum%n]++; } ans+=mod[0]; for(int i=0;i<n;i++) { ans+=(mod[i]*(mod[i]-1)/2); } cout<<ans<<endl; } ``` ::: ## [Kattis commercials](https://open.kattis.com/problems/commercials) 撰寫者:百鬼真香 因為每播一次廣告需要花 $P$ 元,而每次播廣告可以多收入 $a_{i}$ 元,所以可以先把每個廣告時段的收入減掉 $P$,接著計算哪個連需時段可以賺最多錢,也就是連續最大和,就可以了。 :::spoiler 參考程式 ```cpp= #include <bits/stdc++.h> using namespace std; #define ll long long #define N 200005 int main() { ios::sync_with_stdio(0); cin.tie(0); int n, p; cin >> n >> p; int a[N]; for(int i=0; i<n; i++){ cin >> a[i]; a[i] -= p; } int tmp = 0, ans = 0; for(int i=0; i<n; i++){ tmp += a[i]; if(tmp > ans) ans = tmp; else if(tmp < 0) tmp = 0; } cout << ans << endl; return 0; } ``` ::: --- # 排序 ## [Kattis recount](https://open.kattis.com/problems/recount) 撰寫者:Eason 先排序好所有人的名字,方便紀錄每個名字出現幾次,最後找出出現最多次及第二多次的名字,並比較兩者是否相同。 另外這題也可以利用 map 來解。 :::spoiler 參考程式碼 ```cpp= #include<bits/stdc++.h> #define weakson ios::sync_with_stdio(0), cin.tie(0); using namespace std; vector<string> v; int main(){ weakson; while (true){ string temp; getline (cin, temp); if (temp == "***") break; v.emplace_back (temp); } sort (v.begin(), v.end()); int j; pair <int, string> Max, Sec; Max.first = 0; Sec.first = -1; for (int i = 0; i < v.size(); i = j){ for (j = i; j < v.size(); j++){ if (v[j] != v[i]) break; } pair<int, string> tmp = make_pair (j - i, v[i]); if (tmp.first > Sec.first) Sec = tmp; if (Sec.first > Max.first) swap (Sec, Max); } if (Max.first == Sec.first) cout << "Runoff!\n"; else cout << Max.second << '\n'; return 0; } ``` ::: ## [Kattis height](https://open.kattis.com/problems/height) 撰寫者:fishhh 這一題就是要求「插入排序」的交換次數。 如果不知道插入排序的話: > 可以先看一下使用插入排序資料會長怎樣:https://youtu.be/kPRA0W1kECg?si=ZWsnPvuH4gXa-AaI&t=10 > 可以看講師的介紹:https://hackmd.io/@sa072686/cp/%2FfS7erjD_Q3ekLPbzzxSWRg#%E6%8F%92%E5%85%A5%E6%8E%92%E5%BA%8F%E6%B3%95-Insertion-Sort > 或是題目的敘述: 一名學生將被選為排隊的第一人。然後,選擇另一個學生,找到隊伍中第一個比他高的人,站在那個人的前面,從而導致他後面的所有學生都後退以騰出空間。如果沒有比他高的學生,那麼他就會站在隊伍的最後。這個過程持續進行,一次一個學生,直到所有學生都排隊,此時學生將按身高順序排隊。 :::spoiler 參考程式 ```cpp= void solve(){ int t; cin>>t; while(t--){ int k; cin>>k; int ary[30]; for(int i=1;i<=20;i++){ cin>>ary[i]; } int ans = 0; for(int i=2;i<=20;i++){ for(int j=i;j-1>=1;j--){ if(ary[j]<ary[j-1]){ swap(ary[j],ary[j-1]); ans++; } } } cout<<k<<" "<<ans<<"\n"; } } ``` ::: ## [AtCoder abc073_c](https://atcoder.jp/contests/abc073/tasks/abc073_c) 撰寫者:百鬼真香 讀完題目可以很直觀的發現,如果一個數字叫到 2 的倍數次,那他最後就不會在單子上,所以直接算出現奇數次的數字的個數就可以了。 如何計算每個數字出現的次數? 排序後,同樣的數字就會在一起,只要掃過一次就知道了。 :::spoiler 參考程式 ```cpp= #include <bits/stdc++.h> using namespace std; #define ll long long #define N 200005 int main() { ios::sync_with_stdio(0); cin.tie(0); int n; cin >> n; vector<int> a; for(int i=0; i<n; i++){ int x; cin >> x; a.push_back(x); } sort(a.begin(), a.end()); int now = a[0], ans = 0, cnt = 1; for(int i=1; i<n; i++){ if(a[i] == now) cnt++; else{ ans += cnt & 1; now = a[i]; cnt = 1; } } ans += cnt & 1; cout << ans << '\n'; return 0; } ``` ::: ## [AtCoder abc085_b](https://atcoder.jp/contests/abc085/tasks/abc085_b) 撰寫者:fishhh <!-- 這個問題其實有一個專有名詞:「LIS」,可以透過 DP 在 $O(n \log n)$ 的時間內完成。 但這一題 $n$ 只有到 100 所以我們只要想一個比較簡單的方法就好。 --> 題目意思就是要對所有麻糬做排序,並且由大到小疊,但上下兩個麻糬之間一定要是嚴格大於。 可以發現這一題等價於把數列中重複的數挑出來。 那我們可以先排序後,計算有幾對數是一樣的,接下來只需要把它減掉即可。 :::spoiler ```cpp= int ary[120]; void solve(){ int n; cin>>n; for(int i=1;i<=n;i++){ cin>>ary[i]; } for(int i=1;i<=n;i++){ //插入排序 for(int j=i-1;j>=1;j--){ if(ary[j+1]<ary[j]){ swap(ary[j+1],ary[j]); } } } int same = 0; for(int i=2;i<=n;i++){ if(ary[i]==ary[i-1])same++; } cout<<n-same<<"\n"; } ``` ::: ## [Kattis nothanks](https://open.kattis.com/problems/nothanks) 撰寫者:百鬼真香 題目:找所有連續數字的第一個數並加起來 解法 1: 直接的解法就是排序後,如果是連續數字,前後兩個數就只差 1,所以從小排到大後,只要一個數是前一個數加一,那就可以跳過他,否則就加上他。 特別的,排序後的第一個數一定是其中一個解,所以可以直接加上他。 :::spoiler 參考程式 ```cpp= #include <bits/stdc++.h> using namespace std; #define ll long long #define N 90005 int main() { ios::sync_with_stdio(0); cin.tie(0); int n; cin >> n; int a[N]; for(int i=0; i<n; i++) cin >> a[i]; sort(a, a+n); int ans = a[0]; for(int i=1; i<n; i++){ if(a[i] == a[i-1] + 1) continue; ans += a[i]; } cout << ans << endl; return 0; } ``` ::: \ 解法 2: 因為想到了所以不寫一下感覺就浪費了,以下為毒瘤,請謹慎考慮是否要浪費時間在這。 剛剛的解法時間是 $O(n\log{n})$,那可不可以優化成 $O(n)$? 題目的數字在 90000 內,所以我們可以用一個 01 陣列來表示一個數是否出現過,那麼題目就變成:有幾個連續的 1,並算出所有連續 1 的開頭的 index 和。 如何判斷一個連續 1 的區間: 一個連續 1 區間代表其前後都是 0,所以如果相鄰兩數 xor 等於 1 就代表這是一個連續 1 區間的開頭或結尾。 如何確定他是區間開頭: 如果 $a_i\ xor\ a_{i+1} = 1$,且 $a_{i+1} = 1$,那他就是開頭,否則就是結尾 這樣一來我們就可以 $O(n)$ 解了。 :::spoiler 參考程式 ```cpp= #include <bits/stdc++.h> using namespace std; #define ll long long #define N 90005 int main() { ios::sync_with_stdio(0); cin.tie(0); int n; cin >> n; int a[N] = {0}; for(int i = 0; i < n; i++){ int x; cin >> x; a[x] = 1; } int ans = 0; for(int i=1; i<N; i++) ans += (a[i] ^ a[i-1]) * a[i] * i; cout << ans << endl; return 0; } ``` ::: ## [Kattis synchronizinglists](https://open.kattis.com/problems/synchronizinglists) 撰寫者:Eason 題目會給你很多個 $n$,對於每一個 $n$,會有兩組長度為 $n$ 的陣列 $a$、$b$。你要做的事把兩個陣列都分別排序好後(假設排序好的陣列叫做 $as$、$bs$),讓 $mp[as_i]=bs_i$,最後求 $mp[a_i]$。注意到 $a_i$ 的範圍是 $[-10000,10000]$,如果直接 $mp[a_i]$ 會出事,所以可以特判 $a_i<0$ 的時候就存在 $mp[10000-a_i]$,這樣就能保證索引值不為負。或者可以直接用 std::map,就不用擔心負數的問題了。 :::spoiler 參考程式碼 ```cpp= #include<bits/stdc++.h> #define weakson ios::sync_with_stdio(0), cin.tie(0); using namespace std; int main(){ weakson; int n; while (cin >> n){ if (n == 0) break; vector<int> a(n), b(n), cpa(n), mp(20005); for (int i = 0; i < n; i++){ cin >> a[i]; cpa[i] = a[i]; } for (int i = 0; i < n; i++){ cin >> b[i]; } sort (cpa.begin(), cpa.end()); sort (b.begin(), b.end()); for (int i = 0; i < n; i++){ if (cpa[i] < 0) mp[10000 - cpa[i]] = b[i]; else mp[cpa[i]] = b[i]; } for (int i = 0; i < n; i++){ if (a[i] < 0) cout << mp[10000 - a[i]] << '\n'; else cout << mp[a[i]] << '\n'; } cout << '\n'; } } ``` ::: ## [AtCoder abc219_c](https://atcoder.jp/contests/abc219/tasks/abc219_c) 撰寫者:fishhh 這一題的意思是,題目會給一個長度為 $26$ 的排列,也就是題目希望的字典序。 例如,我們常用的字典序就是 $abcdefghijk.....$ 也就是 $a$ 最大,$b$ 第二大...。 因為我們會寫 merge sort 所以整個題目**與一般排序不同的地方只有「比較」這件事情上面,其餘的完全相同**。 但是,比較兩個字串的自定義字典序會很麻煩,所以我們只要寫一個程式來做比較就好,非常非常容易。 :::spoiler 參考程式碼 ```cpp= string order; string ary[500010]; int pos[30]; bool great(string a,string b){ // 判斷 a 是否大於 b for(int i=0;i<a.size();i++){ if(a[i]<b[i]){ return 1; //確實 a 大於 b } else if(a[i]>b[i]){ // a 小於 b return 0; } } // 有一個字串是另一個字串的子字串 // 例如 abcd 跟 abcdfvfge // 這時候就比較大小,長度小的在前面 if(a.size()<=b.size()){ return 1; } return 0; } void solve(){ cin>>order; for(int i=0;i<26;i++){ pos[order[i]-'a'] = i+1; } int n; cin>>n; for(int i=1;i<=n;i++){ cin>>ary[i]; } } ``` ::: ## [Kattis conformity](https://open.kattis.com/problems/conformity) 撰寫者:Jun-an 這題要找出最受歡迎的「課程組合」的人數。 假設某個學生選了 $100$ $101$ $102$ $103$ $488$ 這五門課, 那麼選課組合就是 「$100$ $101$ $102$ $103$ $488$」。 不過每個課程組合都沒有照順序排, 因此計算每個課程組合有多少人選之前要先排序。 至於課程組合的部分,可以發現都是由 $5$ 個長度為 $3$ 的數字組成, 所以可以直接用 $long$ $long$ $int$ 儲存,或是你要用 $string$ 也行。 接著是要計算最受歡迎的課程組合, 排序過後會發現一樣的課程組合會排在一起, 因此只需要用一次 $for$ 迴圈掃過即可。 這裡補充一下,如果出現多種最受歡迎組合的選課人數一樣時, 需要全部加總。 :::spoiler 參考程式碼 ```cpp= #include <iostream> #include <algorithm> using namespace std; int course[10005][5]; long long save[10005]; int main() { int n; while (cin >> n && n != 0) { for (int i = 0; i < n; ++i) { for (int j = 0; j < 5; ++j) { cin >> course[i][j]; } sort(course[i], course[i] + 5); for (int j = 0; j < 5; ++j) { save[i] = save[i] * 1000 + course[i][j]; } } sort(save, save + n); int mx = 1, cnt = 1, res = 1; for (int i = 1; i < n; ++i) { if (save[i] == save[i - 1]) { ++cnt; if (i == n - 1) { if (cnt > mx) { mx = cnt; res = cnt; } else if (cnt == mx) { res += cnt; } } } else { if (cnt > mx) { mx = cnt; res = cnt; } else if (cnt == mx){ res += cnt; } cnt = 1; } } cout << res << '\n'; // clear. for (int i = 0; i < n; ++i) { for (int j = 0; j < 5; ++j) { course[i][j] = 0; } save[i] = 0; } } } ``` ::: ## [CSES 1074](https://cses.fi/problemset/task/1074/) 撰寫者:fishhh 題目的意思:給你 $n$ 根木棍,可以對每一個選擇要增長或是縮短,並且操作完後每一根長度一樣。 假設我們決定要讓最後的長度為 $x$,那麼成本就會是 $\sum\limits^{n}_{i=1} |a[i] - x|$。 為了讓成本最小化,也就是 $\forall x \in \mathbb{N},min(\sum\limits^{n}_{i=1} |a[i] - x|)$ 經過高中數學的知識洗禮,相信大家都知道只要是這個數列的中位數就可以找到最小值。 所以,我們只要將這個數列排序後,並且將 $x$ 設為中位數然後線性掃一次就可以算出答案了! 這一題就不附程式碼了~ ## [Kattis toflur](https://open.kattis.com/problems/toflur) 撰寫者:fishhh 以下提供本人不太嚴謹的證明,假設由小排到大是 $a_1 \leq a_2 \leq a_3.... \leq a_n$,最後的分數是 $k$。 那麼,我們可以試著打亂它看看,例如說把 $a_5$ 與 $a_6$ 交換看看(因為打亂一定是一堆數字的交換) 喔對,先把裡面的 sigma 展開,會發現其中是一堆平方,也就是 ${a_1}^2 + 2 \times ({a_2}^2 + {a_3}^2 ... + {a_{n-1}}^2) + {a_n}^2$ 然後減掉兩兩相鄰的(乘積)*2 我們把 $a_5$ 跟 $a_6$ 交換本身對於前面的平方和是完全不會影響的。 且 $a_5$ 跟 $a_6$ 本身有參與到的兩兩乘積就只有 $a_4 \times a_5$ 與 $a_5 \times a_6$ 與 $a_6 \times a_7$ 又可以知道,其中的 $a_5 \times a_6$ 完全沒有影響,那就先拿掉了。 一開始對分數的貢獻為:$-2(a_6 \times a_7 + a_4 \times a_5)$ 交換後會變成:$-2(a_5 \times a_7 + a_4 \times a_6)$ 所以我們只要比一下這兩個的大小即可。 1. 先把 -2 拿掉,記得一下我們最後求出來的結果要反過來一次。 - $(a_6 \times a_7 + a_4 \times a_5)$ - $(a_5 \times a_7 + a_4 \times a_6)$ 2. 移項 變成是 - $a_4 \times (a_5-a_6)$ - $a_7 \times (a_5-a_6)$ 可以發現 $a_4$ 明顯小於 $a_7$ (最最最一開始的定義) 但,$(a_5-a_6)$ 又是負的,所以上面的會大於下面的。 但但但,我們需要把結果反過來(因為有拿掉 $-2$) 所以,上面的貢獻小於下面,換句話說,如果變成下面那樣子的話,分數就會增加,但我們只想求分數的最小值,所以不做任何改變(由小到大)就可以得到分數最小值。 ~~打太多字了,懶得寫程式。~~ ## [Kattis judging](https://open.kattis.com/problems/judging) 撰寫者:百鬼真香 題目要求兩個 judge 中同樣的字串最多幾個,所以可以分別計算單一種結果共出現幾次,在兩邊的 judge 中取小的,就是一種結果的最多共同次數。 如何計算單一種結果共出現幾次? 因為要算同種字串出現幾次,用陣列很麻煩,所以可以用另一種資料結構叫 map map 就像是自由版陣列,他可以把任意東西當作 index 也就是當作標籤,在這個標籤下有一個值,所以我們可以把這題的字串當作標籤,出現過幾次當作值,就可以簡單的使用它了。 :::spoiler 參考程式 ```cpp= #include <bits/stdc++.h> using namespace std; #define ll long long #define N 100005 int main() { ios::sync_with_stdio(0); cin.tie(0); int n; map<string, int> mp1, mp2; set<string> s; cin >> n; for (int i = 0; i < n; i++){ string x; cin >> x; mp1[x]++; s.insert(x); } for (int i = 0; i < n; i++){ string x; cin >> x; mp2[x]++; s.insert(x); } int ans = 0; for(auto e:s) ans += min(mp1[e], mp2[e]); cout << ans << endl; return 0; } ``` ::: ## [ZJ b966](https://zerojudge.tw/ShowProblem?problemid=b966) 撰寫者:Koying 講義有提供合併線段的解法,參考程式如下 :::spoiler Code ```cpp= #include <bits/stdc++.h> #define ll unsigned long long using namespace std; pair<int,int> v[10005]; signed main() { int n; cin >> n for(int i=0;i<n;i++) { cin >> v[i].first >> v[i].second; } sort(v,v+n); // 1,2 4,8 5,6 7,9 int a=v[0].first,b=v[0].second; int ans=0; for(int i=0;i<n;i++) { if(v[i].first>b) { ans+=b-a; a=v[i].first; b=v[i].second; } else if((v[i].second)>b) b=v[i].second; } ans+=b-a; cout<<ans<<endl; return 0; } ``` ::: #### 另解 1(只需陣列的解法): 可以觀察到,對於一個被線段覆蓋的區間(如 $[1, 5]$ 就是一個被 $[1, 4], [2, 5]$ 兩線段覆蓋的區間),其起終點一定是一個線段的起終點,因此我們只需要記錄所有線段的起點與終點位置即可。 假設有一個線段 $[1, 4]$,我們在 $1$ 的位置紀錄為 $+1$,代表這個位置多了一條線段,並在 $4$ 的位置紀錄為 $-1$,代表這個位置少了一條線段。 最後,將紀錄的數字做前綴和後得到的結果,就是每個位置的線段覆蓋**厚度**。只要計算 $> 0$ 的塊數就是答案。 時間複雜度:$\mathcal{O}(10^7)$ :::spoiler Code ```cpp= #include <bits/stdc++.h> using namespace std; int x[10005], y[10005]; int cnt[10000005]; signed main() { ios::sync_with_stdio(0); cin.tie(0); int n; cin >> n; for (int i = 0; i < n; i++) { cin >> x[i] >> y[i]; cnt[x[i]]++; cnt[y[i]]--; } int ans = 0; for (int i = 0; i < 10000005; i++) { cnt[i] += cnt[i - 1]; ans += (cnt[i] > 0); } cout << ans << '\n'; } ``` ::: #### 另解 2(使用排序 + struct / pair) 同樣是使用紀錄頭尾的技巧,我們將頭尾座標及狀態包成一個 struct or pair(例如線段 $[1, 4]$ 包成 $(1, +1), (4, -1)$)並用陣列紀錄。 接著我們將這個陣列依照第一個元素(座標)排序,得到的結果便是從左到右的**狀態更新順序**。 接著我們就可以依序對每次狀態更新進行計算,並記錄以下變數: - $now$:上一次更新到哪 - $cur$:現在座標 - $cnt$:線段覆蓋厚度 - $ans$:答案 對於每種狀態更新,只有兩種情況: 1. 原本沒有線段覆蓋,現在多了一個 2. 原本有線段覆蓋,現在多或少了一個 可以發現,能夠修改答案的情況只有第二種,因為在這個座標之前就有線段覆蓋了,所以我們就可以將答案 $ans$ 加上 $cur - now$,並將 $now$ 更新為 $cur$,就可以得到最終的答案 :::spoiler Code ```cpp= #include <bits/stdc++.h> using namespace std; pair<int, int> e[20005]; signed main() { ios::sync_with_stdio(0); cin.tie(0); int n; cin >> n; for (int i = 0; i < n; i++) { int x, y; cin >> x >> y; e[i * 2] = {x, 1}; e[i * 2 + 1] = {y, -1}; } sort(e, e + n * 2); int ans = 0, now = 0, cnt = 0; for (int i = 0; i < n * 2; i++) { if (cnt == 0) { cnt += e[i].second; now = e[i].first; } else { cnt += e[i].second; ans += e[i].first - now; now = e[i].first; } } cout << ans << '\n'; } ``` 兩份解法的時間差異: ![](https://hackmd.io/_uploads/HkZpca513.png) ::: ## [Kattis Sort Of Sorting](https://open.kattis.com/problems/sortofsorting) 撰寫者:Eason 用內建sort寫cmp :::spoiler 參考程式碼 ```cpp= #include<iostream> #include<algorithm> #define ll long long using namespace std; bool cmp(string a, string b){ if (a[0] == b[0]){ if (a[1] == b[1]){ return false; } else{ return a[1] < b[1]; } } else{ return a[0] < b[0]; } } int main(){ ios::sync_with_stdio(0), cin.tie(0); int n; while (cin >> n){ if (n == 0) break; string name[205]; for (int i = 0; i < n; i++){ cin >> name[i]; } stable_sort (name, name + n, cmp); for (int i = 0; i < n; i++){ cout << name[i] << '\n'; } cout << '\n'; } return 0; } ``` ```cpp= #include <bits/stdc++.h> #define fastio ios::sync_with_stdio; cin.tie(0); cout.tie(0); #define int long long using namespace std; typedef pair<int, int> pii; typedef vector<int> vi; typedef vector<vi> v2i; typedef vector<v2i> v3i; typedef vector<string> vs; typedef vector<vs> v2s; typedef vector<pii> vp; typedef vector<vp> v2p; typedef vector<bool> vb; typedef vector<vb> v2b; int n; vs arr; bool compare(string& a, string& b) { return a.substr(0,2) <= b.substr(0,2); } void merge_sort(vs& arr, int l, int r) { if (l + 1 >= r) { return; } int mid = (l + r) >> 1; merge_sort(arr, l, mid); merge_sort(arr, mid, r); vs temp(r - l); int temp_index = 0; int l_ptr = l; int r_ptr = mid; while (l_ptr < mid && r_ptr < r) { if (compare(arr[l_ptr], arr[r_ptr])) { temp[temp_index++] = arr[l_ptr++]; } else { temp[temp_index++] = arr[r_ptr++]; } } while (l_ptr < mid) { temp[temp_index++] = arr[l_ptr++]; } while (r_ptr < r) { temp[temp_index++] = arr[r_ptr++]; } for (int i = l; i < r; i++) { arr[i] = temp[i - l]; } } void solve() { while (true) { cin >> n; if (n == 0) { break; } arr.resize(n); for (int i = 0; i < n; i++) { cin >> arr[i]; } merge_sort(arr, 0, n); for (string i : arr) { cout << i << '\n'; } cout << '\n'; } } signed main() { fastio; solve(); return 0; } ``` ::: ## [UVa 11462](http://domen111.github.io/UVa-Easy-Viewer/?11462) 撰寫者:Eason 直接排序好後輸出,要特別注意 Uva 有嚴格比對,行末不能輸出多餘的空格。 :::spoiler 參考程式碼 ```cpp= #include<bits/stdc++.h> #define weakson ios::sync_with_stdio(0), cin.tie(0); using namespace std; int main(){ weakson; int n; while (cin >> n){ if (n == 0) break; vector<int> v(n); for (int i = 0; i < n; i++){ cin >> v[i]; } sort (v.begin(), v.end()); for (int i = 0; i < v.size(); i++){ cout << v[i]; if (i != v.size() - 1) cout << ' '; } cout << '\n'; } return 0; } ``` ::: ## [AtCoder abc208_c](https://atcoder.jp/contests/abc208/tasks/abc208_c) 撰寫者:百鬼真香 題目要平均發給每人糖果,如果有剩的就從 ID 小的開始一人給一個,所以我們可以用 ID 排序後,每個人有 K/N 個糖果,再從小到 K%N 一人給一個就發完了。 但是,題目要求用原本的順序輸出,所以我們需要在一開始記錄每個人的 index,並在輸出前照 index 排序,就是正確答案了。 因為每個人要記錄 ID、index、糖果數,所以可以利用 struct 來當作每個人。 :::spoiler sort cmp 的另外一種寫法 正常來說,要另外定義比較函式需要這樣寫: ```cpp= bool cmp(int a, int b){ return a < b; } int main(){ int a[100]; ... sort(a, a+100, cmp); } ``` 不過,這樣寫還要開新的函式,要移動鼠標打斷思緒,感覺很不爽。 所以有一種不同的寫法可以把比較函式寫在 main 函式中,名為匿名函式,~~或許是因為不用取名~~ ```cpp= int main(){ int a[100]; ... sort(a, a+100, [](int a, int b){ return a < b; });// 匿名函式 } ``` 這種寫法跟 cmp 的結構差不多 我們一樣需要兩個變數 $a, b$ 做比較 `(int a, int b)` 並在函式中寫上需要的比較條件 `{ return a < b; }` 最後在前面加上中括號,告訴他這是匿名函式 `[]` 就大功告成了 `[](int a, int b){ return a < b; }` 這種寫法可以用在簡單的比較函式上,因為為了幾個字母開一個函式實在麻煩,而且這種寫法更易讀也更好寫。 ::: :::spoiler 參考程式 ```cpp= #include <bits/stdc++.h> using namespace std; #define ll long long #define N 200005 struct citizen{ ll id, i, v; }a[N]; int main() { ios::sync_with_stdio(0); cin.tie(0); ll n, k; cin >> n >> k; for(int i=0; i<n; i++){ cin >> a[i].id; a[i].v = k / n; a[i].i = i; } sort(a, a+n, [](citizen a, citizen b){ return a.id < b.id; }); for(int i=0; i < k%n; i++) a[i].v++; sort(a, a+n, [](citizen a, citizen b){ return a.i < b.i; }); for(int i=0; i<n; i++) cout << a[i].v << "\n"; return 0; } ``` :::