2025牛客暑期多校训练营5
E. Mysterious XOR Operation
题意:给你\(n\)个数,两个数的贡献是它们的异或和从低位到高位的第奇数个\(1\)的二进制和。求两两价值和。
按位考虑。
对于第\(i\)位,有多少对让这一位产生价值?首先它们的这一位不同,然后它们的低\(i\)位的\(1\)的个数的奇偶性不同。
因为奇偶性不同,那么如果没有一对\(1\)抵消,就是奇数加偶数等于奇数个\(1\),那么第\(i\)位就能产生贡献,否则有\(k\)位都是\(1\)被异或掉了,那么总共消掉了\(2k\)个\(1\),剩下\(1\)的个数依然是奇数,所以第\(i\)位依然产生贡献。
那么记录第\(i\)位是\(1\)或\(0\),且低\(i\)位\(1\)的个数是奇偶性即可。
点击查看代码
#include <bits/stdc++.h>
using i64 = long long;
void solve() {
int n;
std::cin >> n;
std::vector<int> a(n);
for (int i = 0; i < n; ++ i) {
std::cin >> a[i];
}
i64 ans = 0;
for (int i = 0; i < 30; ++ i) {
int c00 = 0, c01 = 0, c10 = 0, c11 = 0;
int mask = (1ll << (i + 1)) - 1;
for (int j = 0; j < n; ++ j) {
int cnt = __builtin_popcount(a[j] & mask);
if (a[j] >> i & 1) {
if (cnt & 1) {
++ c11;
} else {
++ c10;
}
} else {
if (cnt & 1) {
++ c01;
} else {
++ c00;
}
}
}
ans += (1ll * c00 * c11 + 1ll * c01 * c10) * (1ll << i);
}
std::cout << ans << "\n";
}
int main() {
std::ios::sync_with_stdio(false), std::cin.tie(0), std::cout.tie(0);
int t = 1;
// std::cin >> t;
while (t -- ) {
solve();
}
return 0;
}
H. VI Civilization
赛后补题。
题目还是看原题面吧,虽然长但是讲的还清楚。因为很长所以赛时都没看,补题时发现并不是很难。其实官方题解也讲的很清楚了。
考虑给定\(p\)如何判断能不能\(t\)轮内结束,考虑\(dp\),其实状态不难想,我们想要知道能不能\(t\)轮到达\(s\),那么记每一轮可以到达的最大科技槽,记录轮数还不够,我们想要转移得知道现在的科技点是多少,也就是解锁了几个科技,然后还有生产力需要记录,我们记录之前有多少轮还没用过生产力,那么可以得到\(f[i][j][x]\)为前\(i\)轮解锁了前\(j\)个科技,且有\(x\)轮没有使用生产力的最大科技槽。为什么我们可以不用前面的生产力而把它们存下来?其实并不是没用,如果我们想用到第\(j+1\)个科技上,那么就把前面没用生产力的轮数把生产力用到\(j+1\)上就行了,发现一定能构造一个合法的操作。
转移就是枚举几种情况,一种是加科技槽,一种是不用生产力用科技点解锁科技,那么需要\(\lceil \frac{a_{j+1}}{sum_j} \rceil\)轮,其中\(sum_j = m + \sum_{i=1}^{j} k_i\)。如果用生产力解锁,那么需要\(\lceil \frac{b_{j+1}}{p} \rceil\)轮,然后\(a_{j+1}\)减少\(c_{j+1}\),还需要用科技点搞\(\lceil \frac{a_{j+1} - c_{j+1}}{sum_j} \rceil\)轮。
然后考虑怎么找到最小的\(p\),二分就行。
点击查看代码
#include <bits/stdc++.h>
using i64 = long long;
i64 f[110][110][110];
void solve() {
int m, s, t;
std::cin >> m >> s >> t;
int n;
std::cin >> n;
std::vector<int> a(n + 1), k(n + 1), b(n + 1), c(n + 1);
for (int i = 1; i <= n; ++ i) {
std::cin >> a[i] >> k[i] >> b[i] >> c[i];
}
std::vector<int> sum(n + 1);
sum[0] = m;
for (int i = 1; i <= n; ++ i) {
sum[i] = sum[i - 1] + k[i];
}
auto chmax = [&](i64 & x, i64 y) -> void {
x = std::max(x, y);
};
auto check = [&](int p) -> bool {
memset(f, 0xcf, sizeof f);
f[0][0][0] = 0;
for (int i = 0; i < t; ++ i) {
for (int j = 0; j <= n; ++ j) {
for (int x = 0; x <= i; ++ x) {
if (f[i][j][x] < 0) {
continue;
}
chmax(f[i + 1][j][x + 1], f[i][j][x] + sum[j]);
if (j + 1 <= n) {
int cnt = (a[j + 1] + sum[j] - 1) / sum[j];
if (i + cnt <= t && x + cnt <= t) {
chmax(f[i + cnt][j + 1][x + cnt], f[i][j][x]);
}
if (p > 0) {
int cnt1 = (b[j + 1] + p - 1) / p;
int cnt2 = (a[j + 1] - c[j + 1] + sum[j] - 1) / sum[j];
if (i + cnt2 <= t && x - cnt1 + cnt2 >= 0) {
chmax(f[i + cnt2][j + 1][x - cnt1 + cnt2], f[i][j][x]);
}
}
}
}
}
}
for (int j = 0; j <= n; ++ j) {
for (int x = 0; x <= t; ++ x) {
if (f[t][j][x] >= s) {
return true;
}
}
}
return false;
};
int l = 0, r = 10000;
while (l < r) {
int mid = l + r >> 1;
if (check(mid)) {
r = mid;
} else {
l = mid + 1;
}
}
if (!check(l)) {
l = -1;
}
std::cout << l << "\n";
}
int main() {
std::ios::sync_with_stdio(false), std::cin.tie(0), std::cout.tie(0);
int t = 1;
// std::cin >> t;
while (t -- ) {
solve();
}
return 0;
}
I. Block Combination Minimal Perimeter
题意:有\(i \in [1, n]\)的\(1\times i\)各一个,求构成的矩形的最小周长。
找规律题。
\(n\)是奇数可以\(i, n - i\)配对,那么宽有\(\lceil \frac{n}{2} \rceil\),长度就是\(n\)。
如果是偶数,那么可以\(i, n + 1 - i\)配对。
点击查看代码
#include <bits/stdc++.h>
using i64 = long long;
void solve() {
i64 n;
std::cin >> n;
if (n == 1) {
std::cout << 4 << "\n";
} else if (n & 1) {
std::cout << n * 2 + (n + 1) / 2 * 2 << "\n";
} else {
std::cout << (n + 1) * 2 + n / 2 * 2 << "\n";
}
}
int main() {
std::ios::sync_with_stdio(false), std::cin.tie(0), std::cout.tie(0);
int t = 1;
// std::cin >> t;
while (t -- ) {
solve();
}
return 0;
}
J. Fastest Coverage Problem
题意:一个\(01\)矩阵,\(1\)每秒会往四周蔓延。你可以把一个\(0\)变成\(1\),求蔓延整个矩阵的最小时间。
先求出每个位置被蔓延的时间。然后考虑二分时间。
把所有被蔓延的时间大于等于\(mid\)的拿出来,我们需要找一个位置在\(mid\)秒内蔓延它们。
那么问题变成,给你一些点,求一个点和它们的曼哈顿距离最大不超过\(mid\)。
这是经典问题,把曼哈顿距离转为切比雪夫距离,那么就变成了正方形求交的问题。我们只需要求出最小的\(x\)和最大的\(x\),以及最小的\(y\)和最大的\(y\)。看它们有没有交集。如果有交集,还要看有没有整数解,因为转为切比雪夫坐标后其中的整数点在原坐标系里想要是整数点需要满足横纵坐标奇偶性相同。
点击查看代码
#include <bits/stdc++.h>
using i64 = long long;
const int dx[] = {-1, 0, 1, 0}, dy[] = {0, 1, 0, -1};
void solve() {
int n, m;
std::cin >> n >> m;
std::vector a(n, std::vector<int>(m));
for (int i = 0; i < n; ++ i) {
for (int j = 0; j < m; ++ j) {
std::cin >> a[i][j];
}
}
const int inf = 1e9;
std::queue<std::pair<int, int>> q;
std::vector d(n, std::vector<int>(m, inf));
for (int i = 0; i < n; ++ i) {
for (int j = 0; j < m; ++ j) {
if (a[i][j] == 1) {
q.emplace(i, j);
d[i][j] = 0;
}
}
}
while (q.size()) {
auto [x, y] = q.front(); q.pop();
for (int i = 0; i < 4; ++ i) {
int nx = x + dx[i], ny = y + dy[i];
if (nx < 0 || nx >= n || ny < 0 || ny >= m) {
continue;
}
if (d[nx][ny] == inf) {
d[nx][ny] = d[x][y] + 1;
q.emplace(nx, ny);
}
}
}
auto check = [&](int t) -> bool {
int minx = 1e9, maxx = -1e9, miny = 1e9, maxy = -1e9;
for (int i = 0; i < n; ++ i) {
for (int j = 0; j < m; ++ j) {
if (d[i][j] > t) {
minx = std::min(minx, i + j);
maxx = std::max(maxx, i + j);
miny = std::min(miny, i - j);
maxy = std::max(maxy, i - j);
}
}
}
if (minx >= maxx && miny >= maxy) {
return true;
}
int lx = maxx - t, rx = minx + t;
int ly = maxy - t, ry = miny + t;
if (lx > rx || ly > ry) {
return false;
}
for (int i = lx; i <= rx; ++ i) {
for (int j = ly; j <= ry; ++ j) {
if (i % 2 == j % 2) {
return true;
}
}
}
return false;
};
int l = 0, r = n + m;
while (l < r) {
int mid = l + r >> 1;
if (check(mid)) {
r = mid;
} else {
l = mid + 1;
}
}
std::cout << l << "\n";
}
int main() {
std::ios::sync_with_stdio(false), std::cin.tie(0), std::cout.tie(0);
int t = 1;
// std::cin >> t;
while (t -- ) {
solve();
}
return 0;
}
K. Perfect Journey
赛后补题。
题意:给你一个树和\(m\)条关键边,再给你\(k\)条路径,你要选择一些路径使得他们的并集覆盖所有关键边,求最少选几条路径,以及方案数。
\(m\)很小,先考虑把路径转为二进制数,第\(i\)为表示有没有覆盖第\(i\)条关键边。可以\(dfs\),记\(st_u\)为根到\(u\)的路径上的状态,那么路径\((u, v)\)经过的状态可以表示为\(st_u | st_v \oplus st_{lca(u, v)}\),实际上这个等价于\(st_u \oplus st_v\),于是我们不需要求\(lca\)。
那么记\(cnt_s\)表示状态为\(s\)的路径数,记\(f_s\)为\(s\)的所有子集的方案数。
然后最小路径数小于等于\(m\),因为如果一条边不能贡献新的关键边,那么加入它没有意义。然后我们枚举需要选\(t\)条边,现在问题变成从\(k\)条路径里选\(t\)条路径覆盖所有关键边的方案数。这个问题可以用容斥解决:我们任意选\(t\)条边,其中肯定有些方案是不对的,那么考虑对于每个\(i\),减去没有经过\(i\)的方案数,然后发现还需要加上没有经过其中两个边的方案数,减去没有经过三条边的方案数...
那么实际是求\(\sum_{T} (-1)^{|T|}C(f_{(2^m-1)\oplus T}, t)\)。
我们从小到大枚举\(t\),遇到第一个方案数不为\(0\)的就是解。
代码省略取模类。
点击查看代码
constexpr int P = 998244353;
using Z = MInt<P>;
struct Comb {
int n;
std::vector<Z> _fac;
std::vector<Z> _invfac;
std::vector<Z> _inv;
Comb() : n{0}, _fac{1}, _invfac{1}, _inv{0} {}
Comb(int n) : Comb() {
init(n);
}
void init(int m) {
if (m <= n) return;
_fac.resize(m + 1);
_invfac.resize(m + 1);
_inv.resize(m + 1);
for (int i = n + 1; i <= m; i++) {
_fac[i] = _fac[i - 1] * i;
}
_invfac[m] = _fac[m].inv();
for (int i = m; i > n; i--) {
_invfac[i - 1] = _invfac[i] * i;
_inv[i] = _invfac[i] * _fac[i - 1];
}
n = m;
}
Z fac(int m) {
if (m > n) init(2 * m);
return _fac[m];
}
Z invfac(int m) {
if (m > n) init(2 * m);
return _invfac[m];
}
Z inv(int m) {
if (m > n) init(2 * m);
return _inv[m];
}
Z binom(int n, int m) {
if (n < m || m < 0) return 0;
return fac(n) * invfac(m) * invfac(n - m);
}
} comb;
void solve() {
int n, m, k;
std::cin >> n >> m >> k;
std::vector<std::pair<int, int>> edges(n);
std::vector<std::vector<std::pair<int, int>>> adj(n);
for (int i = 1; i < n; ++ i) {
int u, v;
std::cin >> u >> v;
-- u, -- v;
adj[u].emplace_back(v, i);
adj[v].emplace_back(u, i);
}
std::vector<int> id(n, -1);
for (int i = 0; i < m; ++ i) {
int x;
std::cin >> x;
id[x] = i;
}
std::vector<int> st(n);
auto dfs = [&](auto & self, int u, int fa) -> void {
for (auto & [v, i] : adj[u]) {
if (v == fa) {
continue;
}
st[v] = st[u];
if (id[i] != -1) {
st[v] |= (1 << id[i]);
}
self(self, v, u);
}
};
dfs(dfs, 0, -1);
int M = (1 << m) - 1;
std::vector<int> f(M + 1);
for (int i = 0; i < k; ++ i) {
int u, v;
std::cin >> u >> v;
-- u, -- v;
f[st[u] ^ st[v]] += 1;
}
for (int i = 0; i < m; ++ i) {
for (int j = 0; j <= M; ++ j) {
if (j >> i & 1) {
f[j] += f[j ^ (1 << i)];
}
}
}
for (int t = 1; t <= m; ++ t) {
Z sum = 0;
for (int s = 0; s <= M; ++ s) {
int sign = __builtin_popcount(s) % 2 ? -1 : 1;
sum += sign * comb.binom(f[M ^ s], t);
}
if (sum != 0) {
std::cout << t << " " << sum << "\n";
return;
}
}
std::cout << -1 << "\n";
}
int main() {
std::ios::sync_with_stdio(false), std::cin.tie(0), std::cout.tie(0);
int t = 1;
// std::cin >> t;
while (t -- ) {
solve();
}
return 0;
}