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\) 条):

原来的距离为:

\[\mid w - m \mid = -(w - m) = m - w \]

现在变成了:

\[(x - w) - (m - w) = x - w - m + w = x - m = \Delta \]

因为有 \(s\)\(\Delta\),所以就为 \(s \cdot \Delta\)

\(w > m\)(共有 \(n - s - 1\) 条):

原来距离为:

\[\mid w - m \mid = w - m \]

现在变成了:

\[(w - x) - (w - m) = w - x - w + m = m - x = -\Delta \]

因为有 \(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 题还是太难了。

posted @ 2026-07-02 21:33  AKCoder  阅读(11)  评论(0)    收藏  举报