本场比赛的过题情况非常不好。这场没有什么高深的知识点,都是很基础的题目,希望大家能够好好补题。

暑假已经过半,希望各位能继续努力。

A - CF115A

员工 $i$ 的主管是 $p_i$,员工不能和自己的上司(上司的主管也是上司)同组,求最小组数。

分析

可以将属于同一个上级的员工看作一棵树,由题意得一颗树上不同深度的人不能放在一起,因此答案为树的最大深度。

int main() {
	int n;
	cin >> n;
	vector<int> v(n + 1);
	for (int i = 1; i <= n; i++) {
		cin >> v[i];
	}
	std::function<int(int)> dfs = [&](int u) {
		return v[u] == -1 ? 1 : dfs(v[u]) + 1;
	};
	int ans = 0;
	for (int i = 1; i <= n; i++) {
		ans = max(ans, dfs(i));
	}
	cout << ans;
	return 0;
}

B - ABC248C

求满足条件

$$ \sum_{i=1}^n a_i \leqslant m $$

的长 $n$ 的整数序列 $\{a_i\}$ 的个数,限制 $1 \leqslant a_i \leqslant k$。

分析

非常裸的背包题。设 $dp(i, j)$ 表示长度为 $i$ 和为 $j$ 数量。不难推出转移方程为

$$ dp(i, j)= \sum_{x=1}^{\min(j, k)} dp(i-1, j) + dp(i-1, j - x) $$

using Z = mint<998244353>;

int main() {
	int n, m, k;
	cin >> n >> m >> k;
	vector<Z> dp(k + 1);
	dp[0] = 1;
	for (int i = 0; i < n; i++) {
		vector<Z> ndp(k + 1);
		for (int j = 1; j <= m; j++) {
			for (int u = k; u >= j; u--) {
				ndp[u] += dp[u - j];
			}
		}
		dp = ndp;
	}
	Z sum = 0;
	for (int i = n; i <= k; i++) {
		sum += dp[i];
	}
	cout << sum << '\n';
	return 0;
}

C -CF484A

寻找区间 $[l,r]$ 中 $\operatorname{popcount}$ 最大,且值最小的数。

分析

小清新构造。

如果 $\operatorname{popcount}$ 相同,那么最小的数就是把所有的 $1$ 挤到最低位。

因此我们可以从最低位开始,把 $1$ 添上去。

int main() {
	int T;
	cin >> T;
	while (T--) {
		ll l, r;
		cin >> l >> r;
		ll ans = l;
		for (int i = 0; l <= r; i++) {
			ans = l;
			l |= 1ll << i;
		}
		cout << ans << '\n';
	}
	return 0;
}

D - CF191C

给定一个长为 $n$ 的序列 $\{a_i\}$,找到序列中最大的

$$ a_i \bmod a_j $$

并且满足 $a_i \geqslant a_j$。

分析

由于取模的定义,若 $x \bmod y = r$,那么一定有

$$ x = py + r $$

因此我们可以枚举 $y$ 的倍数 $py$,寻找 $(p-1)y$ 到 $py$ 中的最大值。

具体的说,我们可以枚举每个 $a_j$ 的倍数 $p a_j$,并找到最接近 $p a_j$ 的数 $x$,计算 $x \bmod a_j$。

对于重复的 $a_i$,我们可以开一个桶,使得每个数只被枚举一次。

注意到这样的复杂度是调和级数的,也就是 $O(n \log n)$。

const int N = 2E6 + 5;

int main() {
	int n;
	cin >> n;
	vector<int> vis(N);
	vector<int> v(n), mi(N);
	for (int &vi : v) {
		cin >> vi;
		vis[vi] = true;
	}
	for (int i = 1; i < N; i++) {
		mi[i] = vis[i] ? i : mi[i - 1];
	}
	int ans = 0;
	for (int i = 2; i < N; i++) {
		if (vis[i]) {
			for (int j = i * 2; j < N; j += i) {
				ans = max(ans, mi[j - 1] % i);
			}
		}
	}
	cout << ans << '\n';
	return 0;
}

E - ARC84B

求 $k$ 的正整数中各个数位之和最小值。

分析

图论题可能题面里没有图。

我们并没有办法确定 $k$ 倍数的上界,可能会非常大。但是我们可以考虑答案的每一个数位。

设各个数位之和的函数为 $f(x)$,不难发现性质:在数字 $x$ 后面加一位 $y$ 变成 $10x+y$,那么有

$$ f(10x + y) = f(x) + y $$

因此我们可以把上述过程视为在 $10x+y$ 和 $x$ 之间连了一条长为 $y$ 的边。我们只需要考虑 $0$ 到 $k$ 的倍数的最短路即可。

换一个角度,所有数字模 $k$ 的话,我们只需找到 $0 \to 0$ 的最小环。注意到模 $k$ 相同的话,只用保留最小的 $f(x)$,最后只需跑 $k$ 范围内的最短路即可。

vector<vector<pii>> G;

auto dijkstra(int n, int s) {
	vector<int> dis(n + 1, 1E9);
	vector<char> vis(n + 1);
	dis[s] = 0;
	priority_queue<pii, vector<pii>, std::greater<pii>> pq;
	pq.push({0, s});
	while (!pq.empty()) {
		auto [w, u] = pq.top();
		pq.pop();
		if (vis[u]) {
			continue;
		}
		vis[u] = true;
		for (auto [v, wi] : G[u]) {
			int len = w + wi;
			if (len < dis[v]) {
				dis[v] = len;
				pq.push({len, v});
			}
		}
	}
	return dis;
}

int main() {
	int k;
	cin >> k;
	G.resize(k);
	for (int i = 0; i < k; i++) {
		for (int j = 0; j < 10; j++) {
			G[i].emplace_back((i * 10 + j) % k, j);
		}
	}

	auto dis = dijkstra(k, 1);
	cout << dis[0] + 1;

	return 0;
}

F - CF961E

求有多少有序数対满足

  • $i < j$;
  • $i \leqslant a_j$;
  • $j \leqslant a_i$。

分析

很板的权值树状数组。

我们考虑顺序遍历 $a_j$,需要计算有多少 $i(i < j)$ 满足 $i \leqslant a_j$ 且 $j \leqslant a_i$。

因此我们可以使用树状数组维护满足 $j \leqslant a_i$ 数字的个数。当枚举到 $a_j$ 时,把小于 $j$ 的都清除即可。

template <class T>
struct fwtree; // 树状数组

int main() {
	int n;
	cin >> n;
	vector<int> a(n + 1);
	vector<pair<int, int>> b(n + 1);

	for (int i = 1; i <= n; i++) {
		cin >> a[i];
		b[i] = {a[i], i};
	}

	sort(b.begin() + 1, b.end());

	fwtree<int> tr(n + 1);

	ll ans = 0, p = 1;
	for (int i = 1; i <= n; i++) {
		while (p <= n && b[p].first < i) {
			tr.add(b[p].second, -1);
			p++;
		}
		ans += tr.sum(1, min(a[i], i - 1));
		tr.add(i, 1);
	}
	cout << ans;

	return 0;
}

G - CF484D

给定一个长为 $n$ 的数组 $\{a_i\}$,你需要将数组划分成连续的多段,每一段的值为该段的最大值于最小值之差。

需要最大化总权值。

分析

注意到若一段区间是单调的,那么其作为整体肯定比分割要好。

如果区间内存在拐点,那么可以讨论其应属于前一段还是后一段。

int main() {
	int n;
	cin >> n;
	vector<ll> v(n + 1), dp(n + 1);
	for (int i = 1; i <= n; i++) {
		cin >> v[i];
	}
	ll past = 1;
	for (int i = 2; i <= n; i++) {
		ll d1 = dp[past] + abs(v[i] - v[past + 1]);
		ll d2 = dp[past - 1] + abs(v[i] - v[past]);
		dp[i] = max(d1, d2);
		if (i < n && (v[i + 1] - v[i]) * (v[i] - v[i - 1]) <= 0) {
			past = i;
		}
	}
	cout << dp[n] << '\n';
	return 0;
}

H - CODE FESTIVAL 2017 qual B

给一个连通无向图 $G$,若点 $u$ 能走三条边到达 $v$,并且 $u,v$ 之间没有边,那么可以在他们之间加边。问 $G$ 最多能加几条边。

分析

注意到若 $G$ 无环,则可以対 $G$ 进行二分图,异色可以随意加边。

若 $G$ 有环,不难发现若是偶环,则対结论无影响;若是奇环,可以发现不可染色,所以能得到完全图。

int main() {
	int n, m;
	cin >> n >> m;
	vector<vector<int>> G(n + 1);
	for (int i = 0; i < m; i++) {
		int u, v;
		cin >> u >> v;
		G[u].push_back(v);
		G[v].push_back(u);
	}
	vector<int> col(n + 1, -1);
	bool success = true;
	col[0] = 0;
	std::function<void(int, int)> dfs = [&](int u, int fa) {
		if (!success) {
			return;
		}
		int cu = 1 - col[fa];
		if (col[u] != -1 && col[u] != cu) {
			success = false;
			return;
		}
		col[u] = cu;
		for (int v : G[u]) {
			if (v != fa) {
				dfs(v, u);
			}
		}
	};
	dfs(1, 0);

	if (success) {
		int cnt0 = 0;
		for (int i = 1; i <= n; i++) {
			cnt0 += col[i] == 0;
		}
		cout << 1ll * cnt0 * (n - cnt0) - m;
	} else {
		cout << 1ll * n * (n - 1) / 2 - m;
	}

	return 0;
}