2025 CCPC 网络赛 CG
C. 造桥与砍树
解法一:魔改 prim。
回顾经典的prim算法,从第一个点开始,每次找权值最小的边加入,然后更新到每个点的距离和边的起点,以此往复。
对于这道题来讲,每次更新到其他 \(n-1\) 个点的最小权值显然复杂度太大,但按照贪心的来讲,我们其实每次只选最小的那条边加入而已,所以对于点 \(u\) 来说每次二分去找到权值最接近 \(k-t_u\) 的点即可,如果没有,那就选点权最小的加进去。
按照 prim 算法,维护一条边的两个点,如果我们准备要加入的这个点和我们之前已经连边的点是一个集合,那就不用再加这个点了;否则加入这个点,再对两个点都选取一条最近边加入堆维护。
点击查看代码
#include <bits/stdc++.h>
using namespace std;
using i64 = long long;
void solve() {
int n, k;
cin >> n >> k;
set<array<int,2>> s;
vector<int> a(n + 1);
for (int i = 1; i <= n; i += 1) {
cin >> a[i];
a[i] %= k;
s.insert({a[i], i});
}
auto get = [&](int x)->array<int,2> {
auto t = s.lower_bound({k - x, 0});
return t == s.end() ? *s.begin() : *t;
};
priority_queue<array<int,3>,vector<array<int,3>>,greater<>> Q;
auto [w1, st] = *s.begin();
s.erase(s.begin());
auto [w2, nxt] = get(w1);
i64 ans = 0;
Q.push({(w1 + w2) % k, st, nxt});
while (Q.size()) {
auto [d, u, v] = Q.top();
Q.pop();
if (!s.count({a[v], v})) {
continue;
}
ans += d;
s.erase(s.lower_bound({a[v], v}));
if (s.empty()) {
break;
}
auto [w1, x] = get(a[v]);
Q.push({(w1 + a[v]) % k, v, x});
auto [w2, y] = get(a[u]);
Q.push({(w2 + a[u]) % k, u, y});
}
cout << ans << "\n";
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
int t;
cin >> t;
while (t--) {
solve();
}
return 0;
}
解放二:Boruvka 做法。
不妨先把所有点权都模 $ k $ 处理,问题答案不变。然后把点权从小到大排序,观察边权的邻接矩阵。
忽略边界情况,观察每一行。矩阵的第 $ i $ 行存在一个“溢出分界点”,记为 $ loc_i $,体现为 $ loc_i $ 前面的边权都是 $ a_i + a_j (1 \leq j < loc_i) $,后边的边权都是 $ a_i + a_j - k(loc_i \leq j \leq n) $。记 $ v_{x,y} $ 表示边 \((x, y)\) 的边权。此时由于点权有序,不难发现点权满足$$v_{i, loc_i} \leq v_{i, loc_i+1} \leq \cdots \leq v_{i, n} \leq v_{i, 1} \leq v_{i, 2} \leq \cdots \leq v_{i, loc_i-1}$$
不难发现 \(\{loc_i\}\) 数组是单调不增的。邻接矩阵因此被一段(沿主对角线轴对称的)阶梯形状的轮廓线分成了两个部分(溢出区,非溢出区)。摘自知乎《如何评价 2025 CCPC 网络赛?》中George Plover的回答
根据以上这个性质,可以发现对于点 \(i\) 来说,最短的连边点是从 \(loc_i\) 往右增大的,所以用 boruvka 算法可以很快找到每个点最近的那个点,对于 \(loc_i\) 从右移到 \(1\) 的点,就没必要继续右移了,这个时候是处于‘非溢出区’,往右移一定不会更优,且此时这个点应该也被合并到其他联通块了,只要看和 \(1\) 的点能不能连通即可。
感觉和上面佬的做法很像了,就是套了个 boruvka 的壳,当然还有其他解法,比如各种数据结构乱搞的
点击查看代码
#include <bits/stdc++.h>
using namespace std;
using i64 = long long;
struct DSU {
std::vector<int> f, siz;
DSU() {}
DSU(int n) {
init(n);
}
void init(int n) {
f.resize(n);
std::iota(f.begin(), f.end(), 0);
siz.assign(n, 1);
}
int find(int x) {
while (x != f[x]) {
x = f[x] = f[f[x]];
}
return x;
}
bool same(int x, int y) {
return find(x) == find(y);
}
bool merge(int x, int y) {
x = find(x);
y = find(y);
if (x == y) {
return false;
}
siz[x] += siz[y];
f[y] = x;
return true;
}
int size(int x) {
return siz[find(x)];
}
};
void solve() {
int n, k;
cin >> n >> k;
vector<int> a(n + 1);
for (int i = 1; i <= n; i += 1) {
cin >> a[i];
a[i] %= k;
}
sort(a.begin() + 1, a.end());
int p = n + 1;
vector<int> nxt(n + 1);
for (int i = 1; i <= n; i += 1) {
while (p - 1 >= 1 && a[p - 1] + a[i] >= k) {
p -= 1;
}
nxt[i] = p;
}
i64 ans = 0;
DSU dsu(n + 1);
while (true) {
vector<tuple<int,int,int>> edge;
for (int i = 1; i <= n; i += 1) {
int Min = INT_MAX, to = -1, x = i;
while (nxt[x] <= n && dsu.same(x, nxt[x])) {
nxt[x] += 1;
}
if (nxt[x] <= n && !dsu.same(x, nxt[x])) {
int w = (a[nxt[x]] + a[x]) % k;
if (w < Min) {
Min = w, to = nxt[x];
}
}
else if (nxt[x] > n && !dsu.same(x, 1)) {
int w = (a[1] + a[x]) % k;
if (w < Min) {
Min = w, to = 1;
}
}
if (to != -1) {
edge.emplace_back(Min, i, to);
}
}
sort(edge.begin(), edge.end());
for (auto &[w, u, v] : edge) {
if (dsu.merge(u, v)) {
ans += w;
}
}
if (dsu.size(1) == n) {
break;
}
}
cout << ans << "\n";
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
int t;
cin >> t;
while (t--) {
solve();
}
return 0;
}
G. 序列与整数对
根号分治。
对出现次数小于 \(B\) 的暴力枚举,然后二分另一个数的贡献统计即可,取 \(B = \sqrt{\frac{n^2}q}\) 时,复杂度为 \(O(q\sqrt{n}\log n)\)。
直接暴力枚举也行,按哪个出现次数少就枚举哪个,然后记忆化,这样下来的复杂度貌似均摊也和上面一样。
点击查看代码
#include <bits/stdc++.h>
using namespace std;
using i64 = long long;
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
int n, q;
cin >> n >> q;
vector<int> a(n + 1);
map<int, vector<int>> mp;
for (int i = 1; i <= n; i += 1) {
cin >> a[i];
mp[a[i]].push_back(i);
}
int B = sqrt(n / q * n);
map<array<int,2>,i64> ans;
while (q --) {
int x, y;
cin >> x >> y;
if (ans.count({x, y})) {
cout << ans[ {x, y}] << "\n";
continue;
}
i64 res = 0;
int m = mp[x].size(), k = mp[y].size();
if (x == y) {
cout << 1LL * (m - 1) * m / 2 << "\n";
continue;
}
else if (m < B || m >= B && k >= B && m < k) {
for (auto &u : mp[x]) {
res += mp[y].end() - lower_bound(mp[y].begin(), mp[y].end(), u);
}
}
else {
for (auto &v : mp[y]) {
res += lower_bound(mp[x].begin(), mp[x].end(), v) - mp[x].begin();
}
}
ans[ {x, y}] = res;
cout << res << "\n";
}
return 0;
}
离线离散化,预处理大于 \(\sqrt n\) 的部分,对于两个都小于 \(\sqrt n\) 的部分,可以直接双指针处理,这部分最多也是 \(\sqrt n\) 次,所以复杂度为 \(O(n \sqrt n + q(\log n +\sqrt n))\)。
点击查看代码
#include<bits/stdc++.h>
using namespace std;
#define endl '\n'
const int N=1e5+3;
#define int long long
int n,q;
void solve() {
cin >> n >> q;
int K = sqrt(n);
vector<int> a(n + 1);
vector<int> p;
for (int i = 1; i <= n; ++i) {
cin >> a[i];
p.push_back(a[i]);
}
vector<array<int, 2>> Q;
for (int i = 0; i < q; ++i) {
int x, y;
cin >> x >> y;
Q.push_back({x, y});
p.push_back(x);
p.push_back(y);
}
std::sort(p.begin(), p.end());
p.erase(unique(p.begin(), p.end()), p.end());
int m=p.size();
vector<int> cnt(m + 3);
vector<int> mp[m +3];
for (int i = 1; i <= n; ++i) {
a[i] = lower_bound(p.begin(), p.end(), a[i]) - p.begin() + 1;
mp[a[i]].push_back(i);
cnt[a[i]]++;
}
vector<int> g1[m+3];
vector<int> g2[m+3];
for (int i = 0; i < q; ++i) {
Q[i][0] = lower_bound(p.begin(), p.end(), Q[i][0]) - p.begin() + 1;
Q[i][1] = lower_bound(p.begin(), p.end(), Q[i][1]) - p.begin() + 1;
int x = Q[i][0];
int y = Q[i][1];
g1[x].push_back(y);
g2[y].push_back(x);
}
vector<int> ans1[m+3];
vector<int> ans2[m+3];
for (int i = 1; i <= m; ++i) {
std::sort(g1[i].begin(), g1[i].end());
g1[i].erase(unique(g1[i].begin(), g1[i].end()), g1[i].end());
ans1[i].resize(g1[i].size());
std::sort(g2[i].begin(), g2[i].end());
g2[i].erase(unique(g2[i].begin(), g2[i].end()), g2[i].end());
ans2[i].resize(g2[i].size());
}
vector<int> idx(m + 3, -1);
for (int i = 1; i <= m; ++i) {
if (g1[i].empty())continue;
if (cnt[i] >= K) {
int p = 0;
for (auto x: g1[i]) {
idx[x] = p++;
}
int sum = 0;
for (int j = 1; j <= n; ++j) {
int y = a[j];
if (idx[y] != -1) {
if (cnt[y] < K) {
ans1[i][idx[y]] += sum;
}
}
if (y == i)sum++;
}
for (auto x: g1[i]) {
idx[x] = -1;
}
}
}
for (int i = 1; i <= m; ++i) {
if (g2[i].empty())continue;
if (cnt[i] >= K) {
int p = 0;
for (auto x: g2[i]) {
idx[x] = p++;
}
int sum = 0;
for (int j = n; j >= 1; --j) {
int y = a[j];
if (idx[y] != -1) {
ans2[i][idx[y]] += sum;
}
if (y == i)sum++;
}
for (auto x: g2[i]) {
idx[x] = -1;
}
}
}
for (auto [x, y]: Q) {
if (cnt[x] >= K and cnt[y] < K) {
int id= lower_bound(g1[x].begin(), g1[x].end(),y)-g1[x].begin();
cout<<ans1[x][id]<<endl;
} else if (cnt[y] >= K) {
int id= lower_bound(g2[y].begin(), g2[y].end(),x)-g2[y].begin();
cout<<ans2[y][id]<<endl;
} else {
// cout<<x<<' '<<y<<endl;
int sum = 0;
int l = 0;
for (int i = 0; i < mp[y].size(); ++i) {
auto id = mp[y][i];
while (l < mp[x].size() and mp[x][l] < id) {
l++;
}
sum += l;
// cout<<sum<<' ';
}
// cout<<endl;
cout<<sum<<endl;
}
}
}
signed main(){
ios::sync_with_stdio(false),cin.tie(0);
int t=1;
// cin>>t;
while (t--){
solve();
}
}

浙公网安备 33010602011771号