# 次小生成树 --- ### 非严格次小生成树 #### 定义 在无向图中,边权和最小的满足边权和 **大于等于** 最小生成树边权和的生成树 --- #### 求解方法 - 求出无向图的最小生成树 $T$,设其权值和为 $M$ - 遍历每条未被选中的边 $e = (u,v,w)$,找到 $T$ 中 $u$ 到 $v$ 路径上边权最大的一条边 $e' = (s,t,w')$,则在 $T$ 中以 $e$ 替换 $e'$,可得一棵权值和为 $M' = M + w - w'$ 的生成树 $T'$. - 对所有替换得到的答案 $M'$ 取最小值即可 --- 如何求 $u,v$ 路径上的边权最大值呢? 我们可以使用倍增来维护,预处理出每个节点的 $2^i$ 级祖先及到达其 $2^i$ 级祖先路径上最大的边权,这样在倍增求 LCA 的过程中可以直接求得。 --- ### 严格次小生成树 #### 定义 在无向图中,边权和最小的满足边权和 **严格大于** 最小生成树边权和的生成树 --- #### 求解方法 考虑刚才的非严格次小生成树求解过程,为什么求得的解是非严格的? 因为最小生成树保证生成树中 $u$ 到 $v$ 路径上的边权最大值一定 **不大于** 其他从 $u$ 到 $v$ 路径的边权最大值。换言之,当我们用于替换的边的权值与原生成树中被替换边的权值相等时,得到的次小生成树是非严格的。 解决的办法很自然:我们维护到 $2^i$ 级祖先路径上的最大边权的同时维护 **严格次大边权**,当用于替换的边的权值与原生成树中路径最大边权相等时,我们用严格次大值来替换即可。 这个过程可以用倍增求解,复杂度 $O(m \log m)$。 --- "代码实现" ```cpp #include
#include
const int INF = 0x3fffffff; const long long INF64 = 0x3fffffffffffffffLL; struct Edge { int u, v, val; bool operator<(const Edge &other) const { return val < other.val; } }; Edge e[300010]; bool used[300010]; int n, m; long long sum; class Tr { private: struct Edge { int to, nxt, val; } e[600010]; int cnt, head[100010]; int pnt[100010][22]; int dpth[100010]; // 到祖先的路径上边权最大的边 int maxx[100010][22]; // 到祖先的路径上边权次大的边,若不存在则为 -INF int minn[100010][22]; public: void addedge(int u, int v, int val) { e[++cnt] = (Edge){v, head[u], val}; head[u] = cnt; } void insedge(int u, int v, int val) { addedge(u, v, val); addedge(v, u, val); } void dfs(int now, int fa) { dpth[now] = dpth[fa] + 1; pnt[now][0] = fa; minn[now][0] = -INF; for (int i = 1; (1 << i) <= dpth[now]; i++) { pnt[now][i] = pnt[pnt[now][i - 1]][i - 1]; int kk[4] = {maxx[now][i - 1], maxx[pnt[now][i - 1]][i - 1], minn[now][i - 1], minn[pnt[now][i - 1]][i - 1]}; // 从四个值中取得最大值 std::sort(kk, kk + 4); maxx[now][i] = kk[3]; // 取得严格次大值 int ptr = 2; while (ptr >= 0 && kk[ptr] == kk[3]) ptr--; minn[now][i] = (ptr == -1 ? -INF : kk[ptr]); } for (int i = head[now]; i; i = e[i].nxt) { if (e[i].to != fa) { maxx[e[i].to][0] = e[i].val; dfs(e[i].to, now); } } } int lca(int a, int b) { if (dpth[a] < dpth[b]) std::swap(a, b); for (int i = 21; i >= 0; i--) if (dpth[pnt[a][i]] >= dpth[b]) a = pnt[a][i]; if (a == b) return a; for (int i = 21; i >= 0; i--) { if (pnt[a][i] != pnt[b][i]) { a = pnt[a][i]; b = pnt[b][i]; } } return pnt[a][0]; } int query(int a, int b, int val) { int res = -INF; for (int i = 21; i >= 0; i--) { if (dpth[pnt[a][i]] >= dpth[b]) { if (val != maxx[a][i]) res = std::max(res, maxx[a][i]); else res = std::max(res, minn[a][i]); a = pnt[a][i]; } } return res; } } tr; int fa[100010]; int find(int x) { return fa[x] == x ? x : fa[x] = find(fa[x]); } void Kruskal() { int tot = 0; std::sort(e + 1, e + m + 1); for (int i = 1; i <= n; i++) fa[i] = i; for (int i = 1; i <= m; i++) { int a = find(e[i].u); int b = find(e[i].v); if (a != b) { fa[a] = b; tot++; tr.insedge(e[i].u, e[i].v, e[i].val); sum += e[i].val; used[i] = 1; } if (tot == n - 1) break; } } int main() { std::ios::sync_with_stdio(0); std::cin.tie(0); std::cout.tie(0); std::cin >> n >> m; for (int i = 1; i <= m; i++) { int u, v, val; std::cin >> u >> v >> val; e[i] = (Edge){u, v, val}; } Kruskal(); long long ans = INF64; tr.dfs(1, 0); for (int i = 1; i <= m; i++) { if (!used[i]) { int _lca = tr.lca(e[i].u, e[i].v); // 找到路径上不等于 e[i].val 的最大边权 long long tmpa = tr.query(e[i].u, _lca, e[i].val); long long tmpb = tr.query(e[i].v, _lca, e[i].val); // 这样的边可能不存在,只在这样的边存在时更新答案 if (std::max(tmpa, tmpb) > -INF) ans = std::min(ans, sum - std::max(tmpa, tmpb) + e[i].val); } } // 次小生成树不存在时输出 -1 std::cout << (ans == INF64 ? -1 : ans) << '\n'; return 0; } ``` --- ## 瓶颈生成树 --- ### 定义 无向图 $G$ 的瓶颈生成树是这样的一个生成树,它的最大的边权值在 $G$ 的所有生成树中最小。 --- ### 性质 **最小生成树是瓶颈生成树的充分不必要条件。** 即最小生成树一定是瓶颈生成树,而瓶颈生成树不一定是最小生成树。 关于最小生成树一定是瓶颈生成树这一命题,可以运用反证法证明:我们设最小生成树中的最大边权为 $w$,如果最小生成树不是瓶颈生成树的话,则瓶颈生成树的所有边权都小于 $w$,我们只需删去原最小生成树中的最长边,用瓶颈生成树中的一条边来连接删去边后形成的两棵树,得到的新生成树一定比原最小生成树的权值和还要小,这样就产生了矛盾。 --- ### 例题 "POJ 2395 Out of Hay" 给出 n 个农场和 m 条边,农场按 1 到 n 编号,现在有一人要从编号为 1 的农场出发到其他的农场去,求在这途中他最多需要携带的水的重量,注意他每到达一个农场,可以对水进行补给,且要使总共的路径长度最小。 题目要求的就是瓶颈树的最大边,可以通过求最小生成树来解决。 --- ## 最小瓶颈路 ### 定义 无向图 $G$ 中 x 到 y 的最小瓶颈路是这样的一类简单路径,满足这条路径上的最大的边权在所有 x 到 y 的简单路径中是最小的。 --- ### 性质 根据最小生成树定义,x 到 y 的最小瓶颈路上的最大边权等于最小生成树上 x 到 y 路径上的最大边权。虽然最小生成树不唯一,但是每种最小生成树 x 到 y 路径的最大边权相同且为最小值。也就是说,每种最小生成树上的 x 到 y 的路径均为最小瓶颈路。 但是,并不是所有最小瓶颈路都存在一棵最小生成树满足其为树上 x 到 y 的简单路径。 --- 例如下图: ![](http://39.99.183.126:8888/file/2/mst5.png) 1 到 4 的最小瓶颈路显然有以下两条:1-2-3-4。1-3-4。 但是,1-2 不会出现在任意一种最小生成树上。 --- ### 应用 由于最小瓶颈路不唯一,一般情况下会询问最小瓶颈路上的最大边权。 也就是说,我们需要求最小生成树链上的 max。 倍增、树剖都可以解决,这里不再展开。 --- ## Kruskal 重构树 ### 定义 在跑 Kruskal 的过程中我们会从小到大加入若干条边。现在我们仍然按照这个顺序。 首先新建 $n$ 个集合,每个集合恰有一个节点,点权为 $0$。 每一次加边会合并两个集合,我们可以新建一个点,点权为加入边的边权,同时将两个集合的根节点分别设为新建点的左儿子和右儿子。然后我们将两个集合和新建点合并成一个集合。将新建点设为根。 不难发现,在进行 $n-1$ 轮之后我们得到了一棵恰有 $n$ 个叶子的二叉树,同时每个非叶子节点恰好有两个儿子。这棵树就叫 Kruskal 重构树。 --- 举个例子: ![](http://39.99.183.126:8888/file/2/mst5.png) 这张图的 Kruskal 重构树如下: ![](http://39.99.183.126:8888/file/2/mst6.png) --- ### 性质 不难发现,原图中两个点之间的所有简单路径上最大边权的最小值 = 最小生成树上两个点之间的简单路径上的最大值 = Kruskal 重构树上两点之间的 LCA 的权值。 也就是说,到点 $x$ 的简单路径上最大边权的最小值 $\leq val$ 的所有点 $y$ 均在 Kruskal 重构树上的某一棵子树内,且恰好为该子树的所有叶子节点。 我们在 Kruskal 重构树上找到 $x$ 到根的路径上权值 $\leq val$ 的最浅的节点。显然这就是所有满足条件的节点所在的子树的根节点。 如果需要求原图中两个点之间的所有简单路径上最小边权的最大值,则在跑 Kruskal 的过程中按边权大到小的顺序加边。 --- "[「LOJ 137」最小瓶颈路 加强版](https://loj.ac/problem/137)" ```cpp #include
using namespace std; const int MAX_VAL_RANGE = 280010; int n, m, log2Values[MAX_VAL_RANGE + 1]; namespace TR { struct Edge { int to, nxt, val; } e[400010]; int cnt, head[140010]; void addedge(int u, int v, int val = 0) { e[++cnt] = (Edge){v, head[u], val}; head[u] = cnt; } int val[140010]; namespace LCA { int sec[280010], cnt; int pos[140010]; int dpth[140010]; void dfs(int now, int fa) { dpth[now] = dpth[fa] + 1; sec[++cnt] = now; pos[now] = cnt; for (int i = head[now]; i; i = e[i].nxt) { if (fa != e[i].to) { dfs(e[i].to, now); sec[++cnt] = now; } } } int dp[280010][20]; void init() { dfs(2 * n - 1, 0); for (int i = 1; i <= 4 * n; i++) { dp[i][0] = sec[i]; } for (int j = 1; j <= 19; j++) { for (int i = 1; i + (1 << j) - 1 <= 4 * n; i++) { dp[i][j] = dpth[dp[i][j - 1]] < dpth[dp[i + (1 << (j - 1))][j - 1]] ? dp[i][j - 1] : dp[i + (1 << (j - 1))][j - 1]; } } } int lca(int x, int y) { int l = pos[x], r = pos[y]; if (l > r) { swap(l, r); } int k = log2Values[r - l + 1]; return dpth[dp[l][k]] < dpth[dp[r - (1 << k) + 1][k]] ? dp[l][k] : dp[r - (1 << k) + 1][k]; } } // namespace LCA } // namespace TR using TR::addedge; namespace GR { struct Edge { int u, v, val; bool operator<(const Edge &other) const { return val < other.val; } } e[100010]; int fa[140010]; int find(int x) { return fa[x] == 0 ? x : fa[x] = find(fa[x]); } void kruskal() { // 最小生成树 int tot = 0, cnt = n; sort(e + 1, e + m + 1); for (int i = 1; i <= m; i++) { int fau = find(e[i].u), fav = find(e[i].v); if (fau != fav) { cnt++; fa[fau] = fa[fav] = cnt; addedge(fau, cnt); addedge(cnt, fau); addedge(fav, cnt); addedge(cnt, fav); TR::val[cnt] = e[i].val; tot++; } if (tot == n - 1) { break; } } } } // namespace GR int ans; int A, B, C, P; int rnd() { return A = (A * B + C) % P; } void initLog2() { for (int i = 2; i <= MAX_VAL_RANGE; i++) { log2Values[i] = log2Values[i >> 1] + 1; } } int main() { initLog2(); // 预处理 cin >> n >> m; for (int i = 1; i <= m; i++) { int u, v, val; cin >> u >> v >> val; GR::e[i] = (GR::Edge){u, v, val}; } GR::kruskal(); TR::LCA::init(); int Q; cin >> Q; cin >> A >> B >> C >> P; while (Q--) { int u = rnd() % n + 1, v = rnd() % n + 1; ans += TR::val[TR::LCA::lca(u, v)]; ans %= 1000000007; } cout << ans; return 0; } ``` --- "[NOI 2018 归程](https://uoj.ac/problem/393)" 首先预处理出来每一个点到根节点的最短路。 我们构造出来根据海拔的最大生成树。显然每次询问可以到达的节点是在最大生成树中和询问点的路径上最小边权 $> p$ 的节点。 根据 Kruskal 重构树的性质,这些节点满足均在一棵子树内同时为其所有叶子节点。 也就是说,我们只需要求出 Kruskal 重构树上每一棵子树叶子的权值 min 就可以支持子树询问。 询问的根节点可以使用 Kruskal 重构树上倍增的方式求出。 时间复杂度 $O((n+m+Q) \log n)$。