次小生成树
题意
给定一张 n n n 个点 m m m 条边的无向图,求无向图的严格次小生成树。
设最小生成树的边权之和为 s u m sum sum,严格次小生成树就是指边权之和大于 s u m sum sum生成树中最小的一个。
输入格式
第一行包含两个整数 n n n 和 m m m。
接下来 m m m 行,每行包含三个整数 x x x, y y y, z z z,表示点 x x x 和点 y y y 之前存在一条边,边的权值为 z z z。
输出格式
包含一行,仅一个数,表示严格次小生成树的边权和。(数据保证必定存在严格次小生成树)
数据范围
N ≤ 1 0 5 , M ≤ 3 × 1 0 5 N\le10^5,M\le3 \times10^5 N≤105,M≤3×105
题解
首先次小生成树与最小生成树一定只有一条边不一样,因为如果有两条不一样的话,那么就可以找这两条其中一条,在次小生成树中连上最小生成树去掉的那条边,就得到了一个环,因为这条边一定是比去掉那条大,所以把这条去掉,我们就得到了一个,比最小生成树大但是比当前次小生成树小的树,因此次小生成树与最小生成树一定只有一条边不一样。
算法一 暴力枚举拆边
我们可以用kruskal 把最小生成树求出来,然后从这个树上把任意一个 u -> v 的最大边和严格次大边求出来,之后遍历所有的u -> v,填上这个边生成一个环,如果原树中最大边不等于这个边,就去掉最大边,否则去掉次大边。
算法的瓶颈在于求出任意两点之间的最大值和次大值需要 O ( n 2 ) O(n^2) O(n2)
Code
#include <iostream>
#include <algorithm>
#include <cstring>
#include <cstdio>
#include <set>
#include <queue>
#include <vector>
#include <map>
#include <unordered_map>
#include <cmath>
#include <stack>
#include <iomanip>
#include <deque>
#include <sstream>
#define x first
#define y second
using namespace std;
typedef long double ld;
typedef long long LL;
typedef pair<int, int> PII;
typedef pair<double, double> PDD;
typedef unsigned long long ULL;
const int N = 510, M = 1e4 + 10, INF = 0x3f3f3f3f, mod = 1e9 + 7;
const double eps = 1e-8;
int dx[] = {-1, 0, 1, 0}, dy[] = {0, 1, 0, -1};
int h[N], e[M], ne[M], w[M], idx;
void add(int a, int b, int v = 0) {
e[idx] = b, w[idx] = v, ne[idx] = h[a], h[a] = idx ++;
}
int n, m, k;
struct Node {
int u, v, w;
bool flag;
bool operator<(const Node &t)const {
return w < t.w;
}
}edges[M];
int dist1[N][N], dist2[N][N];
int f[N];
int find(int x) {
return x == f[x] ? x : f[x] = find(f[x]);
}
void dfs(int u, int fa, int mx1, int mx2, int d1[], int d2[]) { // 递推的求出,以每个点为根的唯一路径,树的特殊性质
d1[u] = mx1, d2[u] = mx2;
for (int i = h[u]; ~i; i = ne[i]) {
int j = e[i];
if (j == fa) continue;
int t1 = mx1, t2 = mx2;
// 最大值和次大值且不能相等
if (w[i] > t1) t2 = t1, t1 = w[i];
else if (w[i] < t1 && w[i] > t2) t2 = w[i];
dfs(j, u, t1, t2, d1, d2);
}
}
int main() {
ios::sync_with_stdio(false), cin.tie(0);
memset(h, -1, sizeof h);
cin >> n >> m;
for (int i = 1; i <= n; i ++ ) f[i] = i;
for (int i = 0; i < m; i ++ )
cin >> edges[i].u >> edges[i].v >> edges[i].w;
sort(edges, edges + m);
LL sum = 0;
// 把树生成出来
for (int i = 0; i < m; i ++ ) {
int a = edges[i].u, b = edges[i].v, w = edges[i].w;
int fa = find(a), fb = find(b);
if (fa != fb) {
f[fa] = fb;
sum += w;
edges[i].flag = true; // 在树中, 为不在树中的做准备
add(a, b, w), add(b, a, w);
}
}
for (int i = 1; i <= n; i ++ )
dfs(i, -1, 0, 0, dist1[i], dist2[i]); //任意一个点的距离到其他点的最大值和次大值
LL res = 3e18;
for (int i = 0; i < m; i ++ )
if (!edges[i].flag) {
int a = edges[i].u, b = edges[i].v, w = edges[i].w;
if (w != dist1[a][b]) // 最小值和次小值
res = min(res, sum + w - dist1[a][b]); // 拆边再加边
else
res = min(res, sum + w - dist2[a][b]);
}
cout << res << endl;
return 0;
}
// 次小生成树,一定是最小生成树去掉一个边,变成两个树以后再加入一条边所形成的
// 如何保证是次小的,每加上一条边都会形成一个环,然后把环上除刚加入的边的最大的一个
// 删掉即可,暴力枚举,res一定属于其中一种情况
算法二 倍增优化
思想还是原来的思想,找一个边替换,考虑对找到任意两点之间的最大值和次大值进行优化。用 d 1 [ u ] [ j ] d1[u][j] d1[u][j]表示从 u u u这个点向上跳 2 j 2^j 2j步这条路径上的最大值, d 2 [ u ] [ j ] d2[u][j] d2[u][j]表示从 u u u这个点向上跳 2 j 2^j 2j步这条路径上的次大值,然后对于u -> v这条边进行进行替换原树中的时候,只需要找到 u, v的 lca,假设lca为p,那么这条路径的最大值和次大值都可以在找lca的往上跳的过程中顺带维护出来。这样时间复杂度就优化成了 O ( n l o g n ) O(nlogn) O(nlogn)
Code
#include <iostream>
#include <algorithm>
#include <cstring>
#include <cstdio>
#include <set>
#include <queue>
#include <vector>
#include <map>
#include <unordered_map>
#include <cmath>
#include <stack>
#include <iomanip>
#include <deque>
#include <sstream>
#define x first
#define y second
using namespace std;
typedef long double ld;
typedef long long LL;
typedef pair<int, int> PII;
typedef pair<double, double> PDD;
typedef unsigned long long ULL;
const int N = 1e5 + 10, M = 3 * N, INF = 0x3f3f3f3f, mod = 1e9 + 7;
const double eps = 1e-8;
int dx[] = {-1, 0, 1, 0}, dy[] = {0, 1, 0, -1};
int h[N], e[M], ne[M], w[M], idx;
void add(int a, int b, int v = 0) {
e[idx] = b, w[idx] = v, ne[idx] = h[a], h[a] = idx ++;
}
int n, m, k;
struct Node {
int u, v, w;
bool ok;
bool operator< (const Node &t) const {
return w < t.w;
}
}edge[M];
int f[N], dep[N], fa[N][17], d1[N][17], d2[N][17];
int q[N];
int find(int x) {
return x == f[x] ? x : f[x] = find(f[x]);
}
LL kruskal() {
for (int i = 1; i <= n; i ++ ) f[i] = i;
sort(edge, edge + m);
LL res = 0;
for (int i = 0; i < m; i ++ ) {
int a = find(edge[i].u), b = find(edge[i].v), w = edge[i].w;
if (a != b) {
f[a] = b;
res += w;
edge[i].ok = true;
}
}
return res;
}
void build() {
memset(h, -1, sizeof h);
for (int i = 0; i < m; i ++ )
if (edge[i].ok) {
int a = edge[i].u, b = edge[i].v, w = edge[i].w;
add(a, b, w), add(b, a, w);
}
}
void bfs() {
memset(dep, 0x3f, sizeof dep);
dep[0] = 0, dep[1] = 1;
q[0] = 1;
int hh = 0, tt = 0;
while (hh <= tt) {
int t = q[hh ++];
for (int i = h[t]; ~i; i = ne[i]) {
int j = e[i];
if (dep[j] > dep[t] + 1) {
dep[j] = dep[t] + 1;
q[++ tt] = j;
fa[j][0] = t;
d1[j][0] = w[i], d2[j][0] = -INF; // 最大值和严格次大值
for (int k = 1; k <= 16; k ++ ) {
int anc = fa[j][k - 1]; // 跳一半的祖先
fa[j][k] = fa[anc][k - 1];
int dd[4] = {d1[j][k - 1], d2[j][k - 1], d1[anc][k - 1], d2[anc][k - 1]}; // 四段
d1[j][k] = d2[j][k] = -INF;
for (int u = 0; u < 4; u ++ ) {
int d = dd[u];
if (d > d1[j][k]) d2[j][k] = d1[j][k], d1[j][k] = d;
else if (d != d1[j][k] && d > d2[j][k]) d2[j][k] = d;
}
}
}
}
}
}
int lca(int a, int b, int w) {
static int distance[N << 1];
int cnt = 0;
if (dep[a] < dep[b]) swap(a, b);
for (int k = 16; k >= 0; k -- )
if (dep[fa[a][k]] >= dep[b]) {
distance[cnt ++] = d1[a][k];
distance[cnt ++] = d2[a][k];
a = fa[a][k];
}
if (a != b) {
for (int k = 16; k >= 0; k -- )
if (fa[a][k] != fa[b][k]) {
distance[cnt ++] = d1[a][k];
distance[cnt ++] = d2[a][k];
distance[cnt ++] = d1[b][k];
distance[cnt ++] = d2[b][k];
a = fa[a][k], b = fa[b][k];
}
distance[cnt ++] = d1[a][0], distance[cnt ++] = d1[b][0]; // 一条路上的最大值只有他自己
}
int dist1 = -INF, dist2 = -INF;
for (int i = 0; i < cnt; i ++ ) {
int d = distance[i];
if (d > dist1) dist2 = dist1, dist1 = d;
else if (d != dist1 && d > dist2) dist2 = d;
}
if (w != dist1) return w - dist1;
return w - dist2;
}
int main() {
ios::sync_with_stdio(false), cin.tie(0);
cin >> n >> m;
for (int i = 0; i < m; i ++ ) {
int a, b, c;
cin >> a >> b >> c;
edge[i] = {a, b, c};
}
LL sum = kruskal();
build();
bfs();
LL res = 1e18;
for (int i = 0; i < m; i ++ )
if (!edge[i].ok) {
int a = edge[i].u, b = edge[i].v, w = edge[i].w;
res = min(res, sum + lca(a, b, w));
}
cout << res << endl;
return 0;
}