# 算法模競2--題解 by yungyao 難度錯估ㄉ部分我很抱歉,我低估題目難度ㄌ ## A Apples 就國小數學啦 主要是浮點數的應用 Subtask 1只要用`long long`儲存$a,b$,然後相除輸出就好 Subtask 2則要用`double`儲存,一樣相除輸出就好 由於我精度只要求到$10^{-4}$,所以不需要特別處理,輸出就好 不過還是小補充一下怎麼輸出高精度的浮點數 以C++來說,你會需要先引入標頭檔`iomanip`,然後利用`setprecision()`和`fixed` 大概像下面這樣 ```cpp = 1 using namepace std; #include <iostream> #include <iomanip> ... int main(){ cout << fixed << setprecision(n);//放入你需要小數點下幾位 double a; ... cout << a; return 0; } ``` ## B Map 裸的dsu題,就跟你們說會考ㄅ 反正就是好好做就好 ### Subtask 1 這個Subtask應該很水 只要對每個query都dfs一次就好了 #### 賽後補充 by yungyao DFS要好好寫QQ ### Subtask 2 這個Subtask則是可以在邊都增完後,跑一輪DFS 就可以知道每個點是否屬於同一個連通塊,然後就好好做 ### Subtask 3 額,就DSU,好好做就好 ::: spoiler AC code ```cpp = 1 using namespace std; //IO #include <iostream> #define theyRSOOOOOOOOODIAN ios_base::sync_with_stdio(false),cin.tie(0); //workspace int dsu[200200]; int query(int k){ if (k == dsu[k]) return k; return dsu[k] = query(dsu[k]); } inline void merge(int i,int j){ dsu[query(i)] = dsu[query(j)]; } int main(){ theyRSOOOOOOOOODIAN int n,q; cin >> n; for (int i=1;i<=n;++i) dsu[i] = i; for (cin >> q;q--;){ char c; int u,v; cin >> c >> u >> v; if (c == '1') merge(u,v); else cout << (query(u) == query(v) ? "yes\n" : "no\n"); } return 0; } ``` ::: #### 賽後補充 by youou 我明明跟鄭永堯講過測資有錯,然後他沒改= = #### 賽後補充的補充 by yungyao 我有改辣,只是漏改了一個Subtask,我很抱歉 ## C Investments 沒那麼裸的背包,這題原本是要出在考幹的,但後來換別題ㄌ,開ㄅ開心 一樣分Subtask講 ### Subtask 1 $n \leq 10$,應該滿明顯的是枚舉就能過,然後你就發現常見的枚舉方式好像不太能做 直接標示取或不取好像不太行,因為取的順序有差 然而此時你可以融合兩個常見的枚舉方式:$2^n$表示狀態加上 $n!$ 表示順序(其實這叫做searching tree) 再對所有順序算出答案 不過直接乘起來好像又太大了,感覺不太可做,但其實不會,因為複雜度雖然是 $O(2^nn!)$ 但實際上計算運算次數會是 $\sum\limits^{n}_{i=1}C^i_n \times i! \times i = \sum\limits^n_{i=1}\frac{n!}{(n-i)!}i$ 在 $n = 10$ 時,這個神奇的算式會得到 $88776910$,大約小於 $10^8$,其實是一個不差的運算次數 不過你在賽中大概也懶得算那麼多啦,反正會過XD(其實我沒測就是ㄌ,但我猜不會有人刻著東西) 打那麼多,重點只是有時候演算法的常數可以小到不可思議,還有其他類似例子(譬如$O(n^3)$區間DP $n = 1000$經常可過) ### Subtask 2 這個Subtask你觀察一下就可以發現,他其實是裸的01背包問題 就...這樣吧,沒寫出來ㄉ再讀一下背包的slides ### Subtask 3 這個就比較有趣ㄌ,你會發現一般的背包問題我們基本上不考慮放置順序,可是這題似乎順序會影響答案 我們還是一樣可以把背包的思路放進這題,只要進行一些小修正就好了 首先我們只要先考慮$r_i - c_i > 0$的情況(Trivial) 接著也知道$dp[i][j] \geq dp[i-1][j]$ 也就是說答案只會變大,因此我們只要對$c_i$由小到大排序 而只對 $dp[i-1][j- (r_i-c_i)] \geq c_i$ 的情況做事就好 剩下的部分就跟普通背包一樣ㄌ 100分get :::spoiler AC code ```cpp = 1 using namespace std; #include <algorithm> //IO #include <iostream> #define theyRSOOOOOOOOODIAN ios_base::sync_with_stdio(false),cin.tie(0); //workspace struct node{ int p; int w,rq; }; int main(){ theyRSOOOOOOOOODIAN int n,k,h; int dp[10010]{}; node stuffs[10010]; cin >> n >> k >> h; for (int i=0;i<=k;++i) dp[i] = h; for (int i=0;i<n;++i){ int p,c,r; cin >> p >> c >> r; stuffs[i] = {p,r-c,c}; } sort (stuffs,stuffs+n,[](node a,node b){return a.rq < b.rq;}); for (int i=0;i<n;++i){ for (int j=k;j>=stuffs[i].p && dp[j-stuffs[i].p] >= stuffs[i].rq;--j){ dp[j] = max(dp[j],dp[j-stuffs[i].p] + stuffs[i].w); } } cout << dp[k]; return 0; } ``` ::: 比較可惜的部分是開$10000 \times 10000$的二維陣列會爆炸 所以必須要滾動,但我可能沒有講好滾動的部分,所以有些人就這樣MLE的不明不白,非常ㄅ歉 #### ㄛ其實 這題是抄來ㄉ,原題是 [TIOJ 1675](https://tioj.ck.tp.edu.tw/problems/1675) ## D Love #### Subtask 1 痾,線段樹砸下去。 #### Subtask 2 痾,單點修改砸下去。 #### Subtask 3 痾,暴力的單點修改砸下去。20只是常數。欸不是這樣有90分欸。 #### Subtask 4 可能大家都不想寫這題(?),我就直接暴雷好了。 首先,gcd有個神奇的性質: $\gcd(a, b) = \gcd(a-b, b)$, 我們可以把它推廣到 $\gcd(a_l, a_{l+1}, ..., a_r) = \gcd(\gcd(a_{l+1} - a_l, a_{l+2} - a_{l+1}, ..., a_r - a_{r-1}), a_r)$ 還記得差分嗎? 對於後面那項 $a_r$,我們可以用差分做區間修改、單點查詢的BIT, 前面那項就用單點修改、區間查詢gcd的線段樹就好了。 複雜度 $\mathcal{O}(q \log n)$。 btw 這題是[TIOJ 1282](https://tioj.ck.tp.edu.tw/problems/1282)。 ## E Knapsack 當你以為這是背包問題ㄉ時候,你就錯ㄌ,其實是裸的MST 阿跟DSU那題一樣,好好做就好 :::spoiler AC code ```cpp = 1 using namespace std; #include <algorithm> //IO #include <iostream> #define theyRSOOOOOOOOODIAN ios_base::sync_with_stdio(false),cin.tie(0); //workspace struct Node{ int u,v; int w; }edge[200200]; int dsu[100100]; int query(int k){ return dsu[k] == k ? dsu[k] : dsu[k] = query(dsu[k]); } void modify(int i,int j){ dsu[query(i)] = query(j); } int maxEdge[100100]; int main(){ theyRSOOOOOOOOODIAN int n,m; cin >> n >> m; for (int i=1;i<=n;++i) dsu[i] = i; for (int i=0;i<m;++i) cin >> edge[i].u >> edge[i].v >> edge[i].w; sort (edge,edge+m,[](Node a,Node b){return a.w < b.w;}); int e = 0; for (int i=0;i<m&&e<n-1;++e){ while (i < m && query(edge[i].u) == query(edge[i].v)) ++i; if (i < m){ modify (edge[i].u,edge[i].v); maxEdge[edge[i].u] = max(maxEdge[edge[i].u],edge[i].w); maxEdge[edge[i].v] = max(maxEdge[edge[i].v],edge[i].w); } } for (int i=1;i<=n;++i) cout << maxEdge[i] << ' '; return 0; } ``` ::: ### Subtask 1 特別講一下這個Subtask,你會發現既然 $m = n - 1$,又保證圖連通 代表這張圖一定是一棵樹 而樹的最小生成樹就是整棵樹 所以輸出每個點最大的邊就好ㄌ ## F Answers 本次預期跟D並列第二大難題 不過理論上這題會再難一點 需要多一點競賽經驗比較做的出來,所以你們現階段想不到我滿可以理解的 ### Subtask 1 這個Subtask理論上超級好做的,沒有人去寫實在很可惜 在左界都是1的情況下,你只需要對右界做前綴和就好了 而查詢都在修改後面表示你只需要存每個 $r$ 有多少答案 然後再做前綴和,查詢時只要回答 $r$ 的前綴和是多少就好了 ### Subtask 2 跟上一個Subtask類似,但修改跟查詢輪替的情況下 你就得用BIT去做(或線段樹) 也是實作題而已,只有一個人寫也是偏可惜 :::spoiler 80 points get ```cpp = 1 using namespace std; //IO #include <iostream> #define theyRSOOOOOOOOODIAN ios_base::sync_with_stdio(false),cin.tie(0); //workspace int bit[3030]; void modify(int k,int val){ for (;k<=3000;k+=k&-k) bit[k] += val; } int query(int k){ int r = 0; for (;k;k-=k&-k) r += bit[k]; return r; } int main(){ theyRSOOOOOOOOODIAN int q; for (cin >> q;q--;){ char o; cin >> o; if (o == '1'){ int u,v,w; cin >> u >> v >> w; modify (v,w); } else{ int l,r; cin >> l >> r; cout << query(r) << '\n'; } } return 0; } ``` ::: ### Subtask 3 這個Subtask跟pD的最後一個一樣,寫不出來正常啦 簡單來說我們可以畫一張表格,把左右界分別映射到x/y平面上 下面以縱軸為左界,橫軸為右界 那我們假設在 $[3,5]$ 有了一組解 (v 的位置) ![](https://i.imgur.com/bof0d6S.png) 那會讓用x劃記的都可以用到這組解(也就是所有左界$\leq l$,右界$\geq r$的區間) 那如果換個角度想查詢的狀態 假設要查詢 $[4,6]$ 這個區間 (v 的位置) ![](https://i.imgur.com/NQj6rBH.png) 那所有被x標記的區間都必須被算到 (X標記的區間則是不合法的區間,因為 $l > r$) 也就是X軸前綴、Y軸後綴和,你就可以發現,我們可以用一個二維BIT去維護這個東西ㄌ 但要注意一個維度是前綴,另一個是後綴,所以小心不要混淆 :::spoiler AC code ```cpp = 1 using namespace std; //IO #include <iostream> #define theyRSOOOOOOOOODIAN ios_base::sync_with_stdio(false),cin.tie(0); //workspace int bit[3001][3001]; inline int query(int x,int y){ int val = 0; for (int i=x;i<=3000;i+=i&-i) for (int j=y;j;j-=j&-j) val += bit[i][j]; return val; } inline void modify(int x,int y,int val){ for (int i=x;i;i-=i&-i) for (int j=y;j<=3000;j+=j&-j) bit[i][j] += val; } int main(){ theyRSOOOOOOOOODIAN int q; cin >> q; while (q--){ char c; int l,r,w; cin >> c; if (c == '1'){ cin >> l >> r >> w; modify(l,r,w); } else{ cin >> l >> r; cout << query(l,r) << '\n'; } } return 0; } ``` ::: --- 整體來說,大家都很棒辣 接下來上機考加油ㄛ~~~