2025 ICPC 成都区域赛 ABCDGJL
A. A Lot of Paintings
数学。
基于四舍五入,所以 \(\frac{b_i}{\sum b}\) 要满足 \(\frac{a_i}{100}-0.005\le \frac{b_i}{\sum b}<\frac{a_i}{100}+0.005\),即 \(\frac{b_i}{\sum b} \in[\frac{2a_i-1}{200},\frac{2a_i+1}{200})\),也就是说,如果 \(a_i\) 存在对应的数组 \(b\) 且 \(\sum a\ne 0\),那 \(\sum b\) 一定在 \(200\) 以内;当 $n>200 $ 并且 \(\sum a =0\) 时,这个时候 \(b_i\in[0,1)\),要使得 \(\frac{b_i}{\sum b}=0\),此时 \(b_i=1,\sum b = 200\) 是刚好等于 \(0.005\) 会四舍五入进 \(1\) 的,那么此时只要满足 \(\sum b = 201\) 就可以解决 \(a\) 全 \(0\) 的问题了。
具体地,可以枚举 \(s=\sum b\),先把每个 \(b_i\) 都变成下界 \(\lceil\frac{(2a_i-1)s}{200}\rceil\),对于多出来的 \(s-\sum b\),我们从前往后把每个 \(b_i\) 往上界 \(\lfloor\frac{(2a_i+1)s-1}{200}\rfloor\) 调整,最后满足 \(s=\sum b\) 即找到答案。
点击查看代码
#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 += 1) {
std::cin >> a[i];
}
std::vector<int> d(n);
for(int s = 1; s <= 201; s += 1) {
int sd = 0;
for(int i = 0; i < n; i += 1) {
d[i] = std::max(0, ((2 * a[i] - 1) * s + 199) / 200);
sd += d[i];
}
if(sd > s) {
continue;
}
for(int i = 0; i < n; i += 1) {
int x = ((2 * a[i] + 1) * s - 1) / 200;
x -= d[i];
if(sd + x > s) {
x = s - sd;
}
d[i] += x;
sd += x;
}
if(sd == s) {
std::cout << "YES\n";
for(int i = 0; i < n; i += 1) {
std::cout << d[i] << " \n"[i + 1 == n];
}
return;
}
}
std::cout << "NO\n";
}
int main() {
std::ios::sync_with_stdio(false);
std::cin.tie(nullptr);
int t;
std::cin >> t;
while (t--) {
solve();
}
return 0;
}
B. Blood Memories
解法一:Floyd,矩阵快速幂。
将 \(2^n\times 2^n\) 个状态写成矩阵的形式,那么矩阵到矩阵间的转移就是求一个 Floyd 的形式,该部分直接用矩阵快速幂加速即可。
点击查看代码
#include <bits/stdc++.h>
using i64 = long long;
template<class T>
struct Matrix {
i64 N;
std::vector<std::vector<T>> A;
Matrix() : N(0) {}
Matrix(int n) : N(n), A(n, std::vector<T>(n, 0)) {}
Matrix(const std::vector<std::vector<T>>& mat) {
assert(!mat.empty() && !mat[0].empty()); // 确保非空
N = mat.size();
A = mat;
}
const std::vector<T>& operator[](int i) const {
return A[i];
}
Matrix operator*(const Matrix &b) const {
assert(N == b.N);
Matrix res(N);
for (int i = 0; i < N; i++) {
for (int k = 0; k < N; k++) {
for (int j = 0; j < N; j++) {
res.A[i][j] = std::max(res.A[i][j],A[i][k] + b.A[k][j]);
}
}
}
return res;
}
Matrix power(Matrix res, i64 k) const {
Matrix base = *this;
while (k) {
if (k & 1) {
res = res * base;
}
base = base * base;
k >>= 1;
}
return res;
}
};
void solve() {
int n, m, k, R;
std::cin >> n >> m >> k >> R;
std::vector<std::array<int,2>> a(n);
for(auto &[x, y] : a) {
std::cin >> x >> y;
}
std::vector<std::array<i64,2>> v(1 << n);
for(int o = 0; o < 1 << n; o += 1) {
for(int i = 0; i < n; i += 1) {
v[o][0] += (o >> i & 1) * a[i][0];
v[o][1] += (o >> i & 1) * a[i][1];
}
}
Matrix<i64> A(1 << n);
for(int o = 0; o < 1 << n; o += 1) {
for(int p = 0; p < 1 << n; p += 1) {
i64 nv = __builtin_popcount(o & p);
nv *= k;
nv += v[p][1];
A.A[o][p] = (nv <= m) * v[p][0];
}
}
A = A.power(A, R - 1);
i64 ans = 0;
for(int i = 0; i < 1 << n; i += 1) {
ans = std::max<i64>(ans, A[0][i]);
}
std::cout << ans << "\n";
}
int main() {
std::ios::sync_with_stdio(false);
std::cin.tie(nullptr);
int t;
std::cin >> t;
while (t--) {
solve();
}
return 0;
}
解法二:\(dp\),找循环节。
猜测中间应该是先从 \(0\) 状态开始走一段开始进入循环,循环完后可能走一段不满足循环节的路退出,并且循环节长度不会太大。
设 \(dp_{i,j,k}\) 表示从 \(j\) 状态到 \(k\) 状态进行了 \(i\) 轮的造成的最大伤害,那么有转移 \(dp[i][j][k]\leftarrow \max (dp[i][j][k],dp[i-1][j][p]+dp[1][p][k])\),也就是三维 Floyd,中间再维护一个 \(pre_{i,j}\) 表示从 \(j\) 状态走 \(i\) 步的一个答案,方便之后计算循环节结束到第 \(R\) 轮退出的那一段区间。
之后再枚举第 \(i\) 轮开始进入循环,循环节以 \(j\) 开始,循环节的长度为 \(l\),那么答案就是 \(dp[i][0][j]+\frac{R-i}{l}\times dp[l][j][j] + pre[(R-i)\% l][j]\),取最大即可。
点击查看代码
#include <bits/stdc++.h>
using namespace std;
#define int long long
#define ll long long
#define endl '\n'
const int mod=998244353;
const int N=8,MAX=1<<6;
ll f[33][MAX][MAX];
int a[N],c[N];
int pre[33][MAX];
void solve() {
int n, m, k, R;
cin >> n >> m >> k >> R;
for (int i = 0; i < n; ++i) {
cin >> a[i] >> c[i];
}
int mx = 1 << n;
int p = 31;
for (int i = 0; i < mx; ++i) {
for (int j = 0; j < mx; ++j) {
for (int l = 0; l <= p; ++l) {
f[l][i][j] = 0;
pre[l][j] = 0;
}
}
}
for (int i = 0; i < mx; ++i) {
for (int j = 0; j < mx; ++j) {
int zt = 0;
ll sum = 0;
int v = 0;
int vs = 1;
for (int l = 0; l < n; ++l) {
if ((j >> l & 1) and v + c[l] <= m) {
v += c[l];
if (i >> l & 1) {
v += k;
if (v > m) {
vs = 0;
break;
}
}
zt |= 1 << l;
sum += a[l];
}
}
// cout<<i<<' '<<zt<<' '<<sum<<endl;
if (vs)
f[1][i][zt] = sum;
}
}
for (int i = 2; i <= 31; ++i) {
for (int j = 0; j < mx; ++j) {
for (int l = 0; l < mx; ++l) {
for (int i1 = 0; i1 < mx; ++i1) {
f[i][j][l] = max(f[i][j][l], f[i - 1][j][i1] + f[1][i1][l]);
}
pre[i][j] = max(pre[i][j], f[i][j][l]);
}
}
}
ll ans = 0;
for (int i = 1; i <= min(R, 31ll); ++i) {
for (int j = 0; j < mx; ++j) {
int sy = R - i;
for (int l = 1; l <= min(R, i); ++l) {
ll res = f[i][0][j];
int cnt = sy / l;
int pp = sy % l;
res += cnt * f[l][j][j];
if (pp) {
res += pre[pp][j];
}
ans = max(ans, res);
}
}
}
cout << ans << endl;
}
signed main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
int t;
cin>>t;
while (t--) {
solve();
}
return 0;
}
C. Crossing River
二分,贪心。
正序枚举需要面临在某些时刻进行抉择,即等不等下一个人,难以讨论的。
正难则反,考虑先二分出一个答案,从后往前讨论,至于终点停在哪边可以直接枚举。
从后往前的时候,可以直接贪心的想直接来回送即可,不用考虑等待的时间,因为这部分等待的时间可以直接摞到最开始的时候就等待完成,后面保证每一趟不空船即可,如果某一边送完了那么来回的时候肯定有一趟会空船,这部分直接减去即可,上述过程可以用双指针模拟即可。
点击查看代码
#include <bits/stdc++.h>
using i64 = long long;
int main() {
std::ios::sync_with_stdio(false);
std::cin.tie(nullptr);
int n, m, k;
std::cin >> n >> m >> k;
std::vector<std::array<int,2>> a(n), b(m);
for(int i = 0; i < n; i += 1) {
std::cin >> a[i][0];
a[i][1] = i;
}
for(int i = 0; i < m; i += 1) {
std::cin >> b[i][0];
b[i][1] = i;
}
sort(a.begin(), a.end(), std::greater<>());
sort(b.begin(), b.end(), std::greater<>());
auto check = [&](i64 T)->std::pair<bool, std::vector<std::tuple<i64,int,int>>> {
for(int p = 0; p < 2; p += 1) {
std::vector<std::tuple<i64,int,int>> res;
bool ok = true;
int idxa = 0, idxb = 0, now = p;
i64 nT = T;
while(idxa < n || idxb < m) {
if(!now) {
if(idxa == n) {
nT -= k;
} else {
if(nT - k >= a[idxa][0]) {
nT -= k;
res.emplace_back(nT, now, a[idxa][1]);
idxa += 1;
} else {
ok = false;
break;
}
}
} else {
if(idxb == m) {
nT -= k;
} else {
if(nT - k >= b[idxb][0]) {
nT -= k;
res.emplace_back(nT, now, b[idxb][1]);
idxb += 1;
} else {
ok = false;
break;
}
}
}
now ^= 1;
}
if(ok) {
return {ok, res};
}
}
return {false, {}};
};
i64 lo = 0, hi = 1E15;
std::vector<std::tuple<i64,int,int>> ans;
while(lo < hi) {
i64 mid = (lo + hi) >> 1;
auto [f, res] = check(mid);
if(f) {
hi = mid;
ans = res;
} else {
lo = mid + 1;
}
}
sort(ans.begin(), ans.end());
std::cout << hi << "\n";
for(auto &[t, p, id] : ans) {
std::cout << t << " " << p << " " << id + 1 << "\n";
}
return 0;
}
D. Deductive Snooker Scoring
模拟。
对红球和彩球分开处理,预处理 \(15\) 个红球能够凑出的分数,然后对于 \(n\ge 6\) 的情况直接枚举两边用了几个红球就行;\(n<6\) 的情况,可以二进制枚举 \(6-n\) 个彩球分配给了谁,然后减去这部分后再按第一部分处理即可。
一些细节需要注意,比如 \(n=0\) 的时候,最后一杆不能通过加 / 进行换人,因为此时已经没球可以打了。
点击查看代码
#include <bits/stdc++.h>
using i64 = long long;
std::string s[16][201];
void solve() {
int a, b, n, p;
std::cin >> a >> b >> n >> p;
auto check = [](const std::string &ans)->void{
int s[2] {}, p = 0;
for(int i = 0; i < ans.size(); i += 1) {
if(ans[i] == '/') {
p ^= 1;
continue;
}
s[p] += ans[i] - '0';
}
std::cout << s[0] << " " << s[1] << " " << p << "\n";
};
if(n >= 6) {
std::string ans = "";
n = 21 - n;
bool ok = false;
for(int i = 0; i <= n; i += 1) {
if(s[i][a] != "NA" && s[n - i][b] != "NA") {
ans = s[i][a] + "/" + s[n - i][b] + "/";
ok = true;
break;
}
}
if(p) {
ans += "/";
}
if(!ok) {
ans = "NA";
}
std::cout << ans << "\n";
return;
}
n = 6 - n;
for(int o = 0; o < 1 << n; o += 1) {
int na = 0, nb = 0;
for(int i = 0; i < n; i += 1) {
if(~o >> i & 1) {
na += i + 2;
} else {
nb += i + 2;
}
}
if(na > a || nb > b) continue;
bool ok = false;
std::string ans = "";
for(int i = 0; i <= 15; i += 1) {
if(s[i][a - na] != "NA" && s[15 - i][b - nb] != "NA") {
ans = s[i][a - na] + "/" + s[15 - i][b - nb];
ok = true;
break;
}
}
if(!ok) {
continue;
}
for(int j = 0; j < 2; j += 1) {
int lst = j;
std::string tmp = j ? "" : "/";
for(int i = 0; i < n; i += 1) {
if((o >> i & 1) != lst) {
tmp += "/";
lst ^= 1;
}
tmp += char('0' + i + 2);
}
if(n == 6 && p != lst) {
continue;
}
if(p != lst) {
tmp += "/";
}
ans += tmp;
std::cout << ans << "\n";
return;
}
}
std::cout << "NA" << "\n";
}
int main() {
std::ios::sync_with_stdio(false);
std::cin.tie(nullptr);
for(int i = 0; i <= 15; i += 1) {
for(int j = 0; j <= 200; j += 1) {
s[i][j] = "NA";
}
}
for(int i = 0; i <= 15; i += 1) {
for(int j = i; j <= i * 8; j += 1) {
if(j == i + 1) {
continue;
}
s[i][j] = "";
int one = i, sum = j - i;
while(sum >= 7 && one) {
s[i][j] += "17";
sum -= 7, one -= 1;
}
if(sum == 1) {
s[i][j].back() -= 1;
sum += 1;
}
if(sum) {
s[i][j] += "1";
s[i][j] += '0' + sum;
one -= 1;
}
while(one--) {
s[i][j] += "//1";
}
}
}
int t;
std::cin >> t;
while (t--) {
solve();
}
return 0;
}
G. GCD of Subsets
数学。
题目要求选出尽可能多的子集使得 \(\text{GCD}=k\),显然把 \(m\) 个数都换成 \(k\) 是最优的。
当 \(k=1\) 的时候,可以想到的是 \((1),(2,3),(4,5)...\) 这样去选择,由此推论,那么当\(k\ne 1\) 的时候,可以选择 \((k),(2k,3k),(4k,5k)...\),如果刚好 \(6k\),那么最后一组就 \((4k,5k,6k)\),这样可选择的答案数为 \(s=\lceil\frac{\frac nk}{2}\rceil\)。
那么剩下可以被替换的数为 \(d=n-\frac nk + [\frac nk \mod 2 = 0]\),后面这项加 \(1\) 是因为像上面 \(6k\) 也可以拿出来被替换,那么当 \(m\le d\) 的时候,这 \(m\) 个数就是单独增加的答案,即 \(m+s\);当 \(m>d\) 时,那么除去 \(n-\frac nk\) 个数都被替换成 \(k\) 外,还可以拆上面的组合,也就是 \(\frac nk\) 中有 \(m - (n - \frac nk)\) 个数被换成 \(k\) 了,剩下依然保持上面的组合,按照上面计算即可。
点击查看代码
#include <bits/stdc++.h>
using i64 = long long;
void solve() {
i64 n, k, m;
std::cin >> n >> k >> m;
i64 x = n / k, d = n - x;
if(m <= d + (x % 2 == 0)) {
std::cout << m + (x + 1) / 2 << "\n";
return;
}
i64 ans = d;
m -= d;
ans += std::min(x, m);
x -= m;
x = std::max(0LL, x);
ans += (x + 1) / 2;
std::cout << ans << "\n";
}
int main() {
std::ios::sync_with_stdio(false);
std::cin.tie(nullptr);
int t;
std::cin >> t;
while (t--) {
solve();
}
return 0;
}
J. Judging Papers
模拟。
对于已经通过的 \(\text{paper}\),我们可以直接计入到答案里。
对于没通过的,可以直接进行一次 \(\text{rebuttal}\) 操作,如果过了,那就贡献到答案里,否则就不管。
点击查看代码
#include <bits/stdc++.h>
using i64 = long long;
void solve() {
int n, k, m, b;
std::cin >> n >> m >> k >> b;
int ans = 0;
std::vector<int> t(m);
for(int i = 0; i < n; i += 1) {
int res = 0;
for(int j = 0; j < m; j += 1) {
std::cin >> t[j];
res += t[j];
}
if(res >= k) {
ans += 1;
continue;
}
res = 0;
for(int j = 0; j < m; j += 1) {
if(t[j] > 0) {
t[j] -= 1;
} else {
t[j] += 1;
}
res += t[j];
}
if(res >= k && b) {
b -= 1;
ans += 1;
}
}
std::cout << ans << "\n";
}
int main() {
std::ios::sync_with_stdio(false);
std::cin.tie(nullptr);
int t;
std::cin >> t;
while (t--) {
solve();
}
return 0;
}
L. Label Matching
解法一:树上启发式合并 \((\text{Dsu On Tree})\)。
因为可以任意次交换,那么以 \(i\) 节点为根的子树 \(T_i\) 中,记 \(cnta_x/cntb_x\) 为各自值域中 \(x\) 的数量,有 \(sum=\sum\limits_{x=1}^n|cnta_x-cntb_x|\) 这些数是无法找到配对的,但是 \(0\) 是可以任意配对的,所以当一棵子树内存在 \(cnta_0+cntb_0\ge sum\) 时,该子树是合法的。
可以考虑先从叶子节点开始计算,合并两棵子树的时候可以用树上启发式合并。
点击查看代码
#include <bits/stdc++.h>
using i64 = long long;
struct DsuOnTree {
int n, dfn = 0;
std::vector<int> sz, big, L, R, Node;
std::vector<std::vector<int>> adj;
//根据题目要求修改
int max = 0, now[2] {};
std::vector<bool> ans;
std::vector<std::array<int,2>> info, cnt;
DsuOnTree(int n): n(n), sz(n), big(n, -1), L(n), R(n), Node(n) {
adj.resize(n);
ans.resize(n);
info.resize(n);
cnt.resize(n + 1);
}
void add(int u, int v) {
adj[u].push_back(v);
adj[v].push_back(u);
}
void add(std::vector<std::array<int,2>> &info_) {
info = std::move(info_);
}
void add(int u) {
//计算贡献
for(int i = 0; i < 2; i += 1) {
int x = info[u][i];
if(!x) {
now[0] += 1;
} else {
now[1] -= abs(cnt[x][0] - cnt[x][1]);
cnt[x][i] += 1;
now[1] += abs(cnt[x][0] - cnt[x][1]);
}
}
}
void del(int u) {
//删除贡献
for(int i = 0; i < 2; i += 1) {
int x = info[u][i];
if(!x) {
now[0] -= 1;
} else {
now[1] -= abs(cnt[x][0] - cnt[x][1]);
cnt[x][i] -= 1;
now[1] += abs(cnt[x][0] - cnt[x][1]);
}
}
}
bool getAns() {
return now[0] >= now[1];
}
void dfs0(int u, int fa) {
Node[dfn] = u;
L[u] = dfn++;
sz[u] = 1;
for (int v : adj[u]) {
if (v != fa) {
dfs0(v, u);
sz[u] += sz[v];
if (big[u] == -1 || sz[big[u]] < sz[v]) {
big[u] = v;
}
}
}
R[u] = dfn;
}
void dfs1(int u, int fa, bool keep) {
// 计算轻儿子的答案
for (int v : adj[u]) {
if (v != fa && v != big[u]) {
dfs1(v, u, false);
}
}
// 计算重儿子答案并保留计算过程中的数据(用于继承)
if (~big[u]) {
dfs1(big[u], u, true);
}
for (int v : adj[u]) {
if (v != fa && v != big[u]) {
// 子树结点的 DFS 序构成一段连续区间,可以直接遍历
for (int i = L[v]; i < R[v]; i++) {
add(Node[i]);
}
}
}
add(u);
ans[u] = getAns();
if (keep == false) {
for (int i = L[u]; i < R[u]; i++) {
del(Node[i]);
}
}
}
void solve() {
dfs0(0, 0);
dfs1(0, 0, false);
for(int i = 0; i < n; i += 1) {
std::cout << ans[i];
}
std::cout << "\n";
}
};
void solve() {
int n;
std::cin >> n;
std::vector<std::array<int,2>> info(n);
for(int j = 0; j < 2; j += 1) {
for(int i = 0; i < n; i += 1) {
std::cin >> info[i][j];
}
}
DsuOnTree dsu(n);
dsu.add(info);
for(int i = 1; i < n; i += 1) {
int u, v;
std::cin >> u >> v;
u --, v --;
dsu.add(u, v);
}
dsu.solve();
}
int main() {
std::ios::sync_with_stdio(false);
std::cin.tie(nullptr);
int t;
std::cin >> t;
while (t--) {
solve();
}
return 0;
}
解法二:启发式合并。
对每个节点维护一个桶和答案,合并的时候将小桶合并到大桶上面去,也就是启发式合并的核心思想,计算答案的时候在桶合并的时候计算,这样复杂度才能保证合并是 \(\text{log}\) 级别的。
具体合并的时候,对于 \(u\) 的两个属性 \(a,b\) 来说,对 \(a\) 做加法,\(b\) 做减法,最后绝对值就是 \(a,b\) 未匹配的个数,统计未匹配的总数与 \(0\) 的作对比即可判断,用 \(\text{map}\) 作桶就是 \(O(n\log^2n)\)。
可能用结构体会写得更加优雅
点击查看代码
#include <bits/stdc++.h>
using i64 = long long;
using Info = std::pair<int,std::unordered_map<int, int>>;
void solve() {
int n;
std::cin >> n;
std::vector<std::array<int,2>> a(n);
for(int i = 0; i < n; i += 1) {
std::cin >> a[i][0];
}
for(int i = 0; i < n; i += 1) {
std::cin >> a[i][1];
}
std::vector adj(n, std::vector<int>());
for(int i = 1; i < n; i += 1) {
int u, v;
std::cin >> u >> v;
u --, v --;
adj[u].push_back(v);
adj[v].push_back(u);
}
std::vector<Info> info(n);
auto merge = [](Info &a,Info &b)->void{
if(a.second.size() < b.second.size()) {
std::swap(a, b);
}
for(auto &[x, y] : b.second) {
if(x == 0) {
a.first -= a.second[x];
} else {
a.first += abs(a.second[x]);
}
a.second[x] += y;
if(x == 0) {
a.first += a.second[x];
} else {
a.first -= abs(a.second[x]);
}
if(a.second[x] == 0) {
a.second.erase(x);
}
}
};
std::vector<bool> ans(n);
auto dfs = [&](auto &&self, int u, int fa)->void{
auto [x, y] = a[u];
info[u].second[x] += 1;
if(!y) {
info[u].second[y] += 1;
} else{
info[u].second[y] -= 1;
}
for(auto &[val, w] : info[u].second) {
if(val == 0) {
info[u].first = w;
} else {
info[u].first -= abs(w);
}
}
for(auto &v : adj[u]) {
if(v == fa) continue;
self(self, v, u);
merge(info[u], info[v]);
}
ans[u] = info[u].first >= 0;
};
dfs(dfs, 0, 0);
for(int i = 0; i < n; i += 1) {
std::cout << ans[i];
}
std::cout << "\n";
}
int main() {
std::ios::sync_with_stdio(false);
std::cin.tie(nullptr);
int t;
std::cin >> t;
while (t--) {
solve();
}
return 0;
}

浙公网安备 33010602011771号