0. 批鬥
難度排序:F<<A=J=I<L=G=E<C=K
賽後批鬥,很遺憾最後只開出了 \(6\) 題無緣晉級賽。
\(F\) 題簽到。隊友寫的。一發過。
\(I\) 開賽就被我秒了,我嘗試讓隊友搶一血,但是隊友說會 \(F\) 和 \(J\) 所以放他先寫完,反正沒手速也搶不了一血。
於是我繼續和另一個隊友去討論了 \(I\) 的正確性,討論出來發現顯然是對的。我們打算寫 \(I\) 的時候已經過了四十多隊了,由於機器下面的倆人沒有程式碼能力,口述做法給了主碼手讓他實現,很快一發過了。(賽後我自己去實現甚至花了半個小時,而且這時候我還不困)
\(J\) 題我們隊刷題少的人在這場比賽之前完全不會微擾法,還好隊友會。我當時沒看。隊友一發過。
\(A\) 題是另一個隊友秒了的,和我討論了正確性後發現很對。但是他手速很慢,實現花了很久。也一發過了。
這時候隊伍排名已經遙遙領先了。接下來才是慘敗。
\(L\) 題隊友猜了一個貪心,甚至我也覺得大機率是對的,但是錯了。事後隊友給了一個幾乎正確的做法,我第一次檢查感覺沒問題,\(WA\) 了一發。很久以後我發現隊友的期望多算了 \(1\) ,深入詢問後,確定了他看錯了題。
重新讀題後更新了公式,但公式莫名其妙被約沒了,這時候隊友懷疑思路是錯的,我提出了堅信這個思路是對的觀點,然後獨自去推公式了。
不久後推出了存在一個 \(v\) 滿足左邊的貢獻為 \(\frac{1}{n} i\) ,右邊為 \(1 + E(X)\) 。這時候隊友也同時發現之前的公式被約沒是因為期望又少算了 \(1\) ,再次檢驗後發現和我剛想到的公式一樣,於是篤定是完全對的。
上面兩個錯誤的觀察卡了我們幾乎兩個多小時。當時我為了追罰時不想求導,勸隊友上機三分秒掉,結果手速過慢寫了十幾分鍾。實際上求個導找極點估計只要三五分鐘。
\(G\) 題是我們卡 \(L\) 的時候隊友寫的。賽後觀察了一下顯然需要強制連勝才能導致遊戲繼續,考慮維護機率字首暴力 \(dfs\) 模後的狀態,容易檢驗正確性為每次只會讓狀態乘以 \(2\) 且最多有 \(\log n\) 層狀態。因為去年多校寫過這種題所以很容易想,如果先開 \(G\) 而不是 \(L\) 我們應該能半小時內想完+寫完。
\(E\) 題一眼看過去完全沒思路,詢問隊友後發現他的思路正確性顯然。於是三個人討論一下後發現維護奇偶染色是顯然的,但是有兩個人不會寫程式碼,只能乾瞪眼等主碼手寫。
但還是寫 \(WA\) 了,後來找 \(bug\) 的時候我提出 \(bfs\) 染色的部分是對的,但是時間還剩二十分鐘估計隊友沒精力聽我說話。雖然懷疑是路徑維護出了問題,但是我自己也沒學過路徑維護的正確做法,沒敢強行讓隊友重寫路徑維護,而是寄託於主碼手自己調。
最後還是沒開出來。
很遺憾晉級失敗了,也不一定能成為外卡選手了。如果被宣告拿不到卡,就是有些人九月已經死了,十一月才閉掉眼睛。
前期的手速也沒有任何作用,遇到 \(L\) 和 \(E\) 這種調不出 \(bug\) 的題,最後總是會體現成被其他隊伍題數碾壓。
F
題意:
給 \(a_0 = 1500\) ,詢問第一個 \(i\) 使得 \(\sum_{i = 0}^{i} a_i \geq 4000\) 。
題解:
線效能過,那就秒了,沒什麼說的。
Code
int n; std::cin >> n;
std::vector<i64> a(n + 1);
a[0] = 1500;
int v = -1;
for (int i = 1; i <= n; i++) {
std::cin >> a[i];
a[i] += a[i - 1];
if (v == -1 && a[i] >= 4000) {
v = i;
}
}
std::cout << v << "\n";
A
題意:
給 \(n\) 支隊伍,每支隊伍能力值為 \(a_i \ (1 \leq i \leq n)\) ,屬於學校 \(b_i \ (1 \leq i \leq n)\) 。有 \(k\) 場區域賽,第 $ \ (1 \leq i \leq k)$ 個賽區限制每個學校最多派遣 \(c_i\) 隊。每支隊伍最多選擇兩個賽區。
每支隊伍不知道其他隊伍的選賽區情況(哪怕是自己學校的),詢問第 \(i, i = 1, 2, \cdots, n\) 支隊伍在最優選擇情況下的最壞的最高排名。
題解:
容易發現這是一個欺騙題,我們並不期望能夠獲得的排名率儘可能優,而是希望排名儘可能高。
於是最高排名一定會選擇最小的賽區。直接 \(ban\) 掉 \(c\) 陣列和可選兩個賽區的操作。
維護出每個學校的隊伍,假設有 \(m\) 個學校分別有 \(t_i \ (1 \leq i \leq m)\) 支隊伍。
考慮最優選擇下的最壞情況。
令 \(mn = \mathbf{min}\{c_i\}\) ,這個賽區最壞會被所有學校最強的 \(\mathbf{min}(mn, t_i)\) 支隊伍選擇。維護出這些隊伍並進行一個升序排序,假設儲存在 \(vec\) 。
考慮每個隊伍,不妨是第 \(i \ (1 \leq i \leq n)\) 個。
- 這個隊伍如果在這些強隊中,只需 \(lower\_bound\) 出第一個不弱於它的隊伍的位置 \(p\) ,這個隊伍一定是它,於是它在 \(p\) 這個位置。
- 這個隊伍如果不在這些強隊中,考慮 \(lower\_bound\) 出第一個不弱於它的隊伍的位置 \(p\) ,擠掉右邊一個同校的隊伍,最壞會讓從 \(p\) 開始的隊伍右移一位,於是它也在 \(p\) 這個位置。
答案是 \(|vec| - 1 - p + 1 = |vec| - p\) 。
Code
int n, k; std::cin >> n >> k;
const int INF = 1 << 30;
int mi = INF;
for (int i = 1; i <= k; i++) {
int x; std::cin >> x;
mi = std::min(mi, x);
}
std::map<std::string, std::vector<int> > sch;
std::vector<int> a(n + 1);
for (int i = 1; i <= n; i++) {
std::string s; int x;
std::cin >> x >> s;
sch[s].push_back(x);
a[i] = x;
}
std::vector<int> teams;
for (auto &t : sch) {
std::vector<int> vec = t.second;
std::sort(vec.begin(), vec.end(), std::greater<>());
for (int i = 0; i < std::min(mi, (int)vec.size()); i++) {
teams.push_back(vec[i]);
}
}
int tot = teams.size();
std::sort(teams.begin(), teams.end());
// for (auto x : teams) std::cout << x << " "; std::cout << "\n";
for (int i = 1; i <= n; i++) {
auto it = std::lower_bound(teams.begin(), teams.end(), a[i]);
int rk = it - teams.begin();
std::cout << tot - rk << "\n";
}
時間複雜度依賴於排序的 \(O(n \log n)\) 和容器呼叫的 \(O(n \log n)\) 。
J
題意:
給 \(n\) 個物品屬性為 \(v_i, c_i, w_i\) 。可以用任意順序堆疊它們。從上往下第 \(i\) 個物品體積會被壓縮為 \(v_i - c_i \times (\sum_{j = 1}^{i - 1} w_j)\) ,詢問這 \(n\) 個物品總共的最小體積。
資料保證物品不會被壓縮成負體積。(哪怕真有這種情況也不影響做法)
題解:
對於尋求最優排列的問題,可以嘗試考慮微擾法。
任選第個 \(i \ (i < n)\) 位的物品,有
考慮僅調換兩個物品的順序,它們的體積之和為
如果第一種情況更優,則應該滿足
排序的微擾法正確性容易構造性證明:
從增量法構造偏序的角度考慮:若可以讓任意相鄰兩個物品滿足偏序,則任意兩個物品滿足偏序。
\(\square\)
存在兩個相鄰物品不具有偏序時,微擾法是失效的。
不難發現 \(\forall i \in [1, n - 1]\) 都和後一個位置滿足偏序,於是按偏序 \(w_i \times c_j \geq w_j \times c_i\) 排序能獲得最優偏序。
Code
int n; std::cin >> n;
std::vector<std::array<i64, 3> > a(n + 1);
for (int i = 1; i <= n; i++) {
i64 w, v, c; std::cin >> w >> v >> c;
a[i] = {w, v, c};
}
std::sort(a.begin() + 1, a.begin() + 1 + n, [&](std::array<i64, 3> A, std::array<i64, 3> B){
// w_1 / c_1 > w_2 / c_2 -> w_1 * c_2 > w_2 * c_1
return A[0] * B[2] > B[0] * A[2];
});
i64 sum = 0, pre = 0;
for (int i = 1; i <= n; i++) {
sum += a[i][1] - a[i][2] * pre;
pre += a[i][0];
}
std::cout << sum << "\n";
時間複雜度 \(O(n \log n)\) 。
I
題意:
給一個十進位制的無符號 \(32\) 位正整數 \(n\) ,詢問 \(n\) 能否構造成多項式:
如果能,輸出一個多項式。
題解:
顯然我們要構造一個相鄰位不能同時為 \(0\) 的多項式。
注意到多項式 \(\sum_{i = 1}^{n} a_i x^{i}\) 若滿足 \(|a_i| < x\) 則一定唯一。(如果有人讀到請自行反證)
由於二進位制與這個多項式的數位存在大量交集,於是從二進位制形態考慮透過微調構造出這個多項式。
顯然:
- \(1 = 2 - 1 = 4 - 2 - 1 = 8 - 4 - 2 - 1 \cdots\)
- \(2 = 4 - 2 = 8 - 4 - 2 = 16 - 8 - 4 - 2 \cdots\)
- 透過歸納法可以證明出 \(2^{i} = 2^{k} - \sum_{j = 1}^{k} 2^{j} , \ \forall k > i\) 。
於是從 \(lowbit(n)\) 開始的所有數位都可以構造成符合條件的情況。當且僅當 \(lowbit(n) > 2\) 時(\(lowbit\) 不在最低兩位上)違反條件。
顯然二進位制一個 \(0\) 如果要變為 \(1\) ,必須從更低位進行補位。而 \(lowbit\) 右邊的任意低位 \(0\) 無位可補,必然導致相鄰 \(0\) 存在。
於是先判斷是否存在多項式,如果存在,則在二進位制上找極大的 \(00 \cdots 1\) 串進行修改,雙指標可以實現。
Code
const int M = 32;
void solve() {
int n; std::cin >> n;
if ((n >> 0 & 1) == 0 && (n >> 1 & 1) == 0) {
std::cout << "NO\n";
return;
}
std::cout << "YES\n";
std::vector<int> ans(32);
int O = __builtin_ctz(n & -n);
for (int i = O, j = O; i < M; i++) {
while (j < i) {
j++;
}
while (j + 1 < M && (n >> j + 1 & 1) == 0) {
j++;
}
if ((n >> j & 1) == 1) {
ans[j] = 1;
continue;
}
// i, j -> 100...0 -> -1 -1 -1 ... 1
ans[j] = 1;
for (int k = i; k < j; k++) {
ans[k] = -1;
}
// std::cout << i << " " << j << "\n";
i = j;
}
for (int i = 0; i < M; i++) {
std::cout << ans[i] << " \n"[(i + 1) % 8 == 0];
}
}
時間複雜度 \(O(Tw)\) 。注意輸出格式為 \(\overline{a_{31}a_{30} \cdots a_{0}}\) 每行空格隔開 \(8\) 個共四行。
副機位剛啟動,剛好點開了這題,然後三分鐘內確定了正確做法。但隊友選擇了先跟榜寫掉簽到題。由於簽到都被隊友秒了所以拖到第三道題才寫。
L
題意:
開局在 \([1, n]\) 隨機一個 \(t\) ,每一秒執行一次以下兩個操作之一
- 待機,\(t\) 會自然減少 \(1\) 。
- 讓 \(t\) 在 \([1, n]\) 重新隨機。
詢問使用最優操作讓 \(t\) 到 \(0\) 的最小時間期望,答案輸出最簡分數下的分子和分母。
題解:
設遊戲開始到遊戲結束的最優期望時間是 \(E(X)\) 。
根據期望的線性性 \(E(X) = \sum_{i = 1}^{n} E(Y_i)\) (第 \(i\) 個點到 \(0\) 的期望)。
去考慮 \(i \in [1, n]\) 上每個點的期望。
- 要麼是直接花費 \(i\) 秒走到 \(0\) ,時間貢獻是 \(i\) 。
- 要麼是使用隨機,對時間貢獻是 \(1 + E(X)\) 。
考慮期望為“隨機變數的取值”乘以“出現這個取值的機率”,\(E(Y_i) = \frac{1}{n} \mathbf{min}(i, 1 + E(X))\) 。
於是有
由於 \(i\) 單調,\(1 + E(X)\) 為常數,一定存在一個 \(v\) 使得
不難證明(比如反證)一個寬鬆邊界是 \(v \in [1, n]\) ,於是
若 \(E(X)\) 為最優則一定最小,顯然 \(E(X)\) 在 \(v\) 的定義域上是對勾函式,於是在 \((0, +\infty]\) 存在極小值。
這時候可以掂量一下是自己求導求極值快還是寫三分快。問題可以解決。
如果求導則有
在 \(\sqrt{2n}\) 的上下取整兩個點進行函式比較即可得到極小值。
Code
i64 gcd(i64 a, i64 b) { return b ? gcd(b, a % b) : a; }
void solve() {
i64 n; std::cin >> n;
// E(X) = \frac{v - 1}{2} + \frac{n}{v} -> \sqrt{2v}
i64 v1 = std::sqrt(2 * n), v2 = std::sqrt(2 * n) + 1;
// std::cout << "v " << v1 << " " << v2 << "\n";
auto calc = [&] (i64 v) -> std::array<i64, 2> {
i64 O = v * v - v + 2LL * n, P = 2LL * v;
i64 g = gcd(O, P);
return {O / g, P / g};
};
std::array<i64, 2> F1 = calc(v1), F2 = calc(v2);
if (F1[0] * F2[1] < F2[0] * F1[1]) {
std::cout << F1[0] << " " << F1[1] << "\n";
} else {
std::cout << F2[0] << " " << F2[1] << "\n";
}
}
時間複雜度 \(O(\log n)\) ,需要求 \(O(1)\) 次 \(gcd\) 。如果三分只會增加一個 \(O(\log n)\) 。
最後說說我隊如何唐掉的 \(L\) ,隊友先猜了一發只待機是最優選擇,當時看榜上過了七十隊,重新整理一下直接過了一百多隊,於是示意衝了一發貪心,結果是錯了。
後續我們大概當機了半個小時,才意識到期望的和等於和的期望 + 每個位置在遊戲開始時的地位是等價的這件事情。然後搞半天發現有人讀錯題,再然後重讀題把公式改錯了,於是卡了幾乎兩個小時。
G
題意:
一局遊戲,\(Alice\) 開局有 \(x\) 個籌碼,\(Bob\) 開局有 \(y\) 個籌碼。 \(Alice\) 贏的機率是 \(p_0\) ,\(Bob\) 贏的機率是 \(p_1\) ,平局的機率是 \(1 - p_0 - p_1\) 。
- 如果有人贏了一局,則敗者會減少勝者的籌碼數量。若敗者籌碼數量因此 \(\leq 0\) ,這輪的勝者直接獲得整局勝利。否則遊戲立刻進入下一輪。
- 如果平局,遊戲立刻進入下一輪。
給出 \(a_0, a_1, b\) 以表示 \(p_0 = \frac{a_0}{b}, p_1 = \frac{a_1}{b}\) 。
詢問 \(Alice\) 能獲得全域性勝利的機率,答案模 \(998244353\) 。
題解:
因為平局會導致遊戲直接進入下一輪,所以平局實際上是一個狀態的自環。因為只考慮獲勝的後繼和失敗的後繼,所以自環是無效狀態。
於是重定義勝敗的機率,\(p_0 = \frac{a_0}{a_0 + a_1}\) \(p_1 = \frac{a_1}{a_0 + a_1}\) 。
分析狀態:
- 若 \(x = y\) ,有 \(p_0\) 的機率 \(Alice\) 轉移向勝態,\(p_1\) 的機率 \(Bob\) 轉移向勝態。
- \(Alice\) 獲得全域性勝利的機率為,到達當前局面的機率 \(cur\) 乘以 \(p_0\) 。
- 若 \(x < y\) ,\(Alice\) 必須連贏後續 \(d = \lceil \frac{y - x}{x} \rceil\) 輪才能使遊戲繼續。遊戲繼續的機率為 \(p_{0}^{d}\) ,\(y := y - d x\) 。
- 若 \(x > y\) ,\(Alice\) 必須連輸後續 \(d = \lceil \frac{x - y}{y} \rceil\) 輪才能使遊戲繼續。遊戲繼續的機率為 \(p_{1}^{d}\) ,\(x := x - d y\) 。
- 但凡 \(Alice\) 沒有連輸 \(d\) 次,都能獲得全域性勝利並終止遊戲。\(Alice\) 在後續連續 \(d\) 次中存在勝利的機率,可以方便地用容斥計算,為 \(1\) 減去 \(Alice\) 達到當前狀態的機率乘以連輸 \(d\) 次的機率,這個機率為到達當前局面的機率 \(cur\) 乘以 \(p_1^{d}\) 。
嘗試暴力搜尋這些狀態,按輪次的不獨立性以乘法原理維護一個搜尋樹字首的機率。
不難分析出每個狀態要麼 \(x\) 至少減半,要麼 \(y\) 至少減半。最多會有 \(2 \log n\) 個狀態。於是複雜度是對的。
或者可以注意到
- \(y := y - dx \Leftrightarrow y := y \bmod x\) 。
- \(x := x - dy \Leftrightarrow x := x \bmod y\) 。
這個狀態遞降速度等於輾轉相除的遞降速度,也可以認為狀態個數是 \(O(\log n)\) 的。
Code
const int MOD = 998244353;
i64 ksm(i64 a, i64 n) {
i64 res = 1; a = (a + MOD) % MOD;
for(;n;n>>=1,a=a*a%MOD)if(n&1)res=res*a%MOD;
return res;
}
void solve() {
i64 x, y; std::cin >> x >> y;
i64 a0, a1, b; std::cin >> a0 >> a1 >> b;
b = a0 + a1;
i64 p0 = a0 * ksm(b, MOD - 2) % MOD;
i64 p1 = a1 * ksm(b, MOD - 2) % MOD;
i64 ans = 0;
std::function<void(i64, i64, i64)> dfs = [&] (i64 cx, i64 cy, i64 cur) {
// std::cout << cx << " " << cy << "\n";
if (cx == cy) {
ans = (ans + cur * p0) % MOD;
return;
} else if (cx < cy) {
i64 d = (cy + cx - 1) / cx - 1; // ceil( (cy - cx) / cx )
dfs(cx, cy - d * cx, cur * ksm(p0, d)) % MOD;);
} else { // cx > cy
i64 d = (cx + cy - 1) / cy - 1; // ceil( (cx - cy) / cy )
ans = ( ans + cur * (1 - ksm(p1, d) + MOD) ) % MOD;
dfs(cx - d * cy, cy, (cur * ksm(p1, d)) % MOD;);
}
};
dfs(x, y, 1);
std::cout << ans << "\n";
}
E
後面三題明天再寫。