[题解] 2025 ICPC 南昌邀请赛暨江西省赛 部分题解 2025 ICPC Nanchang Invitational and Jiangxi Provincial Collegiate Programming Contest
官方题解:[codeforces.com/gym/105911/attachments/download/31650/2025 ICPC Nanchang Invitational Solution.pdf](https://codeforces.com/gym/105911/attachments/download/31650/2025 ICPC Nanchang Invitational Solution.pdf)
赛时榜单:2025年icpc全国邀请赛(南昌)暨2025年(icpc)江西省大学生程序设计竞赛 - 正式赛 | Board - XCPCIO
这把读题占模严重啊,英文题目且奇长。。
A
int a, b, c, d;
cin >> a >> b >> c >> d;
cout << (a + b + c) * d << '\n';
D
值域非常的大,我们直接离散化,然后差分一下,前缀和起来取 \(\max\) 就可以了。
#include <bits/stdc++.h>
using i64 = long long;
using namespace std;
int main() {
ios::sync_with_stdio(0), cin.tie(0), cout.tie(0);
map<int, int> mp;
int n, a, b, c;
cin >> n >> a >> b >> c;
mp[a] = mp[b] = mp[c] = 0;
vector<array<int, 6>> in(n + 1);
for (int i = 1; i <= n; i++) {
for (auto &e : in[i]) {
cin >> e;
}
auto &[x, y, z, X, Y, Z] = in[i];
mp[x] = mp[y] = mp[z] = mp[X] = mp[Y] = mp[Z] = 0;
if (x > X) swap(x, X);
if (y > Y) swap(y, Y);
if (z > Z) swap(z, Z);
}
int sz = 0;
for (auto &[f, s] : mp) {
s = ++sz;
}
vector<int> bx(sz + 2), by(sz + 2), bz(sz + 2);
for (int i = 1; i <= n; i++) {
auto &[x, y, z, X, Y, Z] = in[i];
bx[mp[x]]++, bx[mp[X] + 1]--;
by[mp[y]]++, by[mp[Y] + 1]--;
bz[mp[z]]++, bz[mp[Z] + 1]--;
}
int ans = 0;
for (int i = 1; i <= sz; i++) {
bx[i] += bx[i - 1];
by[i] += by[i - 1];
bz[i] += bz[i - 1];
ans = max({ans, bx[i], by[i], bz[i]});
}
cout << ans << '\n';
return 0;
}
E
我们注意到给了 3 秒,范围是 3e5,对于题目这种,区间扩展收缩可以计算答案的题,考虑莫队。
如何计算 区间内的 k-fold 串个数呢?要想形成 k-fold 串,充要 是串内的所有字符都被 k 整除。
我们在 mod k 意义下计算一个前缀桶,然后对其进行 序号哈希,当序号[l - 1] == 序号[r]
时,\(s_{l, r}\) 是 k-fold串。详见代码。
莫队,桶+哈希,\(O(n \sqrt {n} + n\log n)\) 。
#include <bits/stdc++.h>
using i64 = long long;
using namespace std;
int B, n, k, Q, idx;
i64 res;
vector<int> c;
struct qu {
int l, r, i;
};
bool operator<(const qu &a, const qu &b) {
if (a.l / B != b.l / B) {
return a.l < b.l;
} else {
return a.r < b.r;
}
}
inline void add(int x) {
res += c[x];
c[x]++;
}
inline void del(int x) {
c[x]--;
res -= c[x];
}
int main() {
ios::sync_with_stdio(0), cin.tie(0), cout.tie(0);
idx = res = 0;
cin >> n >> k >> Q;
B = sqrtl(n);
string s;
cin >> s;
s = " " + s;
vector<int> a(26);
map<vector<int>, int> mp;
vector<int> has(n + 1);
mp[a] = 0;
for (int i = 1; i <= n; i++) {
a[s[i] - 'a']++;
a[s[i] - 'a'] %= k;
if (!mp.count(a)) {
mp[a] = ++idx;
}
has[i] = mp[a];
}
c.assign(idx + 1, 0);
vector<qu> q(Q + 1);
for (int i = 1; i <= Q; i++) {
cin >> q[i].l >> q[i].r;
q[i].l--;
q[i].i = i;
}
sort(q.begin() + 1, q.end());
vector<i64> ans(Q + 1);
for (int i = 1, l = 1, r = 0; i <= Q; i++) {
while (l > q[i].l) add(has[--l]);
while (r < q[i].r) add(has[++r]);
while (l < q[i].l) del(has[l++]);
while (r > q[i].r) del(has[r--]);
ans[q[i].i] = res;
}
for (int i = 1; i <= Q; i++) {
cout << ans[i] << '\n';
}
return 0;
}
F
诈骗。
因为 \(c_i\) 始终是 \(c_{i - 1}\) 和 \(r_{i - 1}\) 加权得来,而且权重都是一样的。而答案就是 \(\sum ^{n} _{i = 1}(c_i-r_i)\),所以尽可能调整 \(r_i\) 使得 新的 \(c_{i +1}\) 变得更少就行了。没必要减缓 \(c_i\) 的下降速度,越等损失的越多。
所以由我们操控的 \(r_i\) 每次都设为 \(L\)。
void solve() {
int n, k;
cin >> n >> k;
vector<ld> r(n + 1, 1), c(n + 1);
ld p, L, R;
cin >> r[0] >> c[0] >> p >> L >> R;
for (int i = 1; i <= n; i++) {
r[i] = L;
}
for (int i = 1; i <= k; i++) {
int p;
ld v;
cin >> p >> v;
r[p] = v;
}
ld ans = 0;
for (int i = 1; i <= n; i++) {
c[i] = p * c[i - 1] + (1 - p) * r[i - 1];
ans += c[i] - r[i];
}
cout << fixed << setprecision(10);
cout << ans << '\n';
}
G
可以发现 每次都是除的话,\(\log\) 次内就能除完(\(d >= 2\))。我们可以倒着 dp 预处理走哪条路最优。
具体地,我们可以设 \(dp[i][u]\) 代表 从 \(u\) 出发,走 \(i\) 条边最多能除多少。显然这个点的第 \(i\) 个状态从 它 指向的点的第 \(i-1\) 个状态转移。故有:
其中 存在一条 从 \(u\) 到 \(v\) 的边,权是 \(w\) 。
题目保证有一条出边,所以肯定能转移。
注意转移的时候是同时转移,所以我们可以再开个数组缓冲,详见代码。
时间复杂度:\(O(\ (m + Q) * \log(\max\{x_i\})\ )\)。每个点有一条出边,\(m >= n\)。
#include <bits/stdc++.h>
using i64 = long long;
using namespace std;
const int maxn = 5e5 + 5;
vector<pair<int, int>> g[maxn];
int main() {
ios::sync_with_stdio(0), cin.tie(0), cout.tie(0);
int n, m, Q;
cin >> n >> m >> Q;
for (int i = 1; i <= m; i++) {
int u, v, w;
cin >> u >> v >> w;
g[u].emplace_back(v, w);
}
// 2 ^ 30 > 1e9
vector<vector<i64>> dp(31, vector<i64>(n + 1, 1));
for (int i = 1; i <= 30; i++) {
vector<i64> tp(n + 1, 1);
for (int u = 1; u <= n; u++) {
if (dp[i - 1][u] >= 1e9) {
continue;
}
for (auto &[v, w] : g[u]) {
tp[u] = max(tp[u], dp[i - 1][v] * w);
}
}
dp[i] = tp;
}
while (Q--) {
int p, x;
cin >> p >> x;
for (int i = 1; i <= 30; i++) {
if (dp[i][p] > x) {
cout << i << '\n';
break;
}
}
}
return 0;
}
I
\(k\) 是确定的,这提示我们使用双指针。
首先我们可以对前 \(k\) 个 1
任意组合。设 \(len\) 为 \(k\) 个 1
所占有的区间长度(包括左端和右端延伸出的0
,形如001101000
),则有 \(C^{k}_{len}\) 。
然后我们移动双指针,找到一个新的1
,必须要放掉一个老的1
。此时双指针确定的区间,自然是形如001101
,左端是 多余的0
,右端一定是1
。但其实,我们可以把右端的0
也包进来,所以提前预处理好每个1
右边有多少个0
,设为\(suf[r]\)。
得到形如001101000
。长度为 \(len = r - l +1 + suf[r]\),答案 加上 \(C^{k}_{len}\) 。
注意容斥。我们如果不动 r
及其右边的0
的话,约等于没选这个新的1
。这种状态在之前已经包含过了,再计算肯定是不对的,所以我们要 减去这种情况,这种情况是 在 \([l, r - 1]\)中 写入 \(k - 1\) 个 1
。
写的很丑。双指针,\(O(n)\)。
#include <bits/stdc++.h>
using i64 = long long;
using namespace std;
const int mod = 998244353;
const int maxn = 1e5 + 5;
i64 f[maxn], g[maxn];
i64 ksm(i64 a, i64 n) {
i64 ans = 1;
a %= mod;
while (n) {
if (n & 1) {
ans = ans * a % mod;
}
a = a * a % mod;
n >>= 1;
}
return ans;
}
void init() {
f[0] = 1;
g[0] = 1;
for (int i = 1; i < maxn; i++) {
f[i] = f[i - 1] * i % mod;
g[i] = g[i - 1] * ksm(i, mod - 2) % mod;
}
}
inline i64 C(i64 n, i64 m) {
if (m > n || n < 0 || m < 0) {
return 0;
}
if (m == 0) {
return 1;
}
return f[n] * g[m] % mod * g[n - m] % mod;
}
void solve() {
int n, k;
cin >> n >> k;
string s;
cin >> s;
s = " " + s;
int sz = 0;
int fl = 0;
i64 ans = 0;
int cur = 0;
vector<int> suf(n + 1);
for (int i = n; i >= 1; i--) {
cur += s[i] == '0';
suf[i] = cur;
if (s[i] == '1') {
cur = 0;
}
}
for (int l = 1, r = 1; r <= n; r++) {
if (s[r] == '1') {
sz++;
while (l <= r && sz > k) {
if (s[l] == '1') {
sz--;
}
l++;
}
if (sz == k) {
ans = (ans + C(r - l + 1 + suf[r], k)) % mod;
if (!fl) {
fl = 1;
} else {
ans = (ans - C(r - l, k - 1) + mod) % mod;
}
}
}
}
cout << ans << '\n';
}
int main() {
init();
ios::sync_with_stdio(0), cin.tie(0), cout.tie(0);
int tt = 1;
cin >> tt;
while (tt--) {
solve();
}
return 0;
}
K
不难发现 按住一个石像转 1 次 + 按住自己转 3 次 = 把一个石像往反方向转 1 次 。
另外操作二最多不超过 3 次,所以我们可以无限用,然后\(\mod 4\) 就可以了
最终肯定是把所有雕像都调成一致,再用操作二转回 前方。调成一致调成哪个最优,反正就 4 个方向,直接枚举。
#include <bits/stdc++.h>
using i64 = long long;
using namespace std;
int main() {
ios::sync_with_stdio(0), cin.tie(0), cout.tie(0);
int n;
cin >> n;
vector<int> a(n + 1);
for (int i = 1; i <= n; i++) {
cin >> a[i];
}
vector<i64> b(4);
for (int i = 1; i <= n; i++) {
for (int j = 0; j <= 3; j++) {
b[j] += (a[i] - j + 4) % 4;
}
}
i64 ans = 1e18;
for (int i = 0; i <= 3; i++) {
ans = min(ans, b[i] + (b[i] * 3 + 4 - i) % 4);
}
cout << ans << '\n';
return 0;
}
M
n
个硬币面朝上,翻转其中k
个。问能不能、怎么把硬币分成 正面朝上的相同的两堆。
答案肯定是都能的:
我们直接分成 k
和 n - k
大小的两堆。假设 第一堆中 有 x
个被翻转了。
第一堆中k - x
个正面朝上,第二堆中 n - k - (k - x) = n - 2 * k + x
个硬币正面朝上。
所以第二堆中 n - k - (n - 2 * k + x) = k - x
个硬币被翻转。
把第二堆全翻转一下就可以了。
int n, k;
cin >> n >> k;
cout << string(k, '1') + string(n - k, '4');