最小生成树
最小生成树
定义无向连通图的最小生成树(MST)为边权和最小的生成树。
只有连通图才有生成树,而对于非连通图,只存在生成森林。
Kruskal 算法
按边权升序排序后一次选取边尝试加入 MST,用并查集维护连通性,时间复杂度 \(O(m \log m)\) 。
inline int Kruskal() {
sort(e + 1, e + 1 + m, [](const Edge &x, const Edge &y) { return x.w < y.w; });
for (int i = 1; i <= n; ++i)
fa[i] = i;
int res = 0, cnt = n;
for (int i = 1; i <= m; ++i) {
int fx = find(e[i].u), fy = find(e[i].v);
if (fx == fy)
continue;
merge(fx, fy), res += e[i].w, --cnt;
if (cnt == 1)
return res;
}
return -1;
}
有一个结论:对任意最小生成树,仅保留权值小于 \(L\) 的边所得森林的连通性相同。
Prim 算法
维护一个生成树集合,不断向这个集合内加点。
具体来说,每次要选择距离当前生成树集合最小的一个结点加入,并更新其他结点的距离。
暴力是 \(O(n^2 + m)\) 的,用堆维护是 \(O((n + m) \log n)\) 。
inline int Prim() {
memset(dis + 1, inf, sizeof(int) * n);
memset(vis + 1, false, sizeof(bool) * n);
priority_queue<pair<int, int> > q;
dis[1] = 0, q.emplace(-dis[1], 1);
int res = 0, cnt = 0;
while (!q.empty() && cnt < n) {
int u = q.top().second;
q.pop();
if (vis[u])
continue;
vis[u] = true, res += dis[u], ++cnt;
for (auto it : G.e[u]) {
int v = it.first, w = it.second;
if (dis[v] > w)
dis[v] = w, q.emplace(-dis[v], v);
}
}
return cnt == n ? res : -1;
}
Boruvka 算法
定义一个连通块的最小边为它连向其它连通块的边中权值最小的那一条。
流程:
- 计算每个点分别属于哪个连通块。
- 遍历每条边,若两端点不连通,则用该边尝试更新两连通块的最小边。
- 如果所有连通块都没有最小边,则已经找到了 MST ,否则将各连通块最小边加入 MST。
边权相同时加入第二关键字编号区分边权相同的边。
由于每次迭代连通块数量至少减半,所以时间复杂度是 \(O(m \log n)\) 。
inline int Boruvka() {
dsu.prework(n);
int cnt = 0, res = 0;
for (;;) {
memset(shortest + 1, 0, sizeof(int) * n);
for (int i = 1; i <= m; ++i) {
if (vis[i])
continue;
int fx = dsu.find(e[i].u), fy = dsu.find(e[i].v);
if (fx == fy)
continue;
auto cmp = [](const int &x, const int &y) { return e[x].w == e[y].w ? x < y : e[x].w < e[y].w; };
if (!shortest[fx] || cmp(i, shortest[fx]))
shortest[fx] = i;
if (!shortest[fy] || cmp(i, shortest[fy]))
shortest[fy] = i;
}
bool flag = false;
for (int i = 1; i <= n; ++i)
if (shortest[i] && !vis[shortest[i]]) {
flag = true, ++cnt;
res += e[shortest[i]].w, vis[shortest[i]] = true;
dsu.merge(e[shortest[i]].u, e[shortest[i]].v);
}
if (!flag)
break;
}
return cnt == n - 1 ? res : -1;
}
MST 的唯一性
对于 Kruskal 算法,只要计算出权值相同的边能放几条,实际放了几条,若两者不同,则形成了环,此时 MST 不唯一。
次小生成树
首先有一个结论:若(严格)次小生成树存在,则总可以和最小生成树只差一条边。
非严格次小生成树
求出原图的 MST 后,我们枚举所有未选的边,记作 \((u, v, w)\) ,找到 MST 上 \(u, v\) 路径中边权最大的边,将其替换掉,记录最小值即可。
严格次小生成树
类似的,我们维护 \(u, v\) 路径上的次小值,若替换时边权不严格,则使用次小边替换。
#include <bits/stdc++.h>
typedef long long ll;
using namespace std;
const ll inf = 1e18;
const int N = 1e5 + 7, M = 3e5 + 7, LOGN = 23;
namespace Tree {
vector<pair<int, ll> > edg[N];
ll fir_max[N][LOGN], sec_max[N][LOGN];
int fa[N][LOGN];
int dep[N];
inline void AddEdge(int u, int v, ll w) {
edg[u].push_back(make_pair(v, w));
}
void dfs(int u, int f) {
dep[u] = dep[f] + 1, fa[u][0] = f, sec_max[u][0] = -inf;
for (int i = 1; (1 << i) <= dep[u]; ++i) {
fa[u][i] = fa[fa[u][i - 1]][i - 1];
ll kk[4] = {fir_max[u][i - 1], fir_max[fa[u][i - 1]][i - 1], sec_max[u][i - 1], sec_max[fa[u][i - 1]][i - 1]};
sort(kk, kk + 4);
fir_max[u][i] = kk[3];
int j = 2;
while (~j && kk[j] == kk[3])
--j;
sec_max[u][i] = ~j ? kk[j] : -inf;
}
for (pair<int, ll> it : edg[u]) {
int v = it.first;
ll w = it.second;
if (v != f)
fir_max[v][0] = w, dfs(v, u);
}
}
inline int QueryLCA(int a, int b) {
if (dep[a] < dep[b])
swap(a, b);
for (int i = LOGN - 1; ~i; --i)
if (dep[fa[a][i]] >= dep[b])
a = fa[a][i];
if (a == b)
return a;
for (int i = LOGN - 1; ~i; --i)
if (fa[a][i] != fa[b][i])
a = fa[a][i], b = fa[b][i];
return fa[a][0];
}
inline ll query(int a, int b, ll maxx) {
ll sec = -inf;
for (int i = LOGN - 1; ~i; --i)
if (dep[fa[a][i]] >= dep[b]) {
if (maxx != fir_max[a][i])
sec = max(sec, fir_max[a][i]);
else
sec = max(sec, sec_max[a][i]);
a = fa[a][i];
}
return sec;
}
} // namespace Tree
struct Edge {
int u, v;
ll w;
inline bool operator < (const Edge &rhs) const {
return w < rhs.w;
}
} e[M];
bool used[M];
ll sum;
int n, m;
namespace DSU {
int fa[N];
inline void clear() {
for (int i = 1; i <= n; ++i)
fa[i] = i;
}
inline int find(int x) {
while (x != fa[x])
fa[x] = fa[fa[x]], x = fa[x];
return x;
}
inline void merge(int x, int y) {
fa[find(x)] = find(y);
}
} // namespace DSU
inline void Kruskal() {
sort(e + 1, e + 1 + m);
DSU::clear();
int cnt = 1;
for (int i = 1; i <= m; ++i) {
int fx = DSU::find(e[i].u), fy = DSU::find(e[i].v);
if (fx == fy)
continue;
DSU::merge(fx, fy);
Tree::AddEdge(e[i].u, e[i].v, e[i].w), Tree::AddEdge(e[i].v, e[i].u, e[i].w);
used[i] = true, sum += e[i].w, ++cnt;
if (cnt == n)
break;
}
}
signed main() {
scanf("%d%d", &n, &m);
for (int i = 1; i <= m; ++i)
scanf("%d%d%lld", &e[i].u, &e[i].v, &e[i].w);
Kruskal();
Tree::dfs(1, 0);
ll ans = inf;
for (int i = 1; i <= m; ++i)
if (!used[i]) {
int lca = Tree::QueryLCA(e[i].u, e[i].v);
ll tmpa = Tree::query(e[i].u, lca, e[i].w), tmpb = Tree::query(e[i].v, lca, e[i].w);
if (max(tmpa, tmpb) > -inf)
ans = min(ans, sum - max(tmpa, tmpb) + e[i].w);
}
printf("%lld", ans == inf ? -1 : ans);
return 0;
}
Kruskal 重构树
在 Kruskal 的过程中建立一张新图,对于加入的每一条边都新建一个点, 点权设为这条边的边权,把这条边连接的两个集合设为其儿子,把它作为这两个集合的根。
不难发现这三者等价:
- 原图中两点简单路径最大边权最小值。
- MST 上两点简单路径边权最大值。
- Kruskal 重构树上两点 LCA 的点权。
MST 计数
\(n \leq 100\) 。
注意到 MST 有如下性质:
- 每种权值的边的数量是固定的。
- 不同的生成树中,某一种权值的边任意加入需要的数量后,形成的联通块状态是一样的。
考虑枚举树边的权值 \(w\) ,把权值不是 \(w\) 的树边都加入图中后进行缩点,权值为 \(w\) 的边在缩点后的图中构造 Laplace 矩阵,利用 Matrix-Tree 定理求出方案数。
signed main() {
n = read(), m = read();
for (int i = 1; i <= m; ++i)
e[i].u = read(), e[i].v = read(), e[i].w = read();
if (!Kruskal())
return puts("0"), 0;
int ans = 1;
for (int w : vec) {
dsu.clear(n);
for (auto it : tree)
if (it.w != w)
dsu.merge(it.u, it.v);
int siz = 0;
for (int i = 1; i <= n; ++i)
if (dsu.find(i) == i)
belong[i] = ++siz;
for (int i = 1; i <= n; ++i)
if (dsu.find(i) != i)
belong[i] = belong[dsu.find(i)];
memset(g, 0, sizeof(g));
for (int i = 1; i <= m; ++i)
if (e[i].w == w) {
int u = belong[e[i].u], v = belong[e[i].v];
if (u == v)
continue;
--g[u][v], --g[v][u], ++g[u][u], ++g[v][v];
}
ans = 1ll * ans * Gauss(siz - 1) % Mod;
}
printf("%d", ans);
return 0;
}
应用
给定 \(n\) 个结点的无向完全图。每个点有一个点权为 \(a_i\)。连接 \(i\) 号结点和 \(j\) 号结点的边的边权为 \(a_i\oplus a_j\)。
求这个图的 MST 的权值。
\(1\le n\le 2\times 10^5\),\(0\le a_i< 2^{30}\)。
考虑 Boruvka 算法,每次合并两个连通块。
把每个点的权值放在 Trie 树上后,实际就相当于每次选最小边合并左右两棵子树。
一个技巧:把点的权值排序后依次插入,则子树内点的标号就是连续的。
#include <bits/stdc++.h>
typedef long long ll;
using namespace std;
const int inf = 0x3f3f3f3f;
const int N = 2e5 + 7;
int a[N];
int n;
template <class T = int>
inline T read() {
char c = getchar();
bool sign = (c == '-');
while (c < '0' || c > '9')
c = getchar(), sign |= (c == '-');
T x = 0;
while ('0' <= c && c <= '9')
x = (x << 1) + (x << 3) + (c & 15), c = getchar();
return sign ? (~x + 1) : x;
}
namespace Trie {
int ch[N << 5][2];
int L[N << 5], R[N << 5];
int tot = 1;
inline void insert(int k, int id) {
int u = 1;
for (int i = 31; ~i; --i) {
if (!L[u])
L[u] = id;
R[u] = id;
int idx = k >> i & 1;
if (!ch[u][idx])
ch[u][idx] = ++tot;
u = ch[u][idx];
}
if (!L[u])
L[u] = id;
R[u] = id;
}
inline int query(int u, int d, int k) {
int res = 0;
for (int i = d; ~i; --i)
if (ch[u][k >> i & 1])
u = ch[u][k >> i & 1];
else if (ch[u][~k >> i & 1])
u = ch[u][~k >> i & 1], res |= 1 << i;
else
return 0;
return res;
}
ll dfs(int u, int d) {
if (d == -1)
return 0;
if (ch[u][0] && ch[u][1]) {
int ans = inf;
for (int i = L[ch[u][0]]; i <= R[ch[u][0]]; ++i)
ans = min(ans, query(ch[u][1], d - 1, a[i]) | (1 << d));
return ans + dfs(ch[u][0], d - 1) + dfs(ch[u][1], d - 1);
} else if (ch[u][0])
return dfs(ch[u][0], d - 1);
else if (ch[u][1])
return dfs(ch[u][1], d - 1);
else
return 0;
}
} // namespace Trie
signed main() {
n = read();
for (int i = 1; i <= n; ++i)
a[i] = read();
sort(a + 1, a + 1 + n);
for (int i = 1; i <= n; ++i)
Trie::insert(a[i], i);
printf("%lld", Trie::dfs(1, 31));
return 0;
}