最小生成树
定义无向连通图的最小生成树(MST)为边权和最小的生成树。
只有连通图才有生成树,而对于非连通图,只存在生成森林。
结论
- 对任意最小生成树,仅保留权值小于 L 的边所得森林的连通性相同。
- 对于完全图 (V,E) ,若 E=E1∪E2∪⋯∪Ek=E ,则 MST(E)=MST(MST(E1)∪MST(E2)∪⋯∪MST(Ek)) 。
- MST 的唯一性:对于 Kruskal 算法,只要计算出权值相同的边能放几条,实际放了几条,若两者不同则形成了环,此时 MST 不唯一。
- MST 上两点路径的最大值即为无向图中两点路径最大值的最小值。
求解
P3366 【模板】最小生成树
Kruskal 算法
按边权升序排序后依次选取边尝试加入 MST,用并查集维护连通性,时间复杂度 O(mlogm) 。
| inline int Kruskal() { |
| sort(e + 1, e + 1 + m, [](const Edge &a, const Edge &b) { |
| return a.w < b.w; |
| }); |
| |
| dsu.prework(n); |
| int res = 0, cnt = n; |
| |
| for (int i = 1; i <= m && cnt > 1; ++i) |
| if (dsu.find(e[i].u) != dsu.find(e[i].v)) |
| dsu.merge(e[i].u, e[i].v), res += e[i].w, --cnt; |
| |
| return -1; |
| } |
Prim 算法
维护一个生成树集合,不断向这个集合内加点。
具体的,每次要选择距离当前生成树集合最小的一个结点加入,并更新其他结点的距离。
暴力是 O(n2+m) 的,用堆维护是 O((n+m)logn) 。
| 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(mlogn) 。
优势在于完全图的求解,每次合并的时候可以把所有点都扫一遍,找到每个点距离最短的连通块,更新最小边。
| 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 &a, const int &b) { |
| return e[a].w == e[b].w ? a < b : e[a].w < e[b].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 后枚举所有未选的边 (u,v,w) ,找到 MST 上 u,v 路径中边权最大的边,将其替换掉,记录最小值即可。
严格次小生成树
P4180 [BJWC2010] 严格次小生成树
类似的,维护 u,v 路径上边权的次小值,若替换时边权不严格,则使用次小边替换,时间复杂度 $O()
| #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; |
| |
| struct DSU { |
| int fa[N]; |
| |
| inline void prework(int n) { |
| iota(fa + 1, fa + n + 1, 1); |
| } |
| |
| 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(y)] = find(x); |
| } |
| } dsu; |
| |
| 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 Tree { |
| struct Graph { |
| vector<pair<int, int> > e[N]; |
| |
| inline void insert(int u, int v, int w) { |
| e[u].emplace_back(v, w); |
| } |
| } G; |
| |
| ll fir[N][LOGN], sec[N][LOGN]; |
| int fa[N][LOGN]; |
| int dep[N]; |
| |
| void dfs(int u, int f) { |
| dep[u] = dep[f] + 1, fa[u][0] = f, sec[u][0] = -inf; |
| |
| for (int i = 1; i < LOGN; ++i) { |
| fa[u][i] = fa[fa[u][i - 1]][i - 1]; |
| vector<ll> vec = {-inf, fir[u][i - 1], fir[fa[u][i - 1]][i - 1], sec[u][i - 1], sec[fa[u][i - 1]][i - 1]}; |
| sort(vec.begin(), vec.end()), vec.erase(unique(vec.begin(), vec.end()), vec.end()); |
| fir[u][i] = vec.back(), sec[u][i] = vec[vec.size() - 2]; |
| } |
| |
| for (auto it : G.e[u]) { |
| int v = it.first; |
| |
| if (v != f) |
| fir[v][0] = it.second, dfs(v, u); |
| } |
| } |
| |
| inline int LCA(int x, int y) { |
| if (dep[x] < dep[y]) |
| swap(x, y); |
| |
| for (int i = 0, h = dep[x] - dep[y]; h; ++i, h >>= 1) |
| if (h & 1) |
| x = fa[x][i]; |
| |
| if (x == y) |
| return x; |
| |
| for (int i = LOGN - 1; ~i; --i) |
| if (fa[x][i] != fa[y][i]) |
| x = fa[x][i], y = fa[y][i]; |
| |
| return fa[x][0]; |
| } |
| |
| inline ll query(int x, int h, ll k) { |
| ll res = -inf; |
| |
| for (int i = 0; h; ++i, h >>= 1) |
| if (h & 1) |
| res = max(res, k == fir[x][i] ? sec[x][i] : fir[x][i]), x = fa[x][i]; |
| |
| return res; |
| } |
| } |
| |
| inline void Kruskal() { |
| sort(e + 1, e + 1 + m), dsu.prework(n); |
| int cnt = 1; |
| |
| for (int i = 1; i <= m; ++i) { |
| int u = e[i].u, v = e[i].v, w = e[i].w; |
| |
| if (dsu.find(u) == dsu.find(v)) |
| continue; |
| |
| Tree::G.insert(u, v, w), Tree::G.insert(v, u, e[i].w); |
| dsu.merge(u, v), 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 u = e[i].u, v = e[i].v, w = e[i].w, lca = Tree::LCA(u, v); |
| ll res = max(Tree::query(u, Tree::dep[u] - Tree::dep[lca], w), |
| Tree::query(v, Tree::dep[v] - Tree::dep[lca], w)); |
| |
| if (res > -inf) |
| ans = min(ans, sum - res + e[i].w); |
| } |
| |
| printf("%lld", ans == inf ? -1 : ans); |
| return 0; |
| } |
Kruskal 重构树
在 Kruskal 的过程中建立一张新图,对于加入的每一条边都新建一个点, 点权设为这条边的边权,把这条边连接的两个集合设为其儿子,把它作为这两个集合的根。
不难发现这三者等价:
- 原图中两点简单路径最大边权最小值。
- MST 上两点简单路径边权最大值。
- Kruskal 重构树上两点 LCA 的点权。
应用:P4768 [NOI2018] 归程
MST 计数
P4208 [JSOI2008] 最小生成树计数
n≤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; |
| } |
应用
CF888G Xor-MST
给定 n 个结点的无向完全图。每个点有一个点权为 ai。连接 i 号结点和 j 号结点的边的边权为 ai⊕aj。
求这个图的 MST 的权值。
1≤n≤2×105,0≤ai<230。
考虑 Kruskal 算法,每次合并两个连通块。
把每个点的权值放在 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) { |
| L[u] = (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]; |
| } |
| |
| L[u] = (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; |
| else if (ch[u][0] && ch[u][1]) { |
| int ans = inf; |
| |
| if (R[ch[u][0]] - L[ch[u][0]] + 1 <= R[ch[u][1]] - L[ch[u][1]] + 1) { |
| 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)); |
| } else { |
| for (int i = L[ch[u][1]]; i <= R[ch[u][1]]; ++i) |
| ans = min(ans, query(ch[u][0], 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; |
| } |
| } |
| |
| 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; |
| } |
AT_cf17_final_j Tree MST
给出一棵树,现有一张无向完全图,x,y 之间的边权为 wx+wy+dist(x,y) ,求该完全图的 MST。
n≤2×105
考虑点分治,对于路径过重心的点对 (x,y) ,则其边权为 (wx+dx)+(wy+dy) (d 为到重心的距离),因此该部分的 MST 即子树内所有点和 w+d 最小的点连边。可能会出现两个点在同一子树的情况边权会算大,但是不影响答案。
最后将所有 O(nlogn) 边再拿出来做一次 Kruskal 即可,时间复杂度 O(nlog2n) 。
本题也可以用 Boruvka 做到 O(nlogn) 。
| #include <bits/stdc++.h> |
| typedef long long ll; |
| using namespace std; |
| const int N = 2e5 + 7; |
| |
| struct Graph { |
| vector<pair<int, int> > e[N]; |
| |
| inline void insert(int u, int v, int w) { |
| e[u].emplace_back(v, w); |
| } |
| } G; |
| |
| struct DSU { |
| int fa[N]; |
| |
| inline void prework(int n) { |
| iota(fa + 1, fa + n + 1, 1); |
| } |
| |
| 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(y)] = find(x); |
| } |
| } dsu; |
| |
| vector<tuple<ll, int, int> > edg; |
| vector<int> vec; |
| |
| ll dis[N]; |
| int a[N], siz[N], mxsiz[N]; |
| bool vis[N]; |
| |
| int n, root; |
| |
| int getsiz(int u, int f) { |
| siz[u] = 1; |
| |
| for (auto it : G.e[u]) { |
| int v = it.first; |
| |
| if (!vis[v] && v != f) |
| siz[u] += getsiz(v, u); |
| } |
| |
| return siz[u]; |
| } |
| |
| void getroot(int u, int f, int Siz) { |
| siz[u] = 1, mxsiz[u] = 0; |
| |
| for (auto it : G.e[u]) { |
| int v = it.first; |
| |
| if (!vis[v] && v != f) |
| getroot(v, u, Siz), siz[u] += siz[v], mxsiz[u] = max(mxsiz[u], siz[v]); |
| } |
| |
| mxsiz[u] = max(mxsiz[u], Siz - siz[u]); |
| |
| if (!root || mxsiz[u] < mxsiz[root]) |
| root = u; |
| } |
| |
| void dfs(int u, int f) { |
| vec.emplace_back(u); |
| |
| for (auto it : G.e[u]) { |
| int v = it.first, w = it.second; |
| |
| if (!vis[v] && v != f) |
| dis[v] = dis[u] + w, dfs(v, u); |
| } |
| } |
| |
| void solve(int u) { |
| vis[u] = true, dis[u] = 0, vec.clear(), dfs(u, 0); |
| |
| sort(vec.begin(), vec.end(), [](const int &x, const int &y) { |
| return a[x] + dis[x] < a[y] + dis[y]; |
| }); |
| |
| for (int i = 1; i < vec.size(); ++i) |
| edg.emplace_back(a[vec[0]] + dis[vec[0]] + a[vec[i]] + dis[vec[i]], vec[0], vec[i]); |
| |
| for (auto it : G.e[u]) { |
| int v = it.first; |
| |
| if (!vis[v]) |
| root = 0, getroot(v, u, getsiz(v, u)), solve(root); |
| } |
| } |
| |
| inline ll Kruskal() { |
| sort(edg.begin(), edg.end()); |
| dsu.prework(n); |
| ll ans = 0; |
| |
| for (auto it : edg) { |
| int u = get<1>(it), v = get<2>(it); |
| |
| if (dsu.find(u) == dsu.find(v)) |
| continue; |
| |
| ans += get<0>(it), dsu.merge(u, v); |
| } |
| |
| return ans; |
| } |
| |
| signed main() { |
| scanf("%d", &n); |
| |
| for (int i = 1; i <= n; ++i) |
| scanf("%d", a + i); |
| |
| for (int i = 1; i < n; ++i) { |
| int u, v, w; |
| scanf("%d%d%d", &u, &v, &w); |
| G.insert(u, v, w), G.insert(v, u, w); |
| } |
| |
| getroot(1, 0, n), solve(root); |
| printf("%lld", Kruskal()); |
| return 0; |
| } |
CF1550F Jumping Around
数轴上有 n 个点,坐标为 a1∼n 。给定起点 S 和步长 d ,q 次询问,每次给出 k,T ,表示 x 能跳到 y 当且仅当 |y−x|∈[d−k,d+k] ,求 S 是否能跳到 T 。
n≤2×105 ,q≤106
首先可以发现可达性关于 k 是单调的,即 k<k0 时不可达,k≥k0 时可达。
考虑建出完全图,附上合适的边权满足 k 的单调性。定义两个点 x,y 的边权为 ||ax−ay|−d| ,则只要判断 MST 上两点路径的最大边权是否 ≤k 即可。
完全图的 MST 考虑 Boruvka,每次合并维护每个数向左、向右第一个不在连通块的位置,然后双指针扫一遍即可。
时间复杂度 O(nlogn+q) 。
| #include <bits/stdc++.h> |
| using namespace std; |
| const int inf = 0x3f3f3f3f; |
| const int N = 2e5 + 7; |
| |
| struct Graph { |
| vector<pair<int, int> > e[N]; |
| |
| inline void insert(int u, int v, int w) { |
| e[u].emplace_back(v, w); |
| } |
| } G; |
| |
| struct DSU { |
| int fa[N]; |
| |
| inline void prework(int n) { |
| iota(fa + 1, fa + n + 1, 1); |
| } |
| |
| 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(y)] = find(x); |
| } |
| } dsu; |
| |
| pair<int, int> cur[N]; |
| |
| int a[N], L[N], R[N], pos[N], len[N], val[N], mxd[N]; |
| |
| int n, q, s, d; |
| |
| inline void Boruvka() { |
| dsu.prework(n); |
| int cnt = 0; |
| |
| while (cnt < n - 1) { |
| for (int i = 1; i <= n; ++i) |
| L[i] = (dsu.find(i - 1) == dsu.find(i) ? L[i - 1] : i - 1); |
| |
| for (int i = n; i; --i) |
| R[i] = (dsu.find(i + 1) == dsu.find(i) ? R[i + 1] : i + 1); |
| |
| memset(pos + 1, -1, sizeof(int) * n), memset(len + 1, inf, sizeof(int) * n); |
| |
| auto update = [](int x, int y) { |
| if (x == y || !y || y == n + 1) |
| return; |
| |
| if (pos[x] == -1 || abs(abs(a[x] - a[y]) - d) < len[x]) |
| pos[x] = y, len[x] = abs(abs(a[x] - a[y]) - d); |
| }; |
| |
| for (int i = 1, j = 1; i <= n; ++i) { |
| while (j < n && a[j] <= a[i] + d) |
| ++j; |
| |
| if (dsu.find(j) != dsu.find(i)) |
| update(i, j); |
| else |
| update(i, L[j]), update(i, R[j]); |
| |
| if (dsu.find(j - 1) != dsu.find(i)) |
| update(i, j - 1); |
| else |
| update(i, L[j - 1]), update(i, R[j - 1]); |
| } |
| |
| for (int i = n, j = n; i; --i) { |
| while (j > 1 && a[j] >= a[i] - d) |
| --j; |
| |
| if (dsu.find(j) != dsu.find(i)) |
| update(i, j); |
| else |
| update(i, L[j]), update(i, R[j]); |
| |
| if (dsu.find(j + 1) != dsu.find(i)) |
| update(i, j + 1); |
| else |
| update(i, L[j + 1]), update(i, R[j + 1]); |
| } |
| |
| memset(val + 1, inf, sizeof(int) * n); |
| |
| for (int i = 1; i <= n; ++i) |
| if (len[i] < val[dsu.find(i)]) |
| val[dsu.find(i)] = len[i], cur[dsu.find(i)] = make_pair(pos[i], i); |
| |
| for (int i = 1; i <= n; ++i) |
| if (val[i] != inf && dsu.find(cur[i].first) != dsu.find(cur[i].second)) { |
| G.insert(cur[i].first, cur[i].second, val[i]), G.insert(cur[i].second, cur[i].first, val[i]); |
| dsu.merge(cur[i].first, cur[i].second), ++cnt; |
| } |
| } |
| } |
| |
| void dfs(int u, int f) { |
| for (auto it : G.e[u]) { |
| int v = it.first, w = it.second; |
| |
| if (v != f) |
| mxd[v] = max(mxd[u], w), dfs(v, u); |
| } |
| } |
| |
| signed main() { |
| scanf("%d%d%d%d", &n, &q, &s, &d); |
| |
| for (int i = 1; i <= n; ++i) |
| scanf("%d", a + i); |
| |
| Boruvka(), dfs(s, 0); |
| |
| while (q--) { |
| int t, k; |
| scanf("%d%d", &t, &k); |
| puts(mxd[t] <= k ? "Yes" : "No"); |
| } |
| |
| return 0; |
| } |
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】凌霞软件回馈社区,博客园 & 1Panel & Halo 联合会员上线
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】博客园社区专享云产品让利特惠,阿里云新客6.5折上折
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步