CF2000 A~C 題解

DengStar發表於2024-08-21

Codeforces Round 967 (Div. 2) A~C 題解(未完待續)

唐完了,B 構造不會,C 互動不會,整場爆切 \(1\) 題喜提 \(-115\) rating 強勢上灰!我還會什麼?

I. 2001A - Make All Equal

先找出答案的下界。設眾數為 \(x\),其出現的次數為 \(\operatorname{cnt}(x)\)。由於每次操作只能刪除一個數,答案不可能小於 \(n - \operatorname{cnt}(x)\)

下面證明這個下界是可以達到的。用 \(y\) 代表所有的非眾數,那麼在非眾數被刪光之前,序列可以表示成 \(x \dots x, y \dots y, \dots\) 之類 \(x\)\(y\) 的連續段交替出現的形式。只需證明一定存在一對相鄰元素(這裡的相鄰指環上的相鄰)滿足前者小於後者即可。

反證法,如果不存在,則 \(x > y > \cdots\)。由於序列是一個環,所以可以匯出 \(x > x\),這是不可能的,故得證。

程式碼

II. 2001B - Generate Permutation

我是傻逼,我沒做出來這題

先考察滿足要求的序列有什麼性質。可以發現(事實上,我並沒有發現),對於從左往右的打字員來說,假設他已經打出了 \(x\),要打 \(x + 1\),只有 \(\operatorname{pos}_x > \operatorname{pos}_{x+1}\) 時他才要按一次回車(其中 \(\operatorname{pos}_x\) 表示 \(x\) 的下標)。因此他按回車的次數 \(c_1 = \sum_{i=1}^{n-1} [\operatorname{pos}_x > \operatorname{pos}_{x+1}]\)

(這裡應該模擬一下打字的過程才能更順理成章地發現這個結論:假設我已經打出了 \(x\),要打 \(x + 1\),我能直接移動然後打出來,還是必須得回車?顯然,只有 \(x + 1\)\(x\) 前面時才要回車,這就對應了 \(\operatorname{pos}_x > \operatorname{pos}_{x+1}\)。這樣想的話,這個結論並不難得出,但是我為什麼沒想到呢?)

同樣的,對於從右往左的打字員,只有 \(\operatorname{pos}_x < \operatorname{pos}_{x+1}\) 時他才要按一下回車,因此他按回車的次數 \(c_2 = \sum_{i=1}^{n-1} [\operatorname{pos}_x < \operatorname{pos}_{x+1}]\)

合法的序列應該滿足 \(c_1 = c_2\)。顯然,對於 \(x\)\(x + 1\),要麼 \(\operatorname{pos}_x > \operatorname{pos}_{x+1}\) 成立,要麼 \(\operatorname{pos}_x < \operatorname{pos}_{x+1}\) 成立,所以 \(c_1 + c_2 = n - 1\),因此 \(c_1 = c_2 = \dfrac{n-1}{2}\)。這就說明 \(n\) 是偶數時一定沒有合法方案。

\(n\) 是奇數時,可以手模出許多合法的方案。以 \(n = 7\) 為例,一種好想的思路是:先用一堆降序的數把 \(c_1\) 給佔滿,比如 \(7, 6, 5, 4\)。那麼剩下的數必須是升序的,否則會讓 \(c_1\) 變大。直接填進去即可: \(1, 2, 3, 7, 6, 5, 4\) 就是可行的。(你會發現 \(7, 6, 5, 4, 1, 2, 3\) 是不可行的,因為 \((3, 4)\) 這一對讓 \(c_1\) 變大了)。

因此,\(n\) 是奇數時, \({\color{blue}1, 2, \dots, \left\lfloor \dfrac{n-1}{2} \right\rfloor,} {\color{red}n, n-1, \dots, \left\lfloor \dfrac{n+1}{2} \right\rfloor}\) 是一個合法的方案。這裡用不同顏色區分構造時分別填入的兩段。

void solve()
{
	int n;
	cin >> n;
	if((n & 1) == 0)
	{
		cout << -1 << endl;
		return;
	}
	
	vector<int> ans(n);
	iota(ans.begin(), ans.end(), 1);
	reverse(ans.begin() + (n/2), ans.end());
	for(int x: ans) cout << x << ' ';
	cout << endl;
}

程式碼

III. 2001C - Guess The Tree

下面是官方題解的思路。我自己想不出來這個做法,所以不知道用什麼語言來引入。

把點劃分為兩個點集 \(A\)\(B\)\(A\) 中的點是知道其內部邊集的點,\(B\) 中的點是不知道的。

初始時,任意把一個點加入 \(A\) 中(例如 \(1\)),\(B\) 是剩下的 \(n-1\) 個點。

我們的策略是:每次把一個新的點加入到 \(A\) 中。這個點必須和原來 \(A\) 中的某個點有連邊,否則根本無法把它加入 \(A\)

任選點 \(u \in A\)\(v \in B\),查詢 \(u, v\) 的中點 \(x\)。如果 \(x \in A\),則令 \(u \gets x\),否則令 \(v \gets x\)。這裡的要義是,時刻保證 \(u \in A\)\(v \in B\)。最終當 \(x = u\) 時,就表明 \(u\)\(v\) 之間有邊相連,可以更新答案的邊集。由於我們保證了 \(v \in B\),所以可以把 \(v\) 加入到 \(A\) 中,並在 \(B\) 中刪掉 \(v\)

這裡的查詢類似二分,每次找到一個新點的查詢次數不會超過 \(\log n\),總次數不超過 \(n \log n\),可以透過此題。

但是我不知道我自己該如何想出這些東西