20260629 - 最小生成树总结
依旧不按顺序过题。
J - 货车运输
罚时原因:min 求成 max 了。
设 \((V', E')\) 为图 \((V, E)\) 的 MST,则 \(E\) 中每条边都不能被替换成更小的。
我们发现,如果要使得最小值最大,我们一定要走 MST 上的边。
问题也就转化成了树上路径最小值,用 LCA 求即可。
#include <bits/stdc++.h>
using namespace std;
#define ll long long
#define ull unsigned long long
#define db double
#define all(x) (x).begin(), (x).end()
#define inf (1 << 30)
#define lnf (1LL << 60)
typedef pair<int, int> PII;
typedef array<int, 3> ARR;
constexpr int N = 1e4 + 7;
constexpr int P = 998244353;
int n, m, q, fa[N];
bool vis[N];
int f[N][21], g[N][21], dep[N];
vector<array<int, 3>> ve;
vector<PII> adj[N];
int find(int x) {
return fa[x] == x ? x : fa[x] = find(fa[x]);
}
void dfs(int u, int from) {
vis[u] = true;
for (auto [v, w] : adj[u]) {
if (v == from) continue;
f[v][0] = u;
g[v][0] = w;
dep[v] = dep[u] + 1;
dfs(v, u);
}
}
void init() {
for (int j = 1; j <= 20; j++) {
for (int i = 1; i <= n; i++) {
f[i][j] = f[f[i][j - 1]][j - 1];
g[i][j] = min(g[i][j - 1], g[f[i][j - 1]][j - 1]);
}
}
}
void Kruskal() {
sort(ve.begin(), ve.end(), [&](const ARR &x, const ARR &y) {
return x[0] > y[0];
});
iota(fa + 1, fa + n + 1, 1);
for (auto [w, x, y] : ve) {
int x_ = find(x), y_ = find(y);
if (x_ != y_) {
fa[x_] = y_;
adj[x].push_back({y, w});
adj[y].push_back({x, w});
}
}
}
int lca(int u, int v) {
if (dep[u] < dep[v]) swap(u, v);
int d = dep[u] - dep[v], ans = inf;
for (int i = 0; d; d >>= 1, i++) {
if (d & 1) {
ans = min(ans, g[u][i]);
u = f[u][i];
}
}
if (u == v) return ans;
for (int i = 20; i >= 0; i--) {
if (f[u][i] != f[v][i]) {
ans = min({ans, g[u][i], g[v][i]});
u = f[u][i], v = f[v][i];
}
}
ans = min({ans, g[u][0], g[v][0]});
return ans;
}
int main() {
scanf("%d%d", &n, &m);
for (int i = 1; i <= m; i++) {
int u, v, w;
scanf("%d%d%d", &u, &v, &w);
ve.push_back({w, u, v});
}
Kruskal();
for (int i = 1; i <= n; i++) if (!vis[i]) dfs(i, 0);
init();
scanf("%d", &q);
while (q--) {
int x, y;
scanf("%d%d", &x, &y);
int ans = lca(x, y);
if (find(x) != find(y)) {
ans = -1;
}
printf("%d\n", ans);
}
return 0;
}
D - 买礼物
我们发现,如果对应的 \(K_i,_j\) 连一条边,MST 就是相关联的最小值。
如果新建一个点,连接所有的点,就转换成了 MST 问题。
#include <bits/stdc++.h>
using namespace std;
#define ll long long
#define ull unsigned long long
#define db double
#define all(x) (x).begin(), (x).end()
#define inf (1 << 30)
#define lnf (1LL << 60)
typedef pair<int, int> PII;
typedef array<int, 3> ARR;
constexpr int N = 1e4 + 7;
constexpr int P = 998244353;
int n, m, q, fa[N];
vector<array<int, 3>> ve;
int find(int x) {
return fa[x] == x ? x : fa[x] = find(fa[x]);
}
void Kruskal() {
sort(ve.begin(), ve.end(), [&](const ARR &x, const ARR &y) {
return x[0] < y[0];
});
int ans = 0;
iota(fa + 1, fa + m + 2, 1);
for (auto [w, x, y] : ve) {
int x_ = find(x), y_ = find(y);
if (x_ != y_) {
fa[x_] = y_;
ans += w;
}
}
printf("%d\n", ans);
}
int main() {
scanf("%d%d", &n, &m);
for (int i = 1; i <= m; i++) {
for (int j = 1; j <= m; j++) {
int x;
scanf("%d", &x);
if (!x) continue;
ve.push_back({x, i, j});
}
}
for (int i = 1; i <= m; i++) ve.push_back({n, m + 1, i});
Kruskal();
return 0;
}
I - MinOr Tree
我们发现,\((100)_2\) 一定是比 \((011)_2\) 要大的。
所以,我们可以按位贪心,每次优先选 \(0\)。
如果大于原来的答案,PASS。
如果当前位置是 \(1\) 了,PASS。
然后判断是不是生成树就好了。
可以看看有不有 \(n - 1\) 条边,或用 DSU 判断。
#include <bits/stdc++.h>
using namespace std;
#define ll long long
#define ull unsigned long long
#define db double
#define all(x) (x).begin(), (x).end()
#define inf (1 << 30)
#define lnf (1LL << 60)
typedef pair<int, int> PII;
typedef array<int, 3> ARR;
constexpr int N = 2e5 + 7;
constexpr int P = 998244353;
int n, m, fa[N];
vector<array<ll, 3>> ve;
int find(int x) {
return fa[x] == x ? x : fa[x] = find(fa[x]);
}
void solve() {
ve.clear();
scanf("%d%d", &n, &m);
for (int i = 1; i <= m; i++) {
int u, v, w;
scanf("%d%d%d", &u, &v, &w);
ve.push_back({w, u, v});
}
ll ans = 0;
for (int i = 32; i >= 0; i--) {
iota(fa + 1, fa + n + 1, 1);
bool ok = false;
for (auto [w, x, y] : ve) {
if ((w | ans) >= (ans | (1LL << i))) continue;
if (w & (1LL << i)) continue;
int x_ = find(x), y_ = find(y);
if (x_ != y_) {
fa[x_] = y_;
}
}
for (int j = 1; j <= n; j++) if (find(1) != find(j)) ok = true;
if (ok) ans |= (1LL << i);
}
printf("%lld\n", ans);
}
int main() {
int oT_To = 1;
scanf("%d", &oT_To);
while (oT_To--) solve();
return 0;
}
F - Sum of Maximum Weights
我们发现,如果是弱化版 [APIO2018] 铁人两项的圆方树问题(应该是这个),我们可以直接枚举边,然后看两边子树大小,然后相乘(换根也不阻止你)。
这里可以跑一边 MST,过程中记录子树大小(欸,不就是启发式合并吗?),然后就是弱化版的做法了。
为什么呢?
因为当前选边肯定是最大的边,两侧经过的相乘就是最大值的路径。
#include <bits/stdc++.h>
using namespace std;
#define ll long long
#define ull unsigned long long
#define db double
#define all(x) (x).begin(), (x).end()
#define inf (1 << 30)
#define lnf (1LL << 60)
typedef pair<int, int> PII;
typedef array<int, 3> ARR;
constexpr int N = 1e5 + 7;
constexpr int P = 998244353;
int n, m, q, fa[N], siz[N];
vector<array<int, 3>> ve;
int find(int x) {
return fa[x] == x ? x : fa[x] = find(fa[x]);
}
void Kruskal() {
sort(ve.begin(), ve.end(), [&](const ARR &x, const ARR &y) {
return x[0] < y[0];
});
ll ans = 0;
iota(fa + 1, fa + n + 1, 1);
fill(siz + 1, siz + n + 1, 1);
for (auto [w, x, y] : ve) {
int x_ = find(x), y_ = find(y);
if (x_ != y_) {
fa[x_] = y_;
ans += (ll)siz[x_] * siz[y_] * w;
siz[y_] += siz[x_];
}
}
printf("%lld\n", ans);
}
int main() {
scanf("%d", &n);
for (int i = 1; i < n; i++) {
int u, v, w;
scanf("%d%d%d", &u, &v, &w);
ve.push_back({w, u, v});
}
Kruskal();
return 0;
}
E - Built?
我们发现,两个点相邻肯定排序后相距不是很远。
所以,按照 \(x\) 排序取 \(20\) 个点,按照 \(y\) 排序取 \(20\) 个点,然后跑 MST。
对吗?
其实可以拆点,见 Moya_rao 巨佬的 blogs。
#include <bits/stdc++.h>
using namespace std;
#define ll long long
#define ull unsigned long long
#define db double
#define all(x) (x).begin(), (x).end()
#define inf (1 << 30)
#define lnf (1LL << 60)
typedef pair<int, int> PII;
typedef array<int, 3> ARR;
constexpr int N = 1e5 + 7;
constexpr int P = 998244353;
int n, m, q, fa[N];
vector<array<int, 3>> ve, vc, vk;
int find(int x) {
return fa[x] == x ? x : fa[x] = find(fa[x]);
}
void Kruskal() {
sort(ve.begin(), ve.end(), [&](const ARR &x, const ARR &y) {
return x[0] < y[0];
});
ll ans = 0, t = m;
iota(fa + 1, fa + n + 1, 1);
for (auto [w, x, y] : ve) {
int x_ = find(x), y_ = find(y);
if (x_ != y_) {
fa[x_] = y_;
ans += w;
}
}
printf("%lld\n", ans);
}
int main() {
scanf("%d", &n);
m = n;
for (int i = 1; i <= m; i++) {
int u, v;
scanf("%d%d", &u, &v);
vc.push_back({u, v, i});
vk.push_back({v, u, i});
}
sort(all(vc));
sort(all(vk));
for (int i = 0; i < m; i++) {
for (int j = max(0, i - 20); j < i; j++) {
auto [x1, y1, id1] = vc[i];
auto [x2, y2, id2] = vc[j];
ve.push_back({min(abs(x1 - x2), abs(y1 - y2)), id1, id2});
}
}
for (int i = 0; i < m; i++) {
for (int j = max(0, i - 20); j < i; j++) {
auto [x1, y1, id1] = vk[i];
auto [x2, y2, id2] = vk[j];
ve.push_back({min(abs(x1 - x2), abs(y1 - y2)), id1, id2});
}
}
Kruskal();
return 0;
}
G - Choose Two and Eat One
我们发现,\(N \le 500\),然后就没了?
还要点 quickpow。
#include <bits/stdc++.h>
using namespace std;
#define ll long long
#define ull unsigned long long
#define db double
#define all(x) (x).begin(), (x).end()
#define inf (1 << 30)
#define lnf (1LL << 60)
typedef pair<int, int> PII;
typedef array<ll, 3> ARR;
constexpr int N = 500 + 7;
constexpr int P = 998244353;
int n, m, q, fa[N], a[N];
vector<array<ll, 3>> ve;
int find(int x) {
return fa[x] == x ? x : fa[x] = find(fa[x]);
}
ll quickpow(ll a, ll b) {
ll res = 1;
for (; b; b >>= 1) {
if (b & 1) res = (res * a) % m;
a = (a * a) % m;
}
return res;
}
void Kruskal() {
sort(ve.begin(), ve.end(), [&](const ARR &x, const ARR &y) {
return x[0] > y[0];
});
ll ans = 0;
iota(fa + 1, fa + n + 1, 1);
for (auto [w, x, y] : ve) {
int x_ = find(x), y_ = find(y);
if (x_ != y_) {
fa[x_] = y_;
ans += w;
}
}
printf("%lld\n", ans);
}
int main() {
scanf("%d%d", &n, &m);
for (int i = 1; i <= n; i++) scanf("%d", &a[i]);
for (int i = 1; i <= n; i++) for (int j = 1; j < i; j++)
ve.push_back({(quickpow(a[i], a[j]) + quickpow(a[j], a[i])) % m, i, j});
Kruskal();
return 0;
}
H - Shichikuji and Power Grid
我们发现 Prim 复活了?
Kruskal 做法很恶心。
首先先考虑全建变电站,然后一步一步加边,然后每次取最小值,替换边就好了。
#include <bits/stdc++.h>
using namespace std;
#define ll long long
#define ull unsigned long long
#define db double
#define all(x) (x).begin(), (x).end()
#define inf (1 << 30)
#define lnf (1LL << 60)
typedef pair<int, int> PII;
typedef array<ll, 3> ARR;
constexpr int N = 2000 + 7;
constexpr int P = 998244353;
int n, m, q, fa[N], c[N], k[N], idx[N];
vector<array<ll, 3>> ve, vc;
int find(int x) {
return fa[x] == x ? x : fa[x] = find(fa[x]);
}
void Kruskal() {
sort(ve.begin(), ve.end(), [&](const ARR &x, const ARR &y) {
return x[0] < y[0];
});
ll ans = accumulate(c + 1, c + n + 1, 0LL);
vector<PII> res;
iota(fa + 1, fa + n + 1, 1);
iota(idx + 1, idx + n + 1, 1);
for (auto [w, x, y] : ve) {
int x_ = find(x), y_ = find(y), lct = max(c[x_], c[y_]);
if (x_ != y_ && w <= lct) {
fa[x_] = y_;
int deltal = -lct + w;
ans = ans + deltal; // ans + Δl
int minv = min(c[x_], c[y_]);
if (minv == c[x_]) idx[y_] = idx[x_];
res.push_back({x, y});
c[y_] = min(c[y_], c[x_]);
}
}
printf("%lld\n", ans);
int cnt = 0;
for (int i = 1; i <= n; i++) if (fa[i] == i) ++cnt;
printf("%d\n", cnt);
for (int i = 1; i <= n; i++) if (fa[i] == i) printf("%d ", idx[i]);
puts("");
printf("%d\n", res.size());
for (auto [x, y] : res) printf("%d %d\n", x, y);
}
int main() {
scanf("%d", &n);
m = n;
for (int i = 1; i <= m; i++) {
int u, v;
scanf("%d%d", &u, &v);
vc.push_back({u, v});
}
for (int i = 1; i <= n; i++) scanf("%d", &c[i]);
for (int i = 1; i <= n; i++) scanf("%d", &k[i]);
for (int i = 1; i <= n; i++) {
for (int j = i + 1; j <= n; j++) {
auto [x1, y1, _] = vc[i - 1];
auto [x2, y2, _] = vc[j - 1];
ve.push_back({(ll)(k[i] + k[j]) * (abs(x1 - x2) + abs(y1 - y2)), i, j});
}
}
Kruskal();
return 0;
}
A \(\sim\) C?
“太难了”,这个就算了吧(我不会告诉你我最后做的
K - Spanning Tree Queries
前置知识:小学二年级的函数,MST。
建议使用 desmos 配套学习。
我们发现,两个图像的交点在他们的中点。
首先,MST 使用 Kruskal 求的时候只跟大小关系有关。
所以,当大小关系改变时,当且仅当跨过了中点。
所以,能改变 MST 关系的询问只有 \(m^2\) 个。
每一次跑一遍 MST,总的复杂度是 \(O(m^3\log_2m)\)。
但这样还是过不去。
询问时如果没有在交点上的不久炸了吗?
所以,我们要考虑偏移量。
因为从交点出发,一定是一个一次函数。
设偏移量为 \(\Delta\),交点为 \(m\),则询问点变成了 \(m + \Delta\),我们就设 \(x\) 就为 \(m + \Delta\)。
设左侧单调递减的部分的个数为 \(s\),则右侧为 \(n - 1 - s\),有 \(n - 1\) 条边。
对于某一条被选中的边,权值为 \(w\)。
如果 \(w \le m\)(共有 \(s\) 条):
原来的距离为:
现在变成了:
因为有 \(s\) 个 \(\Delta\),所以就为 \(s \cdot \Delta\)。
若 \(w > m\)(共有 \(n - s - 1\) 条):
原来距离为:
现在变成了:
因为有 \(n - s - 1\) 个,所以就为 \(s \cdot -\Delta\)。
然后就做完了。
坑点:要将所有的 \(w\),\(\lfloor \dfrac{w}{2} \rfloor\),\(\lceil \dfrac{w}{2} \rceil\) 加入 Kruskal 中,不然会 WA test 6。
代码解析:
void init() {
vid.push_back(0); // 要添加 0,0 表示正常 MST
for (auto [w, u, v] : vec) vid.push_back(w); // 加入 w 这条边
for (int i = 0; i < m; i++) {
for (int j = i + 1; j < m; j++) {
int v0 = vec[i][0], v1 = vec[j][0];
vid.push_back((v0 + v1) / 2); // 加入 floor(w / 2) 这条边
vid.push_back((v0 + v1 + 1) / 2); // 加入 ceil(w / 2) 这条边
}
}
sort(all(vid)); // 排序并去重
vid.erase(unique(all(vid)), vid.end());
for (auto i : vid) kruskal(i); // 跑 Kruskal
}
void kruskal(int mid) {
// 正常 Kruakal,然后注意排序的问题,我们肯定想让 bug 经可能大
vector<array<int, 3>> v = vec;
for (int i = 1; i <= n; i++) fa[i] = i;
sort(all(v), [&](const ARR &A, const ARR &B) { // 注意排序
if (abs(A[0] - mid) != abs(B[0] - mid))
return abs(A[0] - mid) < abs(B[0] - mid);
return A[0] > B[0];
});
ll ans = 0, bug = 0;
for (auto [w, x, y] : v) {
int w1 = abs(w - mid);
if (unite(x, y)) {
if (w <= mid) ++bug; // 记录 <= mid 的个数
ans += w1;
}
}
vans.push_back(make_pair(ans, bug)); // 增加答案
}
int main() {
scanf("%d%d", &n, &m);
for (int i = 1; i <= m; i++) {
int u, v, w;
scanf("%d%d%d", &u, &v, &w);
vec.push_back({w, u, v});
}
init(); // 预处理
scanf("%d%d%d%d%d", &p, &k, &a, &b, &c);
auto calc = [&](ll x) -> ll {
auto idv = upper_bound(all(vid), x); // 找到第一个大于他的,然后 -1 表示第一个小于等于他的
int id = idv - vid.begin() - 1;
auto [ans, siz] = vans[id];
ll deltaq = x - vid[id];
return ans + deltaq * siz - deltaq * (n - 1 - siz);
};
ll q = 0;
ll xans = 0;
for (auto &j : vid) debug(j);
for (auto &i : vans) debug(i);
for (int i = 1; i <= p; i++) {
scanf("%lld", &q);
xans ^= calc(q);
debug(calc(q));
}
debug("");
for (int i = p + 1; i <= k; i++) {
q = ((ll)q * a + b) % c, xans ^= calc(q);
debug(calc(q));
}
printf("%lld\n", xans);
return 0;
}
PS:debug 表示调试信息。
笑点分析:调试了 inf 世纪。
总结
发现没,所有的总结都有我们发现,我们注意力堪比拉马努金。
我爱写 TJ。
K 题还是太难了。

浙公网安备 33010602011771号