生成树+次小生成树
生成树(MST)
克鲁斯卡尔(kruskal)算法
算法简介
- 时间复杂度 \(O(E \log E)\)
算法流程
- 给边的边权排序,然后依次加入,如果加入这条边后形成了环就不加入这条边
Code
#include <iostream>
#include <algorithm>
#include <vector>
#include <cmath>
using namespace std;
const int MAXN = 5000 + 10, MAXM = 2e5 + 10;
struct Qwe{
int x, y, w;
}b[MAXM];
int n, m, ANS = 0, cnt = 0;
int fa[MAXN];
bool cmp(Qwe i, Qwe j){
return i.w < j.w;
}
int Find(int x){
if(fa[x] > 0){
return fa[x] = Find(fa[x]);
}
return x;
}
bool as(int i){
int X = Find(b[i].x), Y = Find(b[i].y);
if(X != Y){
fa[X] = Y;
return 1;
}
return 0;
}
int main(){
cin >> n >> m;
for(int i = 1; i <= m; i++){
cin >> b[i].x >> b[i].y >> b[i].w;
}
sort(b + 1, b + 1 + m, cmp);
for(int i = 1; i <= m; i++){
if(as(i)){
ANS += b[i].w, cnt++;
}
}
if(cnt != n - 1){
cout << "orz";
}else{
cout << ANS;
}
return 0;
}
kruskal 重构树
每次合并时新建一节点(点权为边权),左右儿子是两个根,
设两点之间路径长度为经过的最大值,若需要最小化,则变为重构树两点 LCA 的点权。
https://www.luogu.com.cn/problem/CF1416D
https://www.luogu.com.cn/problem/P4197
https://www.luogu.com.cn/problem/P5168
应用
洛谷 - P2619 [国家集训队] Tree I
给你一个无向带权连通图,每条边是黑色或白色。让你求一棵最小权的恰好有 \(need\) 条白色边的生成树(输出边权和)。题目保证有解。每条边的颜色分 \(0\) 和 \(1\)。
边权均为 \([1,100]\) 中的整数。\(1 \le V \le 5 \cdot 10^4\) 且 \(1 \le need \le E \le 10^5\)
时间限制 2.00s,内存限制 500.00MB
解题思路
巧妙利用 kruskal 的边权排序。显然边权越小越靠前,考虑让黑白边等级分化。
于是就会想到偏移量,且边权只有 \(100\),枚举感觉会挂,那就二分吧!(但是细节多了)
二分出偏移量 \(d\) 后偏移边权,在做模板 kruskal。
可以会有一种情况:偏移量为 \(d\) 时 白边数量超了,偏移量为 \(d + 1\) 时 白边数量少了。
题目保证有解,那必定是有偏移后边权相同的黑白边,特殊处理一下即可。
Code
#include <bits/stdc++.h>
using namespace std;
using LL = long long;
const int MAXE = 1e5 + 3, MAXV = 1e5 + 3;
struct Edge{
int u, v, w, col;
}eg[MAXE];
int n, m, need;
int fa[MAXV];
int Getf(int x){
return fa[x] == x ? x : fa[x] = Getf(fa[x]);
}
pair<int, int> kruskal(int D, int op){ // op = 0 使得 sumwight 更大,op = 1 反之
for(int i = 1; i <= m; i++) eg[i].w += (eg[i].col == 0 ? D : 0);
for(int i = 1; i <= n; i++) fa[i] = i;
sort(eg + 1, eg + 1 + m, [&op](Edge i, Edge j){ return i.w == j.w ? (i.col ^ op) < (j.col ^ op) : i.w < j.w; });
int ret = 0, sumwight = 0;
for(int i = 1; i <= m; i++){
int fx = Getf(eg[i].u), fy = Getf(eg[i].v);
if(fx != fy){
fa[fx] = fy;
sumwight += (eg[i].col == 0);
ret += eg[i].w;
}
}
for(int i = 1; i <= m; i++) eg[i].w -= (eg[i].col == 0 ? D : 0);
return {ret, sumwight};
}
int main(){
cin >> n >> m >> need;
for(int i = 1; i <= m; i++){
cin >> eg[i].u >> eg[i].v >> eg[i].w >> eg[i].col;
eg[i].u++, eg[i].v++;
}
int l = -202, r = 202, mid;
while(l <= r){
mid = (l + r) >> 1;
if(kruskal(mid, 0).second < need){
r = mid - 1;
}else if(kruskal(mid, 1).second > need){
l = mid + 1;
}else break;
}
cout << kruskal(mid, 0).first - need * mid;
return 0;
}
普里姆(prim)算法
算法简介
- 类似广搜 和 dijkstra
- 暴力时间复杂度 \(O(V^2 + E)\)
- 二叉堆优化后时间复杂度 \(O((V + E) \log V)\)
算法流程
- 确定起点(可以随机确定,因为生成树最终是一个连通图)
- 加入所有一段与起点相连、另一段不与起点相连的边(这里的相连是指有连通)
- 在所有加入了的边中,选择边权最小的一条边
- 可以使用二叉堆优化
Code
#include <iostream>
#include <algorithm>
#include <vector>
#include <queue>
using namespace std;
const int MAXN = 5000 + 5;
const int MAXM = 2e5 + 5;
struct Edge{ int u, v, w; };
struct cmp{
bool operator() (Edge i, Edge j) const {
return i.w > j.w;
}
};
int n, m, ans = 0;
bool vis[MAXN];
vector<Edge> e[MAXN];
priority_queue<Edge, vector<Edge>, cmp> E;
int Prim(){
int _ans = 0, cnt = 0;
vis[1] = 1, cnt = 1;
for(Edge j : e[1]){
if(1 != j.v && vis[j.v] == 0) E.push({1, j.v, j.w});
}
while(!E.empty()){
Edge i = E.top();
E.pop();
if(vis[i.v] != vis[i.u]) _ans += i.w;
vis[i.v] = 1, vis[i.u] = 1;
for(Edge j : e[i.v]){
if(i.v != j.v && vis[j.v] == 0) E.push({i.v, j.v, j.w});
}
}
return _ans;
}
int main(){
cin >> n >> m;
for(int i = 1, U, V, W; i <= m; i++){
cin >> U >> V >> W;
e[U].push_back({0, V, W}), e[V].push_back({0, U, W});
}
ans = Prim();
for(int i = 1; i <= n; i++){
if(vis[i] == 0) ans = -1;
}
if(ans == -1){
cout << "orz";
}else{
cout << ans;
}
return 0;
}
boruvka 算法
算法简介
- 将 prim 和 kruskal 结合起来。
- 用于求解无向图的最小生成森林。
- 时间复杂度:当原图连通时,每次迭代连通块数量至少减半,算法只会迭代不超过 \(O(\log V)\) 次。当原图不联通时就是一些子问题了。故时间复杂度 \(O(E \log V)\)
算法流程
- 初始时将每个点视作一个连通块。
- 不断重复一下流程:
- 对每个连通块找到一个最近的连通块,即两个连通块之间可以选择一条边相连,距离即为所选边的边权。
- 为每个块找到最小边后,连接这些边(可以任意顺序)。
- 注:不过在连边的时候,任然需要判断是否两端点在同一连通块。
- 最后所有所选边构成一个最小生成森林。
Code
#include <bits/stdc++.h> // brovka
using namespace std;
using LL = long long;
using PII = pair<int, int>;
const int MAXN = 2e5 + 3;
struct Edge{
int u, v, w;
}e[MAXN];
int n, m, ans = 0;
int fa[MAXN];
PII mi[MAXN];
int Getf(int x){
return fa[x] == x ? x : fa[x] = Getf(fa[x]);
}
bool work(){
for(int i = 1; i <= n; i++){
mi[i] = {0, 0};
}
for(int i = 1; i <= m; i++){
int fx = Getf(e[i].u), fy = Getf(e[i].v);
if(fx != fy){
if(mi[fx].first == 0 || mi[fx].second > e[i].w){
mi[fx] = {fy, e[i].w};
}
if(mi[fy].first == 0 || mi[fy].second > e[i].w){
mi[fy] = {fx, e[i].w};
}
}
}
bool ret = 0;
for(int i = 1; i <= n; i++){
if(Getf(i) != i || mi[i].first == 0) continue;
int fx = Getf(i), fy = Getf(mi[i].first);
if(fx != fy){
fa[fx] = fy, ans += mi[i].second;
ret = 1;
}
}
return ret;
}
int main(){
ios::sync_with_stdio(0), cin.tie(0), cout.tie(0);
cin >> n >> m;
for(int i = 1, U, V, W; i <= m; i++){
cin >> U >> V >> W;
e[i] = {U, V, W};
}
for(int i = 1; i <= n; i++) fa[i] = i;
while(work());
cout << ans;
return 0;
}
应用
AT - Built?
CF - Jumping Around
CF - Xor-MST
一个无向完全图,\(n\) 个节点,任意两个节点之间的边权为 \(a_i ⊕ a_j\),求最小生成树。
\(1 \le n \le 2 \cdot 10^5\) 且 \(0 \le a_i < 2^{30}\)
时间限制2.00s 内存限制250.00MB
解法
求完全图的最小生成树,一般使用 brovka 算法。
但是有的题目不需要实际写出 brovka 算法,只需要用到其思想,比如这题。
显然需要建立一个 01-trie,那么在合并连通块的时候,就是有两个儿子的节点左右合并,那么可以直接对于每个有两个儿子的节点求向下的最小异或值。
具体见代码
代码
#include <bits/stdc++.h>
using namespace std;
using LL = long long;
using PII = pair<int, int>;
const int MAXN = 2e5 + 3;
LL ans = 0;
int n;
int a[MAXN];
int ttop = 1, trie[29 * MAXN][2], cnt[29 * MAXN];
inline void Insert(int w, int id){ // 加入 01-trie
int p = 1;
for(int l = 29; l >= 0; l--){
int col = (w >> l) & 1;
if(!trie[p][col]){
trie[p][col] = ++ttop;
}
p = trie[p][col];
}
}
LL C(int x, int y, int dep){ // 找向下延伸的最小异或值
if(dep < 0) return 0;
if(!x || !y) return 1e16;
if((trie[x][1] && trie[y][1]) || (trie[x][0] && trie[y][0])){
return min(C(trie[x][1], trie[y][1], dep - 1), C(trie[x][0], trie[y][0], dep - 1));
}
return min(C(trie[x][1], trie[y][0], dep - 1), C(trie[x][0], trie[y][1], dep - 1)) + (1ll << dep);
}
void dfs(int x, int dep){ // 深搜便利
if(trie[x][0]) dfs(trie[x][0], dep - 1);
if(trie[x][1]) dfs(trie[x][1], dep - 1);
if(trie[x][0] && trie[x][1]){ // 找到有两个儿子的节点
ans += C(trie[x][0], trie[x][1], dep - 1) + (1ll << dep); // 当前位不同,剩下的去搜索寻找 即 C 函数
}
}
int main(){
ios::sync_with_stdio(0), cin.tie(0), cout.tie(0);
cin >> n;
for(int i = 1; i <= n; i++){
cin >> a[i], Insert(a[i], i);
}
dfs(1, 29);
cout << ans;
return 0;
}
次小生成树
对于求非严格次小生成树:
- 枚举不在最小生成树中的一条边,然后求树上两点之间最大边权,然后替换。
对于求严格次小生成树。
- 一样的做法,只不过需要求最大边权和次大边权。
只找到了 严格次小生成树 的题目:https://www.luogu.com.cn/problem/P4180