2024 ICPC 網路賽 第 2 場

zsxuan發表於2024-09-23

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)\) 個。

  1. 這個隊伍如果在這些強隊中,只需 \(lower\_bound\) 出第一個不弱於它的隊伍的位置 \(p\) ,這個隊伍一定是它,於是它在 \(p\) 這個位置。
  2. 這個隊伍如果不在這些強隊中,考慮 \(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)\) 位的物品,有

\[\left ( v_{i} - c_{i} \times pre \right ) + \left ( v_{i + 1} - c_{i + 1} \times (pre + w_{i}) \right ) \]

考慮僅調換兩個物品的順序,它們的體積之和為

\[\left ( v_{i + 1} - c_{i + 1} \times pre \right ) + \left ( v_{i} - c_{i} \times (pre + w_{i + 1}) \right ) \]

如果第一種情況更優,則應該滿足

\[\begin{aligned} \left ( v_{i} - c_{i} \times pre \right ) + \left ( v_{i + 1} - c_{i + 1} \times (pre + w_{i}) \right ) &\leq \left ( v_{i + 1} - c_{i + 1} \times pre \right ) + \left ( v_{i} - c_{i} \times (pre + w_{i + 1}) \right ) \\ v_i + v_j - (c_i + c_j) \times pre - c_j \times w_i &\leq v_i + v_j - (c_i + c_j) \times pre - c_i \times w_j \\ w_i \times c_j &\geq w_j \times c_i \end{aligned} \]

排序的微擾法正確性容易構造性證明:

從增量法構造偏序的角度考慮:若可以讓任意相鄰兩個物品滿足偏序,則任意兩個物品滿足偏序。

\(\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\) 能否構造成多項式:

\[\left \{ \begin{aligned} &n = \sum_{i = 0}^{31} a_i 2^{i} \\ &a_i \in \{-1, 0, 1\} \\ &\forall i \in [0, 30], a_i^{2} + a_{i + 1}^{2} \neq 0 \\ \end{aligned} \right . \]

如果能,輸出一個多項式。

題解:

顯然我們要構造一個相鄰位不能同時為 \(0\) 的多項式。

注意到多項式 \(\sum_{i = 1}^{n} a_i x^{i}\) 若滿足 \(|a_i| < x\) 則一定唯一。(如果有人讀到請自行反證)

由於二進位制與這個多項式的數位存在大量交集,於是從二進位制形態考慮透過微調構造出這個多項式。

顯然:

  1. \(1 = 2 - 1 = 4 - 2 - 1 = 8 - 4 - 2 - 1 \cdots\)
  2. \(2 = 4 - 2 = 8 - 4 - 2 = 16 - 8 - 4 - 2 \cdots\)
  3. 透過歸納法可以證明出 \(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]\) 上每個點的期望。

  1. 要麼是直接花費 \(i\) 秒走到 \(0\) ,時間貢獻是 \(i\)
  2. 要麼是使用隨機,對時間貢獻是 \(1 + E(X)\)

考慮期望為“隨機變數的取值”乘以“出現這個取值的機率”,\(E(Y_i) = \frac{1}{n} \mathbf{min}(i, 1 + E(X))\)

於是有

\[\begin{aligned} &E(X) = \sum_{i = 1}^{n} E(Y_i) = \frac{1}{n} \mathbf{min}(i, 1 + E(X)) \\ &n E(X) = E(Y_i) = \frac{1}{n} \mathbf{min}(i, 1 + E(X)) \\ \end{aligned} \]

由於 \(i\) 單調,\(1 + E(X)\) 為常數,一定存在一個 \(v\) 使得

\[\begin{aligned} &E(Y_i) = i, &i \leq v \\ &E(Y_i) = 1 + E(X), &i > v \\ \end{aligned} \]

不難證明(比如反證)一個寬鬆邊界是 \(v \in [1, n]\) ,於是

\[\begin{aligned} &n E(X) = \sum_{i = 1}^{v} i + (n - v)(1 + E(X)) \\ &n E(X) = \frac{v (v + 1)}{2} + n - v + n E(X) - v E(x) \\ &E(X) = \frac{v + 1}{2} + \frac{n}{v} - 1 = \frac{v - 1}{2} + \frac{n}{v} \\ \end{aligned} \]

\(E(X)\) 為最優則一定最小,顯然 \(E(X)\)\(v\) 的定義域上是對勾函式,於是在 \((0, +\infty]\) 存在極小值。

這時候可以掂量一下是自己求導求極值快還是寫三分快。問題可以解決。

如果求導則有

\[\begin{aligned} F(v) &= \frac{v - 1}{2} + \frac{n}{v} \\ \frac{d}{d v} F(v) &= \frac{1}{2} - n v^{-2} = 0 \\ v^{2} - 2 n &= 0 \\ v &= \sqrt{2 n} \\ \end{aligned} \]

\(\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\)

  1. 如果有人贏了一局,則敗者會減少勝者的籌碼數量。若敗者籌碼數量因此 \(\leq 0\) ,這輪的勝者直接獲得整局勝利。否則遊戲立刻進入下一輪。
  2. 如果平局,遊戲立刻進入下一輪。

給出 \(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}\)

分析狀態:

  1. \(x = y\) ,有 \(p_0\) 的機率 \(Alice\) 轉移向勝態,\(p_1\) 的機率 \(Bob\) 轉移向勝態。
    • \(Alice\) 獲得全域性勝利的機率為,到達當前局面的機率 \(cur\) 乘以 \(p_0\)
  2. \(x < y\)\(Alice\) 必須連贏後續 \(d = \lceil \frac{y - x}{x} \rceil\) 輪才能使遊戲繼續。遊戲繼續的機率為 \(p_{0}^{d}\)\(y := y - d x\)
  3. \(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\) 個狀態。於是複雜度是對的。

或者可以注意到

  1. \(y := y - dx \Leftrightarrow y := y \bmod x\)
  2. \(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

後面三題明天再寫。

C

K

相關文章