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();
    }
}
posted @ 2025-09-21 18:15  Ke_scholar  阅读(272)  评论(10)    收藏  举报