# 考幹考題解 Part 1(?) by yungyao ###### tags: 題解 考幹 只有標題裡面是"XX和她/他的XXXX"是我出的ㄛ 其他題的題解請找其他出題人(雖然好像是我沒寫完,是別人要來找我咩噗 \學弟妹特別強/ ## A部分 這部分都語法題 個人是希望每個人都能把這裡的500分都拿到 希望我在這裡講題解的時候大家都有做到><(結果我根本還沒寫完題解,慘 ### pA3 tags : `string` `implementation` `constructive algorithm` 就是基本的回文性質跟簡單的字串相關語法 理論上分兩個Subtask,也就是25分解跟100分解 但cf不給連續給分,所以可能會有各種奇怪ㄉ分數(像py被卡常之類的www 總之我們只要考慮一個簡單的greedy就好 對於任意一組 $s[i] \neq s[n-i+1]$ ,我們都修改其中任意一項 透過迴文的性質可以很容易的證明做這個改動是好的greedy策略 所以我們只要跑過去數有幾個相異就好了 而滿分解要你輸出改過的字串,也是直接改一下就好了w :::spoiler AC Code ```cpp = 1 using namespace std; #include <iostream> int main(){ int n; string s; cin >> n >> s; int cnt = 0; for (int i=0;i<n/2;++i){ if (s[i] != s[n-i-1]){ ++cnt; s[n-i-1] = s[i]; } } cout << cnt << '\n' << s; return 0; } ``` ::: ### pA4 浩浩和他的夢想柯基 tags : `stl` `queue` `data structure` 這題的定位是裸queue 算是給有在上上學期大社的人的均標,寫不出來代表你太混惹QQ (Subtask 1應該算低標ㄛ 因此對於這題你基本上只需要知道一件事 就是`std::queue` 裡面可以放任何C++的element,而不限於variable 或者你要自己刻一個queue也是可以接受的啦 #### Subtask 1 (31%) 這個Subtask因為保證每隻柯基的名字都一樣,也就是說你只需要記錄第一隻柯基的名字 再來只要記錄收容所裡面有幾隻柯基就好了 所以31分只要這樣就能拿了 :::spoiler 31 points ```cpp int main(){ int siz = 0; int t; string corgi; cin >> t; while (t--){ char cmd; cin >> cmd; if (cmd == '1'){ cin >> corgi; ++siz; } else if (cmd == '2'){ int adp; cin >> adp; if (!adp && siz){ cout << corgi << '\n'; --siz; } } } return 0; } ``` ::: #### Subtask 2,3,4 這三個Subtask基本上一樣 只是對於有些操作需要注意不同的事項,所以可以一起講 反正就是要維護一個queue資料結構 ##### Subtask 2 這個Subtask保證每次對`queue`做`pop`時`queue`都不為空,所以你可以不用檢查是否`empty` ##### Subtask 3 這裡則保證每一次`pop`都可以輸出東西,其實我不知道什麼情況下會寫爛這件事,大概沒判斷好之類的吧,相信拿到這個Subtask的人,其他應該也不難拿XD ##### Subtask 4 就不保證以上兩個Subtask的事,但我相信你們做出這兩個Subtask應該也不難做滿分解啦 :::spoiler 100 points yea ```cpp int main(){ ios_base::sync_with_stdio(false),cin.tie(0); int t; queue <string> Q; cin >> t; while (t--){ int cmd; cin >> cmd; if (cmd == 1){ string s; cin >> s; Q.push(s); } else if (cmd == 2){ int k; cin >> k; if (Q.size()){ if (!k){ cout << Q.front() << '\n'; } Q.pop(); } } } return 0; } ``` ::: ## B部分 這裡會稍難一點,但我相信每個人多少都能拿到一些部分分,有來演算法小社跟算法組的學弟妹應該要至少拿一半的分數,加油ㄛ ### pB1 浩浩和果酒和他們的執秘身高差 tags : `double pointer` `binary-search` 先說一下這題的小故事好了,雖然你們最後沒參加到寒訓 但在籌備過程中,某很電的沉思緯學長一直想知道浩浩跟果酒誰比較高 然後我也是十分之有興趣,但他們每次都會避開站在一起,所以我們一直不知道到底誰比較高 直到某次社遊我跟#yououorz在討論題目的時候討論到[TIOJ 1905 最理想的身高差](https://tioj.ck.tp.edu.tw/problems/1905) 剛好那天我們也是努力的想要知道誰比較高,我就想到可以把那題給簡化一下(其實簡化滿多ㄉ,我到現在還不會寫原題XD)當作你們考幹題,於是就有ㄌ這題 還有就是我為了在最後一筆Subtask卡掉`std::set`,所以$n$給到很大,原本是只打算給到$2\times10^5$ㄉ 反正`std::set`跟`std::map`都是很肥的資結,題目想要搞事~~(像是我)~~就會在這邊卡常(之前寫poj的題目ㄉ時候被卡set大腸懷恨在心辣 #### Subtask 1 這裡保證$0 \leq a_i,b_i \leq 400$,換句話說答案也必定小於400 至於要如何檢查一個數$k$能不能是答案,只要檢查$a_i + k \in b$或$a_i - k \in b$,而這件事可以用一個`bool`陣列或`std::bitset`去存一個值是否被包含在$b$之中,反正都會是$O(1)$ 複雜度$O(nC)$ (btw其實有開放$O(nC^2)$,因為我懶得卡,而且其實想法差不多XD) #### Subtask 2 這個Subtask跟前一個很像,只是$n,a_i,b_i$都變大了,$O(nC)$會過肥 但你只需要做一個小小的優化,就是對於$a$也做跟$b$相同的事 用一個`bool`陣列或`std::bitset`存$a$,就不用對$a$枚舉了,現在只需要對$a$的值域枚舉 複雜度可以降到$O(C^2)$ #### Subtask 3 因為$n \leq 2000$,也就是$O(n^2)$可過,而你會發現差一共也只有$n^2$個,因此你只要兩層`for`迴圈包起來就能拿到分數ㄌ :::spoiler 枚舉亂做拿分數,建北電資北市賽有望 ```cpp LL a[3000100]; int main(){ ios_base::sync_with_stdio(false),cin.tie(0); int n; LL ans = 1e10; cin >> n; for (int i=n;i--;) cin >> a[i]; for (int i=n;i--;){ LL x; cin >> x; for (int j=n;j--;) ans = min(ans,abs(x-a[j])); } cout << ans; return 0; } ``` ::: 但要特別注意的是,最大的差會是$(2^{31}-1) - (-(2^{31}-1)) = 2^{32}-2$,會爆`int`範圍,要用`long long`去儲存。 #### Subtask 4 這應該是所有Subtask裡面最水的 你可以很輕鬆地發現在$min(a) \geq max(b)$的情況下,最小差一定會是$min(a) - max(b)$ 而在$a,b$都呈非嚴格遞減下$min(a) = a_n ,max(b) = b_1$ 所以你只要輸出$a_n - b_1$就好了 5分get,這5分沒拿到我應該可以相信你根本沒看題目QQ #### Subtask 5,6 5,6基本上可以一起講,只有常數差異而已 這題的滿分解基本上有兩種,可以二分搜也可以雙指針 對於這種可以任意挑東西的題目,先sort都是一個好的想法 這題也是這樣,我們先對$a$由小到大排列 接著先考慮雙指針的作法(又稱為爬行法 :::spoiler solution 1 double pointer 我用虛擬碼寫,官解的.cpp檔長的有點醜QQ ``` int n = input() array a = input() array b = input() sort (a) sort (b) int64 ans = 2 ^ 32 - 1 iterator b_ptr = b.begin() //對於b的指針 for element in a{ while *next(b_ptr) < element and next(b_ptr) != b.end() b_ptr = next(b_ptr) ans = min (ans,abs(n - *b_ptr),abs (n - *next(b_ptr))) } print (ans) ``` ::: 這份扣的的概念很簡單 在$a,b$都被排序下 我們對於$a$的每一項都去找到其在$b$當中介於哪兩項之間 由於$b$也已經被排序,所以我們如果維護$a$當中的每一項對應到$b$的哪一項,那這件事會遞增 所以我們就可以利用一個雙指針去好好維護他了 由於像這樣對兩個序列各維護一個遞增指針的作法,其實很像在兩個序列上爬行,所以又被稱為爬行法 但雙指針聽起來比較厲害w 然後我們可以來看一下第二種作法,二分搜的核心概念其實是一樣的 我們想要找到$b$的每一項介於$a$($a,b$反過來其實沒差)的哪兩項之間 如果我們已經將$a$排序的話,應該滿容易可以想到二分搜這個作法 基本上大同小異,差別只是實作查找這個過程而已 :::spoiler solution 2 binary search ```cpp LL a[1500100]; int main(){ theyRSOOOOOOOOODIAN int n; cin >> n; for (int i=0;i<n;++i) cin >> a[i]; sort (a,a+n); LL ans = maxint64; for (int j=0;j<n;++j){ LL b; cin >> b; int i = lower_bound(a,a+n,b) - a; if (i != n) ans = min(ans,a[i] - b); if (i) ans = min(ans,b - a[i-1]); } cout << ans; return 0; } ``` ::: 這邊順便介紹一下`lower_bound()`這個函式 他屬於標頭檔`algorithm` 他可以直接幫你在一個排序好的陣列上做二分搜 要餵給三個參數 `lower_bound ( iterator start, iterator end, k )` 分別是迭代器起始點、迭代器終點、及查找的值 前兩項類似sort餵的參數,array 的話就給他 `a,a+n`,`vector`的話就給他`a.begin(),a.end()` 而他會回傳一個指標,指向在你給他的範圍中,大於等於$k$的最小值 而如果$k$比全部的值都大,那就會回傳終點(也就是 `a+n` 或 `a.end()`) 那如果你想把他當index用,就像我前面範例code一樣,讓他剪掉起點就好了 ### pB2 青蛙和他的青蛙下蛋 tags : `greedy` `bitwise` 這題是我在躺在床上睡不著的時候想到ㄉ 滿分解需要一點數學跟位元運算 不過部分分也是可以用stl做出來的,希望大家都有喇到分>< #### Subtask 1 對於$\exists k(k \in N), n = 2^k$的情形 你可以發現不管怎麼合併,最後一定都能合併成1杯 所以你只要輸出$t$次1,就得到12分ㄌ,開ㄅ開心 #### Subtask 2 那對於其他狀況我們可以考慮真的去模擬合併的過程 至於要如何好好模擬,也不是太麻煩 我們可以發現合併的過程是由小到大 而只要小的還有剩多於兩個把他們合併起來一定是好的 那我們就可以每次看最小的兩個能不能合併 可以的話就合併起來,不行就算了 理論上 $O(n^2),O(n^2logn)$ 都能過,但我也沒試就是ㄌ~~(反正沒人寫這個Subtask~~ #### Subtask 3 跟上一個Subtask類似,但我們可以優化一下找最小兩個的過程 應該滿顯然的用`priority queue`可以解決 複雜度$O(nlogn)$ :::spoiler priority queue sol ```cpp inline void solve(){ int n; priority_queue <int,vector <int>,greater <int>> pq; cin >> n; int cnt = 0; REP (n) pq.push(1); while (pq.size()){ int pop = pq.top(); pq.pop(); if (pq.empty() || pop != pq.top()) ++cnt; else pq.pop(),pq.push(pop * 2); } cout << cnt << '\n'; } //這是我ㄉCF模版 int main(){ ios_base::sync_with_stdio(false),cin.tie(0); int t; for (cin >> t;t--;) solve(); return 0; } ``` ::: #### Subtask 4 應該不難發現,最後每一杯飲料的大小都是 $2^k$ 而由小到大合併後,最後會併成的杯數變會是將 $n$ 以二進位表示後的bit數量 多數人基本上都有想到這件事,接下來就是看你怎麼實作而而已了 這邊介紹一個最簡潔的實作方式 在C\++中有一系列的builtin函式,這些函式不屬於C++ Standard,但幾乎各家編譯器都有將他們實現 而且不需要引入任何標頭檔就能使用 在競賽領域中最常見的就是我們今天的主題`__builtin_popcount()` 這個函式的功能很簡單,就是回傳你給他的數有幾個bit,也就是我這題要求的東西 所以整份code可以精簡如下 :::spoiler 耶 AC ```cpp #include <iostream> using namespace std; int main(){ ios_base::sync_with_stdio(false); cin.tie(0); int t; for(cin >> t;t--;){ int n; cin >> n; cout << __builtin_popcount(n) << '\n'; } return 0; } ``` ::: ### pB3 湯圓和他的交換禮物 tags : `math` `dp` 數學題加打表(你要本機打也是可以啦) 簡單來說透過簡單的排組,你可以知道$n$個人的排列是$n!$種 而你可以發現一個"相同"的排法會被重複計算$n$次 所以$n$個人的相異送法數會是 $\frac{n!}{n} = (n-1)!$ 其實是滿簡單的排組啦我覺得,阿反正你們下次段考就是排組,先預習一下ㄅ 接下來就是怎麼實作他了,不過這應該不難 #### Subtask 1 這個Subtask 是讓你可以用手算ㄉ 反正你就自己畫一畫就好了,也才4種輸入,最多的一組解才24 甚至已經給你兩組輸入的解ㄌ,自己畫圖ㄅw ### Subtask 2 這裡你就分別去算跑一次 $(n-1)!$ 複雜度 $O(qn)$ :::spoiler 部分分 ```cpp #include <iostream> using namespace std; int mod = 1e9 + 7; int main(){ ios_base::sync_with_stdio(false); cin.tie(0); int t; for(cin >> t;t--;){ int n; cin >> n; LL ans = 1; for (LL i = 2;i<n;++i) ans = ans * i % mod; cout << ans << '\n'; } return 0; } ``` ::: #### Subtask 3 這裡你會發現 $O(qn)$ 太慢了 但其實你要輸出的東西是固定的,你可以預處理完再一次輸出 這樣複雜度會是 $O(C) + O(q) = O(max(C,q))$ ($C$一般是指值域) :::spoiler AC Code ```cpp const LL mod = 1e9+7; LL ans[1000100]; int main(){ int q; ans[0] = 1; for (int i=1;i<=1e6;++i){ ans[i] = i * ans[i-1]; ans[i] %= mod; } cin >> q; while (q--){ int n; cin >> n; cout << ans[n-1] << '\n'; } return 0; } ``` ::: ## C部分 這邊只有一題是我出的啦,寫不出來不要怪我ㄛ ### pC2 絲襪和他的模擬城市 tags : `tree` `greedy` `sort` 這題其實是抄的 原題是Codeforces 的 Goodbye 2020 contest題目,我賽中寫那題就覺得很有趣一定要拿來自己出 反正我相信你們應該沒有人打過那場啦XD 阿原本我應該要認真打題解,但我好懶 所以直接去看原題ㄅ [Goodbye 2020 pD](https://codeforces.com/contest/1466/problem/D) [題解](https://codeforces.com/blog/entry/86126) 喔然後subtask 1只要把每個點的點權加起來輸出就好 畢竟你根本沒得分w,這個Subtask 滿多人有拿到的,讚