[题解] 2023 CCPC 黑龙江省赛(A、B、C、E、F、G)The 18th Heilongjiang Provincial Collegiate Programming Contest
题目:https://codeforces.com/gym/104363
vp 赛时就过了 3 个题,调 B 调红了。
A
注意到前两个样例答案是 \(2^k−1\) ,写个快速幂测一下第三个样例果然是,直接交。
时间复杂度 \(O(\log{k})\) 。也可以手动一个一个乘,边乘边取模,那样是 \(O(k)\)。
Code
#include <bits/stdc++.h>
using i64 = long long;
using namespace std;
const int mod = 998244353;
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;
}
int main() {
ios::sync_with_stdio(0), cin.tie(0), cout.tie(0);
int n;
cin >> n;
cout << ksm(2, n - 1);
return 0;
}
B
首先考虑怎样拿最优。
规则:可以 01 交替拿,拿完之后两边的合并到一起。
不难发现 连续的0(下文称 00) 和连续的1(下文称 11)阻挡了一次拿完。
如果我们每次 从中间 拿走一条链,分为种情况:两侧相同(都是 00、都是11),两侧不同(一侧是00、一侧是11)。
对于两侧相同的情况,拿完之后,左右两边原本分段的 2 条链 还是需要拿 2 次。
对于两侧不同的情况,拿完之后,左右两边原本分段的 2 条链 可以合并成 1 条。
如果我们每次 从两边 拿走一条链,原本需要拿的 1 侧还是需要拿 1 次。
综上不难发现最优解是先拿 两侧不同的情况,这比其他情况可以节省 1 次的拿。
如何计算答案呢?
注意到我们可以先拿 00 和 11 中间的 链,然后凑成 两侧不同的情况 。所以 两侧不同的情况 种数 一共有 \(\min\{cnt(00), cnt(11)\}\) 种。
暂时忽略 两侧不同的情况 。我们需要拿 \(cnt(00) + cnt(11) + 1\) 次,才能把串拿完。
现在我们只需要拿 \(cnt(00) + cnt(11) + 1 - \min\{cnt(00), cnt(11)\} = \max\{cnt(00), cnt(11)\} + 1\) 次就可以把链拿完。
对于这个数据范围(1e6),统计 00 和 11 的个数我们可以用线段树,在 pushup 和 query 的时候合并左右孩子的结果。我们需要记录每个区间的 和 还有 区间左右两侧是 0 还是 1(上传的时候用来判断是否能产生新的 00 和 11)。
时间复杂度 \(O(n\log{n}+q\log{n})\) 。
Code
#include <bits/stdc++.h>
using i64 = long long;
using namespace std;
const int maxn = 1e6 + 6;
struct SegTree {
struct Node {
int l, r, v0, v1;
bool L, R, laz;
} t[maxn * 4];
int a[maxn];
inline void pushup(int p) {
auto &me = t[p];
auto &lc = t[p << 1], rc = t[p << 1 | 1];
me.v0 = lc.v0 + rc.v0;
me.v1 = lc.v1 + rc.v1;
me.v1 += lc.R == rc.L && lc.R == 1;
me.v0 += lc.R == rc.L && lc.R == 0;
me.L = lc.L;
me.R = rc.R;
}
inline void pushdown(int p) {
auto &lc = t[p << 1], &rc = t[p << 1 | 1];
if (t[p].laz) {
swap(lc.v0, lc.v1);
swap(rc.v0, rc.v1);
lc.L = !lc.L;
lc.R = !lc.R;
rc.L = !rc.L;
rc.R = !rc.R;
t[p << 1].laz ^= t[p].laz;
t[p << 1 | 1].laz ^= t[p].laz;
t[p].laz = 0;
}
}
void build(int p, int l, int r) {
t[p].l = l, t[p].r = r;
t[p].L = a[l], t[p].R = a[r];
t[p].laz = 0;
t[p].v0 = t[p].v1 = 0;
if (l == r) {
return;
}
int mid = l + r >> 1;
build(p << 1, l, mid);
build(p << 1 | 1, mid + 1, r);
pushup(p);
}
void add(int p, int l, int r) {
if (l <= t[p].l && t[p].r <= r) {
swap(t[p].v0, t[p].v1);
t[p].L = !t[p].L;
t[p].R = !t[p].R;
t[p].laz = !t[p].laz;
return;
}
pushdown(p);
int mid = t[p].l + t[p].r >> 1;
if (l <= mid)
add(p << 1, l, r);
if (r > mid)
add(p << 1 | 1, l, r);
pushup(p);
}
array<int, 4> query(int p, int l, int r) {
if (l <= t[p].l && t[p].r <= r) {
return {t[p].L, t[p].R, t[p].v0, t[p].v1};
}
pushdown(p);
int mid = t[p].l + t[p].r >> 1;
int L1, R1, L2, R2, v0 = 0, v1 = 0;
bool ok1 = 0, ok2 = 0;
if (l <= mid) {
auto res = query(p << 1, l, r);
L1 = res[0];
R1 = res[1];
v0 += res[2];
v1 += res[3];
ok1 = 1;
}
if (r > mid) {
auto res = query(p << 1 | 1, l, r);
L2 = res[0];
R2 = res[1];
v0 += res[2];
v1 += res[3];
ok2 = 1;
}
if (ok1 && ok2) {
v0 += R1 == L2 && R1 == 0;
v1 += R1 == L2 && R1 == 1;
return {L1, R2, v0, v1};
} else if (ok1) {
return {L1, R1, v0, v1};
} else {
return {L2, R2, v0, v1};
}
}
} T;
int main() {
ios::sync_with_stdio(0), cin.tie(0), cout.tie(0);
int n, Q;
cin >> n >> Q;
for (int i = 1; i <= n; i++) {
char x;
cin >> x;
T.a[i] = (x - '0');
}
T.build(1, 1, n);
while (Q--) {
char op;
int l, r;
cin >> op >> l >> r;
if (op == 'Q') {
auto res = T.query(1, l, r);
cout << max(res[2], res[3]) + 1 << '\n';
} else {
T.add(1, l, r);
}
cout << "";
}
return 0;
}
C
这道题我找到两种解法。
#1 Brute Force
思路:
一共三个转盘,先把其中一个转成 0,然后操控剩下两个转成 0,
转第一个的时候可能会影响到剩下两个,不过没关系,不用管过程,直接枚举最终结果(每个转了几次)
最终的枚举最终的状态是一共转了几圈,检查是否合法,取最小值。
Step 1: 判断结果是否合法
设 3 个盘每个盘转了 \(t[i]\) 下, \(sum = t[0] + t[1] + t[2]\)
因为一次转 2 个,所以 sum 必须是偶数
因为每次转的两个必须不一样,所以最大的那个不能超过 sum / 2
Step 2: 枚举所有可能状况
最终状态一定是三个盘都转好的。
根据 Step 1,我们需要知道每个转盘在结束时转了几下。
首先 i-th 转盘一开始就被转了 \(x[i]\) 下。我们计算好把他转好(转到0)需要 多少下,设为 \(z[i]\) 。结束的时候 i-th 转盘一定总共转了 \(z[i] + k * y[i]\) 下,我们可以枚举这个 \(k\) 。
\(z[i] = (y[i] - x[i]) \bmod y[i]\) (最后要取模防止 \(x[i]\) 本来就是 0)
按照 思路 所说,我们应当先把其中一个转好,这里我们假定为 0 号转盘。然后枚举 1号、2号转盘转的次数。
直接转好 0 号转盘是 \(z[i]\) 次。不过有一种特殊情况,当 y[1] 和 y[2] 都是奇数或者都是偶数且 y[0] 是奇数时,我们可以通过再转 y[0] 次 0 号转盘来创造合法情况,所以还可以转 \(z[0] + y[0]\) 次。
转好 1 号、2 号转盘 最多各不超过 \(z[i] + 1999*y[i]\) 次。因为本质上转 1 号 和转 2号 都是为了 转 0 而不得不转的。补偿环 0 的操作只能是 p 次「转 0&1」加 q 次「转 0&2」,它们对环 0 的净偏移量是 \((p*y[1]+q*y[2]) \bmod y[0]\) ,多转没用。
然后我们再枚举一遍 先转好 1 号转盘和先转好 2 号转盘的情况。
时间复杂度 \(O(T*\max\{x_{i}\}*\max\{y_{i}\})\) ,这里循环次数写成常数应该可以循环展开,会快很多。
Code
#include <bits/stdc++.h>
using i64 = long long;
using namespace std;
const int INF = 1e9;
void solve() {
int x[3] = {}, y[3] = {}, z[3] = {};
for (int i = 0; i < 3; i++) {
cin >> x[i];
}
for (int i = 0; i < 3; i++) {
cin >> y[i];
// 看看 第 i 个盘 还需要转多少下 才能到 0
z[i] = (y[i] - x[i]) % y[i];
}
int ans = INF;
auto work = [&](array<int, 3> t) {
for (int i = 0; i < 3; i++) {
t[i] += z[i];
}
int sum = t[0] + t[1] + t[2];
if (sum % 2 == 0 && max({t[0], t[1], t[2]}) <= sum / 2) {
ans = min(ans, sum / 2);
}
};
for (int i = 0; i < 2; i++) {
for (int j = 0; j < 2000; j++) {
for (int k = 0; k < 2000; k++) {
work({i * y[0], j * y[1], k * y[2]});
}
}
}
swap(z[0], z[1]);
swap(y[0], y[1]);
for (int i = 0; i < 2; i++) {
for (int j = 0; j < 2000; j++) {
for (int k = 0; k < 2000; k++) {
work({i * y[0], j * y[1], k * y[2]});
}
}
}
swap(z[0], z[1]);
swap(y[0], y[1]);
swap(z[0], z[2]);
swap(y[0], y[2]);
for (int i = 0; i < 2; i++) {
for (int j = 0; j < 2000; j++) {
for (int k = 0; k < 2000; k++) {
work({i * y[0], j * y[1], k * y[2]});
}
}
}
if (ans == INF) {
cout << -1 << '\n';
} else {
cout << ans << '\n';
}
}
int main() {
ios::sync_with_stdio(0), cin.tie(0), cout.tie(0);
int tt = 1;
cin >> tt;
while (tt--) {
solve();
}
return 0;
}
#2 题解思路
思路在官方题解里有。下面的代码使用 o4-mini 生成的(太强了,一发就过)。实现过程写在注释里了。
时间复杂度 \(O(y_{0} \log y_{0} + lcm(y_{1}, y_{2}))\) 。
#include <bits/stdc++.h>
using i64 = long long;
using namespace std;
// 预处理:求 l = lcm(y1, y2)。
// 求解一个长度为 y0 的数组 f,表示对于任意模 y0 的余数 w,
// 最小的可表示成 p·y1+q·y2 且 ≡ w (mod y0) 的值。
// 建图:节点 0…y0−1,
// 边 u→(u+y1)%y0,权重 y1
// 边 u→(u+y2)%y0,权重 y2
// 从 0 起跑,跑 Dijkstra得最短路 dist[w]。
// 枚举 t0 = 0…l−1,计算当前最小总步数:
// t1 = (y2 − (x2 + t0) % y2) % y2
// t2 = (y1 − (x1 + t0) % y1) % y1
// S = (x0 + t1 + t2) % y0
// 还需加上的额外步数 s = dist[(y0 − S) % y0] (若 dist[...] 为无穷,跳过)
// 总步数 = t0 + t1 + t2 + s,取最小。
// 若最终最小值仍为无穷,输出 −1,否则输出该最小值。
// 复杂度:O(y0 log y0 + l),其中 l = lcm(y1,y2),y_i ≤2000,T≤10。
const i64 INF = 1e18;
void solve() {
int x[3] = {}, y[3] = {};
for (int i = 0; i < 3; i++) cin >> x[i];
for (int i = 0; i < 3; i++) cin >> y[i];
int y0 = y[0], y1 = y[1], y2 = y[2];
// Dijkstra 求 dist[w] = min{p*y1+q*y2 | (p*y1+q*y2)%y0 == w}
// 这里的 dist[] 就是转 p 步 y0 所需要的 最小转数
vector<i64> dist(y0, INF);
priority_queue<pair<i64, int>, vector<pair<i64, int>>, greater<>> pq;
dist[0] = 0; pq.emplace(0, 0);
while (!pq.empty()) {
auto [d, u] = pq.top(); pq.pop();
if (d > dist[u]) continue;
for (int inc : {y1, y2}) {
int v = (u + inc) % y0;
i64 nd = d + inc;
if (nd < dist[v]) {
dist[v] = nd;
pq.emplace(nd, v);
}
}
}
int L = lcm(y1, y2);
i64 ans = INF;
for (int t0 = 0; t0 < L; t0++) {
int t1 = (y2 - (x[2] + t0) % y2) % y2;
int t2 = (y1 - (x[1] + t0) % y1) % y1;
int S = (x[0] + t1 + t2) % y0;
int need = (y0 - S) % y0;
if (dist[need] == INF) continue;
ans = min(ans, i64(t0) + t1 + t2 + dist[need]);
}
cout << (ans == INF ? -1 : ans) << '\n';
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
int tt; cin >> tt;
while (tt--) solve();
return 0;
}
E
注意到数据范围非常小,直接按照题意模拟即可。
时间复杂度 \(O(n!)\) 。
Code
#include <bits/stdc++.h>
using i64 = long long;
using namespace std;
int n, m;
int zi, mu;
int vis[11];
void dfs(int x) {
if (x == n) {
mu++;
if (!vis[n]) {
zi++;
}
}
if (x <= m) {
for (int i = 1; i <= n; i++) {
if (!vis[i]) {
vis[i] = 1;
dfs(x + 1);
vis[i] = 0;
}
}
} else {
if (!vis[x]) {
vis[x] = 1;
dfs(x + 1);
vis[x] = 0;
} else {
for (int i = 1; i <= n; i++) {
if (!vis[i]) {
vis[i] = 1;
dfs(x + 1);
vis[i] = 0;
}
}
}
}
}
int main() {
ios::sync_with_stdio(0), cin.tie(0), cout.tie(0);
cin >> n >> m;
dfs(1);
cout << fixed << setprecision(10);
cout << 1.0 * zi / mu << '\n';
return 0;
}
F
题意说要把这个树捋成一条链,答案就是 。
时间复杂度 \(O(n)\) 。
Code
#include <bits/stdc++.h>
using i64 = long long;
using namespace std;
const int maxn = 1e5 + 5;
vector<int> g[maxn];
int ans = 0;
void dfs(int x, int fa) {
if (g[x].empty()) {
return;
}
ans += (int)g[x].size() - 1;
for (auto &v : g[x]) {
if (v == fa) {
continue;
}
dfs(v, x);
}
}
int main() {
ios::sync_with_stdio(0), cin.tie(0), cout.tie(0);
int n;
cin >> n;
for (int i = 2; i <= n; i++) {
int x;
cin >> x;
g[x].push_back(i);
}
dfs(1, 0);
cout << ans << '\n';
return 0;
}
G
官方题解思路明确。关键有以下几点:
- 计算 OAB 的面积,是 \(\frac{1}{2} \cdot A \times B = \frac{1}{2} \left| A \right|\left| B \right|sin\angle AOB\) 。这里直接算叉积就好, 是 \(A\times B=Ax*By - Ay*Bx\) 。
- 多边形的重心,是所有顶点矢量和相加,然后 x y 坐标分别 除以顶点个数。
- 利用叉积乘自身为 0 的性质,化简公式,消去变量 "A 的顶点坐标和"。
- 叉乘满足分配律,将分子化为能排序的形式。
- 贪心求解,使分子尽可能的大,枚举分母。
时间复杂度 \(O(n\log n)\) 。
Code
#include <bits/stdc++.h>
using i64 = long long;
using namespace std;
struct Pt {
i64 x, y;
Pt() {
x = 0;
y = 0;
}
i64 operator^(const Pt &b) const {
return x * b.y - y * b.x;
}
};
int main() {
ios::sync_with_stdio(0), cin.tie(0), cout.tie(0);
int n;
cin >> n;
vector<Pt> v(n + 1);
Pt S;
for (int i = 1; i <= n; i++) {
cin >> v[i].x >> v[i].y;
S.x += v[i].x;
S.y += v[i].y;
}
vector<i64> a(n + 1);
for (int i = 1; i <= n; i++) {
a[i] = S ^ v[i];
}
sort(a.begin() + 1, a.end(), greater<>());
i64 pre = 0;
long double ans = 0;
// |A| >= 1 && |B| >= 1
for (int i = 1; i < n; i++) {
pre += a[i];
ans = max(ans, 1.0L * pre / (2.0L * i * (n - i)));
}
cout << fixed << setprecision(12) << ans << '\n';
return 0;
}
I
直接没看懂题目啊,看了半天没看懂。

浙公网安备 33010602011771号