Codeforces Round 942 Div.2 題解

ImALAS發表於2024-05-01

蹭個熱度,挽救一下 cnblogs 蒸蒸日上的閱讀量。

Q: 你是手速狗嗎?

A: 我覺得我是。

2A

因為選的 \(w\) 一定可以讓它合法,一次操作可以看作 \(a\) 陣列向右平移一位。列舉操作次數後暴力判斷即可。

#include <bits/stdc++.h>

void work() {
	int n;
	std::cin >> n;
	std::vector<int> a(n + 1, 0), b(n + 1, 0);
	for (int i = 1; i <= n; ++i)
		std::cin >> a[i];
	for (int i = 1; i <= n; ++i)
		std::cin >> b[i];
	for (int x = 0; x <= n; ++x) {
		bool flg = true;
		for (int i = 1; i <= n - x; ++i)
			flg &= a[i] <= b[i + x];
		if (flg) return std::cout << x << '\n', void();
	}
	return;
}

int main() {
	std::cin.tie(nullptr)->sync_with_stdio(false);
	int t; std::cin >> t;
	while (t--) work();
	return 0;
}

2B

U 視作 \(1\),將 D 視作 \(0\),一次操作就是拿走一個 \(1\),將兩側分別異或 \(1\)

觀察一下樣例,盲猜 U 有奇數個先手贏,反之後手贏。來理解一下。

根據上述轉化,判定條件等價於判斷全域性異或和為 \(0\) 還是為 \(1\)。一次操作對全域性異或和的影響是,將其異或上 \(1\)

初始異或和為 \(1\) 時,先手 必定 可以操作,到後手時異或和為 \(0\),後手 可能 可以操作,回到先手,異或和還是 \(1\)必定 可以操作。如此往復,只要後手能操作,先手必定可以操作。

另一種情況類似。

感覺這個題挺有意思的,突破點在於對一次操作影響的觀察。

#include <bits/stdc++.h>

void work() {
	int n;
	std::cin >> n;
	std::string s;
	std::cin >> s;
	int cnt = 0;
	for (int i = 0; i < n; ++i)
		if (s[i] == 'U') ++cnt;
	if (!cnt) return std::cout << "NO\n", void();
	if (cnt & 1) {
		std::cout << "YES\n";
	} else {
		std::cout << "NO\n";
	}
	return;
}

int main() {
	std::cin.tie(nullptr)->sync_with_stdio(false);
	int t; std::cin >> t;
	while (t--) work();
	return 0;
}

2C / 1A

順著自己的感覺走,我們應該會想到儘量讓 \(a\) “平均”,也就是 \(k\) 優先給小的分配。

畫一畫會發現最 “緊湊” 的結構是 \(1,2,3,4,1,2,3,4\) 這樣的模式,直覺上很對,因為 “利用率” 最高。

將貢獻寫出來,假設最小值為 \(x\),比 \(x\) 大的有 \(y\) 個,我們一定會把這 \(y\) 個儘量往前放,讓較小的 \(x\) 利用充分,這樣總共有 \((x-1)n+1+y\) 個合法子段。

然後就是模擬,把 \(k\) 儘量往小的分。差分維護即可。

時間複雜度 \(\mathcal O(n\log n)\),瓶頸在排序。

#include <bits/stdc++.h>

using i64 = long long;

void work() {
	int n;
	i64 k;
	std::cin >> n >> k;
	std::vector<i64> a(n, 0);
	for (int i = 0; i < n; ++i)
		std::cin >> a[i];
	std::sort(a.begin(), a.end());
	std::vector<i64> dlt(n, 0);
	for (int i = 0; i < n - 1; ++i) {
		if (!k) break;
		i64 x = 1ll * (i + 1) * (a[i + 1] - a[i]);
		if (k >= x) {
			dlt[i] += a[i + 1] - a[i];
			k -= x;
		} else {
			i64 d = k / (i64)(i + 1);
			for (int j = 0; j <= i; ++j)
				a[j] += d;
			k -= 1ll * (i + 1) * d;
			for (int j = i; ~j; --j)
				if (k) ++a[j], --k;
			break;
		}
	}
	i64 sum = 0;
	for (int i = n - 1; ~i; --i)
		sum += dlt[i], a[i] += sum;
	i64 d = k / (i64)n;
	k -= 1ll * n * d;
	for (int i = n - 1; ~i; --i)
		a[i] += d;
	for (int i = n - 1; ~i; --i)
		if (k) ++a[i], --k;
	i64 res = 0;
	for (int i = n - 1; ~i; --i)
		if (a[i] > a[0]) ++res;
	std::cout << 1ll * a[0] * n - n + 1 + res << '\n';
	return;
}

int main() {
	std::cin.tie(nullptr)->sync_with_stdio(false);
	int t; std::cin >> t;
	while (t--) work();
	return 0;
}

2D / 1B

  • D1

列舉 \(d=\gcd(a,b)\),令 \(a\gets a/d,b\gets b/d\),我們要求 \(\gcd(a,b)=1,bd|(a+b)\)

要滿足這個限制,必然有 \(a=(kd-1)b\),又因為 \(\gcd(a,b)=1\),所以 \(b=1\)

於是列舉 \(d,a\),判斷是否有 \(d|(a+1)\) 即可。根據調和級數的經典結論,時間複雜度 \(\mathcal O(\sum n\ln n)\)。可以更快但場上寫這個方便。

#include <bits/stdc++.h>

using i64 = long long;

void work() {
	int n, m;
	std::cin >> n >> m;
	i64 ans = 0;
	for (int d = 1; d <= n && d <= m; ++d) {
		for (int a = 1; a * d <= n; ++a)
			if ((a + 1) % d == 0) ++ans;
	}
	std::cout << ans << '\n';
	return;
}

int main() {
	std::cin.tie(nullptr)->sync_with_stdio(false);
	int t; std::cin >> t;
	while (t--) work();
	return 0;
}
  • D2

前排提示:非正解。

按照劇本,繼續列舉 \(d=\gcd(a,b),a\gets a/d,b\gets b/d\),要有 \((a+b)|bd\)

因為此時 \(\gcd(a,b)=1\),所以 \((a+b)\not| \ b\),於是 \((a+b)|d\)

列舉 \(d\) 的因子 \(x\),再列舉 \(b\),令 \(a=x-b\),判一下是否有 \(\gcd(a,b)=1\)\(a,b\) 都滿足邊界限制即可。

場上我只是打了個這個程式碼來檢驗正確性,測了一下樣例發現跑得飛快,\(n,m\) 取滿也只有 1e7 左右個合法數對,0.5s 就能跑出來,交上去直接 pp 了,也沒 fst。

正解好像挺智慧,不太會。

#include <bits/stdc++.h>

using i64 = long long;

int gcd(int x, int y) { return y ? gcd(y, x % y) : x; }

void work() {
	int n, m;
	std::cin >> n >> m;
	int ans = 0;
	for (int d = 1; d <= n && d <= m; ++d) {
		int lmt = (n / d) + (m / d);
		for (int x = 2; x <= d && x <= lmt; ++x) {
			if (d % x) continue ;
			for (int b = 1; b < x && b * d <= m; ++b) {
				int a = x - b;
				if (a >= 1 && a * d <= n && gcd(a, b) == 1)
					++ans;
			}
		}
	}
	std::cout << ans << '\n';
	return;
}

int main() {
	std::cin.tie(nullptr)->sync_with_stdio(false);
	int t; std::cin >> t;
	while (t--) work();
	return 0;
}

2E / 1C

有一種 CNOI 模擬賽的美。

首先發現增量構造非常對,因為 \(a_i\)\(i\) 這個節點的貢獻無論多少輪都是 \(a_i\),於是只要確定了 \(a_{1\sim i-1}\) 就可以直接知道 \(a_i\) 是啥。

此時我們需要知道 \(a_{1\sim i-1}\) 做了 \(k\) 輪後對 \(i\) 的貢獻,顯然對於 \(j\in [1,i-1]\)\(a_j\) 的貢獻係數是獨立的。

假設樹狀陣列上 \(j\)\(i\)\(n\) 個點,做 \(k\) 輪這個過程的貢獻係數是 \(\dbinom{n+k-2}{k-1}\)

這個係數咋看出來?我不太會直接推導,只能講講咋找規律。手摸幾次操作應該會看出來和二項式係數有關係,每行前面 1, 2 較簡單的可以直接寫成二項式係數,其餘項根據 \(s_i=s_{i-1}+a_i\) 的公式,聯想一下遞推公式 \(\dbinom{n}{m}=\dbinom{n-1}{m-1}+\dbinom{n-1}{m}\) 湊一下就行。

正常的方法是組合推導,或者拉格朗日插值。

回到正文,得到貢獻係數以後,根據吸收恆等式 \(\dbinom{n}{m}=\dfrac nm \dbinom{n-1}{m-1}\) 在樹狀陣列上計算即可。\(\mathcal O(n\log n)\)

#include <bits/stdc++.h>
#define pb emplace_back
#define fir first
#define sec second

using i64 = long long;
using pii = std::pair<int, int>;

constexpr int mod = 998244353;
constexpr int maxn = 2e5 + 5;
void Add(int& x, int y) { if ((x += y) >= mod) x -= mod; return ; }
void sub(int& x, int y) { if ((x -= y) < 0) x += mod; return ; }
int n, k, a[maxn], inv[maxn];

struct Fenwick {
	int c[maxn];
	Fenwick() { memset(c, 0, sizeof(c)); }
	void clr(int n) { std::fill(c, c + 1 + n, 0); return; }
	void add(int x, int y) {
		int coef = 1, cnt = 1;
		for (; x <= n; x += x & -x) {
			Add(c[x], 1ll * y * coef % mod);
			++cnt;
			coef = 1ll * coef * (cnt + k - 2) % mod * inv[cnt - 1] % mod;
		}
		return;
	} 
} tr;

void init(int n) {
	inv[1] = 1;
	for (int i = 2; i <= n; ++i)
		inv[i] = 1ll * (mod - mod / i) * inv[mod % i] % mod;
	return;
}

void work() {
	std::cin >> n >> k;
	for (int i = 1; i <= n; ++i)
		std::cin >> a[i];
	tr.clr(n);
	for (int i = 1; i <= n; ++i) {
		int x = a[i];
		sub(x, tr.c[i]);
		tr.add(i, x);
		std::cout << x << ' ';
	}
	std::cout << '\n';
	return;
}

int main() {
	init(maxn - 1);
	std::cin.tie(nullptr)->sync_with_stdio(false);
	int t;
	std::cin >> t;
	while (t--) work();
	return 0;
}

2F / 1D

被爆了 /kk。

直接做是很困難的,考慮二分答案 \(mid\),每個位置的操作都是獨立的,連邊 \(i\to b_i\) 得到一個基環樹森林,\(a_i\) 能變成的數就是 \(a_i\) 在基環樹上走 \(\le mid\) 步能到達的點。

此時有一個顯然的貪心:對於 \(i\),選一個 \(\ge a_{i-1}\) 的,\(a_i\) 能到達的最小的 \(x\)\(a_i\gets x\)

涉及樹鏈上點權前驅,主席樹最佳化一下可以 log^2,但是 N_z 表示專門卡掉了這個演算法。

轉換視角:我們不再讓 \(a_i\) 去找他能到達的點的前驅,而是去嘗試將一個數填入 \(a_i\)。其實是一個 two-pointers 的 trick。

具體地,令 \(j=1\),順序遍歷,假設當前遍歷到 \(a_i\),如果 \(a_i\) 能透過 \(\le mid\) 步到達 \(j\),則 \(a_i\gets j\),反之 \(j\gets j+1\),如果 \(j>m\) 則無解。

判斷 \(a_i\) 是否能透過 \(\le mid\) 步到達 \(j\) 是簡單的,時間複雜度 \(\mathcal O((n+m)\log m)\),有點 dirty work,不想碰基環樹。

luanmenglei 大神說的很有道理:這題的難點在於你不把它當成一個大 DS 題。

#include <bits/stdc++.h>
#define pb emplace_back
#define fir first
#define sec second

using i64 = long long;
using pii = std::pair<int, int>;

constexpr int maxn = 1e6 + 5;
int n, m, r, rt[maxn], a[maxn], b[maxn], dfc, dfn[maxn], siz[maxn], bel[maxn], cnt, onc[maxn], dep[maxn], sic[maxn];
bool vis[maxn];
std::vector<int> G[maxn];

void dfs(int u, int ff) {
	dep[u] = dep[ff] + 1;
	dfn[u] = ++dfc;
	siz[u] = 1;
	rt[u] = r;
	for (auto& v : G[u]) {
		if (onc[v] || dfn[v] || v == ff) continue ;
		dfs(v, u);
		siz[u] += siz[v];
	}
	return;
}

bool check(int L) {
	auto cov = [&](int x, int y) {
		return dfn[x] <= dfn[y] && dfn[y] < dfn[x] + siz[x];
	};
	auto myabs = [&](int x) {
		return x > 0 ? x : -x;
	};
	auto chk = [&](int x, int y) {
		if (bel[x] != bel[y]) return false;
		if (onc[x] && onc[y]) {
			int d = (sic[bel[x]] + onc[x] - onc[y]) % sic[bel[x]];
			return d <= L;
		} else if (onc[y]) {
			int d = dep[x] - 1;
			x = rt[x];
			d += (sic[bel[x]] + onc[x] - onc[y]) % sic[bel[x]];
			return d <= L;
		} else if (onc[x]) {
			return false;
		} else {
			return cov(y, x) && dep[x] - dep[y] <= L;
		}
		return true;
	};
	for (int i = 1, j = 1; i <= n; ++i) {
		for (; j <= m; ++j) 
			if (chk(a[i], j)) break;
		if (j > m) return false;
	}
	return true;
}

void work() {
	std::cin >> n >> m;
	for (int i = 1; i <= n; ++i)
		std::cin >> a[i];
	for (int i = 1; i <= m; ++i)
		G[i].clear();
	for (int i = 1; i <= m; ++i) {
		std::cin >> b[i];
		G[b[i]].pb(i);
	}
	std::fill(vis + 1, vis + 1 + m, false);
	std::fill(onc + 1, onc + 1 + m, 0);
	std::fill(dep + 1, dep + 1 + m, 0);
	std::fill(bel + 1, bel + 1 + m, 0);
	std::fill(sic + 1, sic + 1 + m, 0);
	for (int i = 1; i <= m; ++i) {
		if (vis[i]) continue ;
		std::vector<int> pth;
		for (int x = i; !vis[x]; x = b[x])
			pth.pb(x), vis[x] = true;
		int x = pth.back();
		if (bel[b[x]]) {
			for (auto& v : pth)
				bel[v] = bel[b[x]];
			continue; 
		}
		std::vector<int> cyc;
		x = b[x];
		while (pth.back() != x) cyc.pb(pth.back()), pth.pop_back();
		pth.pop_back();
		cyc.pb(x);
		std::reverse(pth.begin(), pth.end());
		++cnt;
		sic[cnt] = cyc.size();
		for (int i = 0; i < cyc.size(); ++i)
			onc[cyc[i]] = i + 1, bel[cyc[i]] = cnt;
		for (auto& v : pth)
			bel[v] = cnt;
	}
	std::fill(siz + 1, siz + 1 + m, 0);
	std::fill(dfn + 1, dfn + 1 + m, 0);
	std::fill(rt + 1, rt + 1 + m, 0);
	for (int i = 1; i <= m; ++i)
		assert(bel[i] > 0);
	dfc = 0;
	for (int i = 1; i <= m; ++i)
		if (onc[i]) r = i, dfs(i, 0);
	int l = 0, r = m;
	while (l <= r) {
		int mid = (l + r) >> 1;
		if (check(mid)) {
			r = mid - 1;
		} else {
			l = mid + 1;
		}
	}
	std::cout << (check(l) ? l : -1) << '\n';
	return;
}

int main() {
	std::cin.tie(nullptr)->sync_with_stdio(false);
	int t; std::cin >> t;
	while (t--) work();
	return 0;
}

1E

不會格路計數,不會反射容斥 /chandou。

相關文章