生成树
最小生成树#
稀疏图用prim+heap,稠密图用kruscal,一般给的是稀疏图
prim+heap模板:

#include <queue> #include <cstdio> #include <cstring> #include <iostream> #include <map> #include <string> #include <cmath> #include <iomanip> #include <algorithm> using namespace std; const int N = 2000+10; const int inf = 0x3f3f3f3f; int n, m; double dis[N]; int vis[N]; struct E { int to, next; double w; }edge[N*N]; int tot, head[N]; inline void add_edge(int u, int v, double w) { edge[tot].to = v; edge[tot].w = w; edge[tot].next = head[u]; head[u] = tot++; } bool Prim_heap() { int st = 1; for (int i = 0; i <= n; ++ i) dis[i] = inf; memset(vis, 0, sizeof(vis)); priority_queue<pair<double,int> > q; dis[st] = 0; q.push({0,st}); while (!q.empty()) { int u = q.top().second; q.pop(); vis[u] = 1; for (int i = head[u]; ~i; i = edge[i].next) { int v = edge[i].to; double w = edge[i].w; if (!vis[v] && dis[v] > w) { dis[v] = w; q.push(make_pair(-dis[v], v)); //优先队列大根堆变小根堆小 骚操作:只需一个‘-’号; } } } for (int i = 1; i <= n; ++ i) if (dis[i] == inf) return 0; //判断是否不存在最小生成树 return 1; } void init() { tot = 0; //memset(head, -1, sizeof (head)); for (int i = 0; i <= n; ++ i) head[i] = -1; } int main() { int T; scanf("%d", &T); while (T--) { scanf("%d", &n); init(); int x[n+1], y[n+1]; for (int i = 1; i <= n; ++ i) scanf("%d %d", &x[i], &y[i]); for (int i = 1; i <= n; ++ i) { for (int j = 1; j <= n; ++ j) { if (i == j) continue; double temp = sqrt((x[i]-x[j])*(x[i]-x[j])+(y[i]-y[j])*(y[i]-y[j])); if (temp < 10 || temp > 1000) continue; add_edge(i, j, temp*100); } } if (Prim_heap()) { double ans = 0; for (int i = 1; i <= n; ++ i) ans += dis[i];//, cout << dis[i] << endl; cout << fixed << setprecision(1) << ans << endl; } else cout << "oh!" << endl; } return 0; }
kruscal:

#include <iostream> #include <algorithm> #include <cstdio> #include <cstring> #include <vector> #include <cmath> using namespace std; const int N = 100000+10, M = 200000+10; struct Edge{ int u,v; double w; bool operator < (const Edge &E)const { return w<E.w; } }edge[M]; int fa[N]; int n,m; int _find(int x) { if(fa[x]==x)return x; else return fa[x]=_find(fa[x]); } int main() { scanf("%d", &n); int cnt=0, ans=0; int x[n+1], y[n+1]; int k; for (int i = 1; i <= n; i++) fa[i] = i; for (int i = 1; i <= n; i++) scanf("%d %d", &x[i], &y[i]); scanf("%d", &k); for (int i = 1,u,v; i <= k; ++ i) { scanf("%d %d", &u, &v); fa[_find(u)] = _find(v); } for (int i = 1; i < n; ++ i) { for (int j = i+1; j <= n; ++ j) { ++cnt, edge[cnt].u = i, edge[cnt].v = j, edge[cnt].w = sqrt((x[i]-x[j])*(x[i]-x[j])+(y[i]-y[j])*(y[i]-y[j])); } } sort(edge+1, edge+cnt+1); vector<pair<int, int> > rec; for (int i = 1; i <= cnt; i++) { int u = _find(edge[i].u); int v = _find(edge[i].v); //double w = edge[i].w; if (u != v) { cnt ++; fa[u] = v; rec.push_back({edge[i].u, edge[i].v}); } } for (int i = 0; i < rec.size(); ++ i) { cout << rec[i].first << " " << rec[i].second << endl; } return 0; }
次小生成树#
(非严格 / 严格)Kruscal + 倍增LCA O(nlogn+ mlogm)
顺便存个倍增LCA

#include <bits/stdc++.h> using namespace std; const int maxn = 500000+10; struct Edge{ int to, next; int w; } edge[maxn*2]; int head[maxn], cnt; int depth[maxn], fa[maxn][19], lg[maxn]; inline void Add_Edge(int u, int v, int w){ edge[cnt].next = head[u]; edge[cnt].to = v; edge[cnt].w = w; head[u] = cnt++; } void dfs(int now, int fath) { //now表示当前节点,fath表示它的父亲节点 fa[now][0] = fath; depth[now] = depth[fath] + 1; for(int i = 1; i <= lg[depth[now]]; ++i) fa[now][i] = fa[fa[now][i-1]][i-1]; //这个转移可以说是算法的核心之一 //意思是now的2^i祖先等于now的2^(i-1)祖先的2^(i-1)祖先 //2^i = 2^(i-1) + 2^(i-1) for(int i = head[now]; ~i; i = edge[i].next) if(edge[i].to != fath) dfs(edge[i].to, now); } int LCA(int x, int y) { if(depth[x] < depth[y]) //用数学语言来说就是:不妨设x的深度 >= y的深度 swap(x, y); while(depth[x] > depth[y]) x = fa[x][lg[depth[x]-depth[y]] - 1]; //先跳到同一深度 if(x == y) //如果x是y的祖先,那他们的LCA肯定就是x了 return x; for(int k = lg[depth[x]] - 1; k >= 0; --k) //不断向上跳(lg就是之前说的常数优化) if(fa[x][k] != fa[y][k]) //因为我们要跳到它们LCA的下面一层,所以它们肯定不相等,如果不相等就跳过去。 x = fa[x][k], y = fa[y][k]; return fa[x][0]; //返回父节点 } void init() { memset(head, -1, sizeof(head)); cnt = 0; } int main() { int n, m, s; scanf("%d%d%d", &n, &m, &s); init(); for(int i = 1; i <= n-1; ++i) { int x, y; scanf("%d%d", &x, &y); Add_Edge(x, y, 0), Add_Edge(y, x, 0); } for(int i = 1; i <= n; ++i) //预先算出log_2(i)+1的值,用的时候直接调用就可以了 lg[i] = lg[i-1] + (1 << lg[i-1] == i); //看不懂的可以手推一下 dfs(s, 0); for(int i = 1; i <= m; ++i) { int x, y; scanf("%d%d",&x, &y); printf("%d\n", LCA(x, y)); } return 0; }
严格次小生成树 题目链接:P4180 [BJWC2010]严格次小生成树

#include <bits/stdc++.h> using namespace std; typedef long long ll; const ll linf = 2147483647000000; const int inf = 0x3f3f3f3f; const int maxn = 100000+10; const int maxm = 400000+10; struct Edge{ int to, next; ll w; } edge[maxn*2]; struct Tree{ int to, from; ll w; bool operator< (const Tree& tt) {return w < tt.w;} } tree[maxm]; int n, m; int head[maxn], cnt; int depth[maxn], fa[maxn][19], lg[maxn], fat[maxn]; //fa用于lca查询, fat用于并查集 ll dis1[maxn][19], dis2[maxn][19]; int used[maxn]; inline ll read() { int x=0; char ch=getchar(); while(ch<'0'||ch>'9')ch=getchar(); while(ch>='0'&&ch<='9')x=x*10+(ch^48),ch=getchar(); return x; } inline void Add_Edge(int u, int v, ll w){ edge[cnt].next = head[u], edge[cnt].to = v, edge[cnt].w = w; head[u] = cnt++; // edge[cnt].next = head[v], edge[cnt].to = u, edge[cnt].w = w; // head[v] = cnt++; } //因为DFS可能会爆栈。我就没用了 //void dfs(int now, int fath) { //now表示当前节点,fath表示它的父亲节点 // fa[now][0] = fath; depth[now] = depth[fath] + 1; // for(int i = 1; i <= lg[depth[now]]; ++i) // fa[now][i] = fa[fa[now][i-1]][i-1]; //这个转移可以说是算法的核心之一 // //意思是now的2^i祖先等于now的2^(i-1)祖先的2^(i-1)祖先 // //2^i = 2^(i-1) + 2^(i-1) // for(int i = head[now]; ~i; i = edge[i].next) // if(edge[i].to != fath) dfs(edge[i].to, now); //} void bfs() { queue<int> q; q.push(1), depth[1] = 1; while (!q.empty()) { int fath = q.front(); q.pop(); for(int i = head[fath]; ~i; i = edge[i].next) { int now = edge[i].to; if (depth[now]) continue; fa[now][0] = fath, depth[now] = depth[fath] + 1; dis1[now][0] = edge[i].w, dis2[now][0] = -linf; q.push(now); for(int j = 1; j <= lg[depth[now]]; ++j) { fa[now][j] = fa[fa[now][j-1]][j-1]; //最大值肯定是max不用管 dis1[now][j] = max(dis1[now][j-1], dis1[fa[now][j-1]][j-1]); //这可真像树形DP,而我树形DP老菜了。。 //如果这一次的权值与最大权值相同,那么次大值不用更新,直接次大取大即可 if (dis1[now][j-1] == dis1[fa[now][j-1]][j-1]) dis2[now][j] = max(dis2[now][j-1], dis2[fa[now][j-1]][j-1]); //如果这一次的权值与比最大权值还大,那么就在更新前的最大和次大值取大 if (dis1[now][j-1] > dis1[fa[now][j-1]][j-1]) dis2[now][j] = max(dis2[now][j-1], dis1[fa[now][j-1]][j-1]); //如果这一次的权值比最大权值小,那么就在这次的权值和次大值取大 if (dis1[now][j-1] < dis1[fa[now][j-1]][j-1]) dis2[now][j] = max(dis1[now][j-1],dis2[fa[now][j-1]][j-1]); } } } } int LCA(int x, int y) { if(depth[x] < depth[y]) //用数学语言来说就是:不妨设x的深度 >= y的深度 swap(x, y); while(depth[x] > depth[y]) x = fa[x][lg[depth[x]-depth[y]] - 1]; //先跳到同一深度 if(x == y) //如果x是y的祖先,那他们的LCA肯定就是x了 return x; for(int k = lg[depth[x]] - 1; k >= 0; --k) //不断向上跳(lg就是之前说的常数优化) if(fa[x][k] != fa[y][k]) //因为我们要跳到它们LCA的下面一层,所以它们肯定不相等,如果不相等就跳过去。 x = fa[x][k], y = fa[y][k]; return fa[x][0]; //返回父节点 } int _find(int x) {return fat[x]==x?x:fat[x]=_find(fat[x]);} ll Kruscal() { ll sum = 0; sort(tree+1, tree+m+1); for (int i = 1; i <= n; ++ i) fat[i] = i; for (int i = 1; i <= m; ++ i) { int u = tree[i].from, v = tree[i].to; ll w = tree[i].w; int fx = _find(u), fy = _find(v); if (fx != fy) { used[i] = 1, sum += tree[i].w, fat[fx] = fy; Add_Edge(u, v, w), Add_Edge(v, u, w); } } return sum; } void init() { memset(head, -1, sizeof(head)); memset(used, 0, sizeof(used)); memset(depth, 0, sizeof(depth)); cnt = 0; memset(dis1, 0, sizeof(dis1)); memset(dis2, 0, sizeof(dis2)); memset(fa, 0, sizeof(fa)); } //计算u到v路径上的最大值跟w的差值 ll cal(int u, int v, ll w) { ll val1=0, val2=0; for (int i = lg[depth[u]]; i >= 0; -- i) { //倍增找最大 if (depth[fa[u][i]] >= depth[v]) { if (dis1[u][i] > val1) { val2 = val1; val1 = dis1[u][i]; } val2 = max(val2, dis2[u][i]); u = fa[u][i]; } } if (val1 != w) return w - val1; return w - val2; } ll solve() { ll base = Kruscal(); ll ans = linf; bfs(); for (int i = 1; i <= m; ++ i) { if (used[i]) continue; int u = tree[i].from, v = tree[i].to; int lca = LCA(u, v); ll w = tree[i].w; ans = min(ans, min(base+cal(u, lca, w), base+cal(v, lca, w))); } return ans; } int main() { for(int i = 1; i < maxn; ++i) //预先算出log_2(i)+1的值,用的时候直接调用就可以了 lg[i] = lg[i-1] + (1 << lg[i-1] == i); //看不懂的可以手推一下 scanf("%d%d", &n, &m); init(); for(int i = 1; i <= m; ++i) scanf("%d%d%lld", &tree[i].from, &tree[i].to, &tree[i].w); cout << solve() << endl; return 0; }
非严格次小生成树 给个板子题吧:K - The Unique MST POJ - 1679

#include <cstring> #include <iostream> #include <queue> #include <cstdio> #include <algorithm> using namespace std; typedef long long ll; const ll linf = 2147483647000000; const int inf = 0x3f3f3f3f; const int maxn = 1000+10; const int maxm = 40000+10; struct Edge{ int to, next; ll w; } edge[maxn*2]; struct Tree{ int to, from; ll w; bool operator< (const Tree& tt) const {return w < tt.w;} } tree[maxm]; int n, m; int head[maxn], cnt; int depth[maxn], fa[maxn][19], lg[maxn], fat[maxn]; //fa用于lca查询, fat用于并查集 ll dis1[maxn][19]; int used[maxn]; inline ll read() { int x=0; char ch=getchar(); while(ch<'0'||ch>'9')ch=getchar(); while(ch>='0'&&ch<='9')x=x*10+(ch^48),ch=getchar(); return x; } inline void Add_Edge(int u, int v, ll w){ edge[cnt].next = head[u], edge[cnt].to = v, edge[cnt].w = w; head[u] = cnt++; // edge[cnt].next = head[v], edge[cnt].to = u, edge[cnt].w = w; // head[v] = cnt++; } //因为DFS可能会爆栈。我就没用了 //void dfs(int now, int fath) { //now表示当前节点,fath表示它的父亲节点 // fa[now][0] = fath; depth[now] = depth[fath] + 1; // for(int i = 1; i <= lg[depth[now]]; ++i) // fa[now][i] = fa[fa[now][i-1]][i-1]; //这个转移可以说是算法的核心之一 // //意思是now的2^i祖先等于now的2^(i-1)祖先的2^(i-1)祖先 // //2^i = 2^(i-1) + 2^(i-1) // for(int i = head[now]; ~i; i = edge[i].next) // if(edge[i].to != fath) dfs(edge[i].to, now); //} void bfs() { queue<int> q; q.push(1), depth[1] = 1; while (!q.empty()) { int fath = q.front(); q.pop(); for(int i = head[fath]; ~i; i = edge[i].next) { int now = edge[i].to; if (depth[now]) continue; fa[now][0] = fath, depth[now] = depth[fath] + 1; dis1[now][0] = edge[i].w; q.push(now); for(int j = 1; j <= lg[depth[now]]; ++j) { fa[now][j] = fa[fa[now][j-1]][j-1]; //最大值肯定是max不用管 dis1[now][j] = max(dis1[now][j-1], dis1[fa[now][j-1]][j-1]); } } } } int LCA(int x, int y) { if(depth[x] < depth[y]) //用数学语言来说就是:不妨设x的深度 >= y的深度 swap(x, y); while(depth[x] > depth[y]) x = fa[x][lg[depth[x]-depth[y]] - 1]; //先跳到同一深度 if(x == y) //如果x是y的祖先,那他们的LCA肯定就是x了 return x; for(int k = lg[depth[x]] - 1; k >= 0; --k) //不断向上跳(lg就是之前说的常数优化) if(fa[x][k] != fa[y][k]) //因为我们要跳到它们LCA的下面一层,所以它们肯定不相等,如果不相等就跳过去。 x = fa[x][k], y = fa[y][k]; return fa[x][0]; //返回父节点 } int _find(int x) {return fat[x]==x?x:fat[x]=_find(fat[x]);} ll Kruscal() { ll sum = 0; sort(tree+1, tree+m+1); for (int i = 1; i <= n; ++ i) fat[i] = i; for (int i = 1; i <= m; ++ i) { int u = tree[i].from, v = tree[i].to; ll w = tree[i].w; int fx = _find(u), fy = _find(v); if (fx != fy) { used[i] = 1, sum += tree[i].w, fat[fx] = fy; Add_Edge(u, v, w), Add_Edge(v, u, w); } } return sum; } //(非严格)计算u到v路径上的最大值跟w的差值 ll cal2(int u, int v) { ll val=0; for (int i = lg[depth[u]]; i >= 0; -- i) { //倍增找最大 if (depth[fa[u][i]] >= depth[v]) { val = max(val, dis1[u][i]); u = fa[u][i]; } } return val; } ll solve(ll base) { ll ans = linf; bfs(); for (int i = 1; i <= m; ++ i) { if (used[i]) continue; int u = tree[i].from, v = tree[i].to; int lca = LCA(u, v); ll w = tree[i].w; ll maxw = max(cal2(u, lca), cal2(v, lca)); ans = min(ans, base+w-maxw); } return ans; } void init() { memset(head, -1, sizeof(head)); memset(used, 0, sizeof(used)); memset(depth, 0, sizeof(depth)); cnt = 0; memset(dis1, 0, sizeof(dis1)); memset(fa, 0, sizeof(fa)); } int main() { int T; scanf("%d", &T); for(int i = 1; i < maxn; ++i) //预先算出log_2(i)+1的值,用的时候直接调用就可以了 lg[i] = lg[i-1] + (1 << lg[i-1] == i); //看不懂的可以手推一下 while (T--) { scanf("%d%d", &n, &m); init(); for(int i = 1; i <= m; ++i) scanf("%d%d%lld", &tree[i].from, &tree[i].to, &tree[i].w); ll base = Kruscal(); ll ans = solve(base); //cout << ans << " " << base << endl; if (ans > base) cout << base << endl; else cout << "Not Unique!" << endl; } return 0; }
B - Qin Shi Huang's National Road System HDU - 4081
这道题目比较毒瘤吧。虽然就是考一个很简单的非严格次小生成树的点。
只需要枚举每一条边,之后如果这条边在MST上那么直接边两端val相加除以mst-w即可
否则就是将u和v的路径上最大的那条边代替w
我采用上面的方式一直WA,改用rec记录最大值之后就A了。真是离谱

//#include <bits/stdc++.h> #include <queue> #include <cstdio> #include <cstring> #include <algorithm> #include <iostream> #include <cstdlib> using namespace std; typedef long long ll; const ll linf = 2147483647000000; const int inf = 0x3f3f3f3f; const int maxn = 1000+10; const int maxm = maxn*maxn+10; struct Edge{ int to, next; double w; } edge[maxn*2]; struct Tree{ int to, from; double w; bool operator< (const Tree& tt) {return w<tt.w;} } tree[maxm]; int n, m; int head[maxn], cnt; int depth[maxn], fa[maxn][19], lg[maxn], fat[maxn]; //fa用于lca查询, fat用于并查集 double dis1[maxn][19]; int used[maxn]; double xx[maxn], yy[maxn], val[maxn]; double rec[maxn][maxn]; int cntt; inline void Add_Edge(int u, int v, double w){ edge[cnt].next = head[u], edge[cnt].to = v, edge[cnt].w = w; head[u] = cnt++; } void bfs() { queue<int> q; q.push(1), depth[1] = 1; while (!q.empty()) { int fath = q.front(); q.pop(); for(int i = head[fath]; ~i; i = edge[i].next) { int now = edge[i].to; if (depth[now]) continue; fa[now][0] = fath, depth[now] = depth[fath] + 1; dis1[now][0] = edge[i].w; q.push(now); for(int j = 1; j <= lg[depth[now]]; ++j) { fa[now][j] = fa[fa[now][j-1]][j-1]; dis1[now][j] = max(dis1[now][j-1], dis1[fa[now][j-1]][j-1]); } } } } int LCA(int x, int y) { if(depth[x] < depth[y]) //用数学语言来说就是:不妨设x的深度 >= y的深度 swap(x, y); while(depth[x] > depth[y]) x = fa[x][lg[depth[x]-depth[y]] - 1]; //先跳到同一深度 if(x == y) //如果x是y的祖先,那他们的LCA肯定就是x了 return x; for(int k = lg[depth[x]] - 1; k >= 0; --k) //不断向上跳(lg就是之前说的常数优化) if(fa[x][k] != fa[y][k]) //因为我们要跳到它们LCA的下面一层,所以它们肯定不相等,如果不相等就跳过去。 x = fa[x][k], y = fa[y][k]; return fa[x][0]; //返回父节点 } int _find(int x) {return fat[x]==x?x:fat[x]=_find(fat[x]);} double Kruscal() { double sum = 0; sort(tree+1, tree+cntt+1); for (int i = 1; i <= n; ++ i) fat[i] = i; for (int i = 1; i <= cntt; ++ i) { int u = tree[i].from, v = tree[i].to; double w = tree[i].w; int fx = _find(u), fy = _find(v); if (fx != fy) { used[i] = 1, sum += tree[i].w, fat[fx] = fy; Add_Edge(u, v, w), Add_Edge(v, u, w); } } return sum; } void init() { memset(head, -1, sizeof(head)); memset(used, 0, sizeof(used)); memset(depth, 0, sizeof(depth)); memset(fa, 0, sizeof(fa)); memset(dis1, 0, sizeof(dis1)); cnt = cntt = 0; } //(非严格)计算u到v路径上的最大值跟w的差值 double cal2(int u, int v) { double val=0; for (int i = lg[depth[u]]; i >= 0; -- i) { //倍增找最大 if (depth[fa[u][i]] >= depth[v]) { val = max(val, dis1[u][i]); u = fa[u][i]; } } return val; } double solve(double base) { double ans = 0; bfs(); for (int i = 1; i < n; ++ i) { for (int j = 1; j <= n; ++ j) { int lca = LCA(i, j); rec[i][j] = rec[j][i] = max(cal2(i, lca), cal2(j, lca)); } } for (int i = 1; i < n; i++) { for (int j = i+1; j <= n; j++) { double v = base - rec[i][j]; v = (val[i] + val[j]) / v; ans = max(ans, v); } } return ans; } int main() { // freopen("in.txt","r",stdin); //输入重定向,输入数据将从in.txt文件中读取 // freopen("out1.txt","w",stdout); //输出重定向,输出数据将保存在out.txt文件中 int T; scanf("%d", &T); for(int i = 1; i <= maxn; ++i) //预先算出log_2(i)+1的值,用的时候直接调用就可以了 lg[i] = lg[i-1] + (1 << lg[i-1] == i); //看不懂的可以手推一下 while (T--) { scanf("%d", &n); init(); for(int i = 1; i <= n; ++i) scanf("%lf%lf%lf", &xx[i], &yy[i], &val[i]); for (int i = 1; i < n; ++ i) { for (int j = i+1; j <= n; ++ j) { double w = sqrt((xx[i]-xx[j])*(xx[i]-xx[j])+(yy[i]-yy[j])*(yy[i]-yy[j])); tree[++cntt].from = i, tree[cntt].to = j, tree[cntt].w = w; //cout << i << " " << j << " " << w << endl; } } double base = Kruscal(); double ans = solve(base); printf("%.2f\n", ans); } return 0; }
D - Is There A Second Way Left? UVA - 10462
这几道非严格最小生成树让我成功意识到了我的初始化不够完善。
这道题就是判断有没有最小生成树和次小生成树。

#include <cstring> #include <iostream> #include <queue> #include <cstdio> #include <algorithm> using namespace std; typedef long long ll; const ll linf = 2147483647000000; const int inf = 0x3f3f3f3f; const int maxn = 200+20; const int maxm = 40000+10; struct Edge{ int to, next; ll w; } edge[maxn*2]; struct Tree{ int to, from; ll w; bool operator< (const Tree& tt) const {return w < tt.w;} } tree[maxm]; int n, m; int head[maxn], cnt; int depth[maxn], fa[maxn][19], lg[maxn], fat[maxn]; //fa用于lca查询, fat用于并查集 ll dis1[maxn][19]; int used[maxn]; int cntt; inline void Add_Edge(int u, int v, ll w){ edge[cnt].next = head[u], edge[cnt].to = v, edge[cnt].w = w; head[u] = cnt++; } void bfs() { queue<int> q; q.push(1), depth[1] = 1; while (!q.empty()) { int fath = q.front(); q.pop(); for(int i = head[fath]; ~i; i = edge[i].next) { int now = edge[i].to; if (depth[now]) continue; fa[now][0] = fath, depth[now] = depth[fath] + 1; dis1[now][0] = edge[i].w; q.push(now); for(int j = 1; j <= lg[depth[now]]; ++j) { fa[now][j] = fa[fa[now][j-1]][j-1]; dis1[now][j] = max(dis1[now][j-1], dis1[fa[now][j-1]][j-1]); } } } } int LCA(int x, int y) { if(depth[x] < depth[y]) swap(x, y); while(depth[x] > depth[y]) x = fa[x][lg[depth[x]-depth[y]] - 1]; if(x == y) return x; for(int k = lg[depth[x]] - 1; k >= 0; --k) if(fa[x][k] != fa[y][k]) x = fa[x][k], y = fa[y][k]; return fa[x][0]; } int _find(int x) {return fat[x]==x?x:fat[x]=_find(fat[x]);} ll Kruscal() { ll sum = 0; sort(tree+1, tree+m+1); for (int i = 1; i <= n; ++ i) fat[i] = i; for (int i = 1; i <= m; ++ i) { int u = tree[i].from, v = tree[i].to; ll w = tree[i].w; int fx = _find(u), fy = _find(v); if (fx != fy) { used[i] = 1, sum += tree[i].w, fat[fx] = fy; Add_Edge(u, v, w), Add_Edge(v, u, w); cntt++; } } return sum; } void init() { memset(head, -1, sizeof(head)); memset(used, 0, sizeof(used)); memset(depth, 0, sizeof(depth)); cnt = cntt = 0; memset(dis1, 0, sizeof(dis1)); memset(fa, 0, sizeof(fa)); } //(非严格)计算u到v路径上的最大值 ll cal2(int u, int v) { ll val=0; for (int i = lg[depth[u]]; i >= 0; -- i) { //倍增找最大 if (depth[fa[u][i]] >= depth[v]) { val = max(val, dis1[u][i]); u = fa[u][i]; } } return val; } ll solve(ll base) { ll ans = linf; bfs(); for (int i = 1; i <= m; ++ i) { if (used[i]) continue; int u = tree[i].from, v = tree[i].to; int lca = LCA(u, v); ll w = tree[i].w; ans = min(ans, base+w-max(cal2(u,lca),cal2(v,lca))); } return ans; } int main() { // freopen("in.txt","r",stdin); //输入重定向,输入数据将从in.txt文件中读取 // freopen("out1.txt","w",stdout); //输出重定向,输出数据将保存在out.txt文件中 int T; scanf("%d", &T); for(int i = 1; i < maxn; ++i) //预先算出log_2(i)+1的值,用的时候直接调用就可以了 lg[i] = lg[i-1] + (1 << lg[i-1] == i); //看不懂的可以手推一下 for (int Ti = 1; Ti <= T; ++ Ti) { scanf("%d%d", &n, &m); init(); for(int i = 1; i <= m; ++i) scanf("%d%d%lld", &tree[i].from, &tree[i].to, &tree[i].w); ll base = Kruscal(); cout << "Case #" << Ti << " : "; if (cntt != n-1) { cout << "No way" << endl; continue; } if (m == n-1) { cout << "No second way" << endl; continue; } cout << solve(base) << endl; } return 0; }
最小树形图#
算法流程#
对于每一次循环:
- 贪心找出所谓的“最小带环树形图”,(就是上面的万能图的红边)
顺便记录一下自己是从哪里来的(入边的起点) - 把所有选出的边加到ans里面
- 找环记录环,统计数量
- 如果没环,代表完成了,退出循环
- 把所有不是环上的点全部设置为自己是一个独立环(大小为1的新环)
- 重新设置边权&缩点
- 完成缩点,重新设置n和root,然后初始
附一个朱刘算法 O(VE):

#include <iostream> #include <cstdio> #include <cstring> #include <cmath> using namespace std; typedef long long ll; const int maxn = 1000+50; const int maxm = 50000+50; const int inf = 0x3f3f3f3f; inline int read() { int a=0;char c=getchar(); while(c<'0'||c>'9') c=getchar(); while(c>='0'&&c<='9') a=(a<<1)+(a<<3)+c-'0',c=getchar(); return a; } struct edge{int u,v;ll w;} e[maxm]; int cnt,fa[maxn],scc[maxn],top[maxn]; ll minw[maxn]; //cnt当前图环的数量 //scc[u]代表u节点在第scc[u]个环中 //top[u]代表u所在链的代表元素 类似并查集 //minw[u]为当前连到u点的最短边的边权 fa[v]当前连到v点的最短边的u inline ll zhuliu(int r, int n, int m) { ll ans = 0; while(1) { for (int i = 1; i <= n; ++i) minw[i] = inf; for (int i = 1; i <= m; ++i) if (e[i].u != e[i].v && e[i].w < minw[e[i].v]) {//不是自环 并且边权比选定的还小 fa[e[i].v] = e[i].u, minw[e[i].v] = e[i].w; } int cnt = 0, u; minw[r] = 0; for (int i = 1; i <= n; ++ i) { scc[i] = top[i] = 0; if (minw[i] == inf) return -1; } for (int i = 1; i <= n; ++i) { ans += minw[i]; for (u = i; u != r && top[u] != i && !scc[u]; u = fa[u]) top[u]=i; //找环 //找到包含不在环中的点最多的链 打上标记 if(u != r && !scc[u]) { //这时候还满足条件说明vis[u]==i 即成环,将在环里的点打上环的编号 scc[u] = ++cnt; for (int v = fa[u]; v != u; v = fa[v]) scc[v] = cnt; } } if (!cnt) return ans; //没环就是找到答案了 for (int i = 1; i <= n; ++i) if (!scc[i]) scc[i] = ++cnt;//i节点不存在当前树中 就给他自己成一个环 for (int i = 1; i <= m; ++i) { ll last = minw[e[i].v]; //last等于当前连进v点的边的最小权值 if ((e[i].u=scc[e[i].u]) != (e[i].v=scc[e[i].v])) e[i].w -= last; //当前边的两个端点不在同一个环内 } n = cnt, r = scc[r];//缩完点后 当前点数就为环数 根节点就是根节点所在的环 } } int main() { int T; scanf("%d", &T); for (int Ti = 1; Ti <= T; Ti++) { int n, m; scanf("%d%d", &n, &m); for(int i = 1,u,v,w; i <= m; i++) { scanf("%d%d%lld", &u, &v, &w); e[i] = {u+1,v+1,w}; } printf("Case #%d: ", Ti); ll ans = zhuliu(1,n,m); if (ans != -1) printf("%lld\n", ans); else printf("Possums!\n"); } return 0; }
效率更好的便是用tajan优化的。O(m+nlogn)
tajan优化主要在于找最小入边。使用左偏树和按秩合并并查集优化处理 (虽然我不是很懂)。
存个酷酷的tajan板子 不过这个常数比较大的样子。如果复杂度够的话还是用朱刘吧。

#include <iostream> #include <cstdio> #include <cstring> #include <cstdlib> #define il inline #define ri register #define Size 100050 using namespace std; int fa[Size],cnt,is[Size]; il int find(int); il void read(int&),Union(int,int); struct leftist{ struct point{ int l,r,d,v,t,to; }a[Size]={{0,0,-1,0,0,0}}; int r[Size]; il void merge(int&x,int&y){ if(!x||!y){x^=y;return;} if(a[x].v>a[y].v)swap(x,y); a[y].t-=a[x].t,a[y].v-=a[x].t; merge(a[x].r,y); if(a[a[x].l].d<a[a[x].r].d) a[x].l^=a[x].r^=a[x].l^=a[x].r; a[x].d=a[a[x].r].d+1; } il void spread(int&p){ a[a[p].l].t+=a[p].t,a[a[p].r].t+=a[p].t; a[a[p].l].v+=a[p].t,a[a[p].r].v+=a[p].t; a[p].t=0; } il void pop(int&x){ spread(x),merge(a[x].l,a[x].r),x=a[x].l; } il point*top(int&x){ while(r[x]&&!(find(a[r[x]].to)^x))pop(r[x]); if(!r[x])puts("-1"),exit(0); a[r[x]].to=find(a[r[x]].to); return &a[r[x]]; } }L; int pre[Size]; int main(){ int n,m,r,ans(0);leftist::point*temp; read(n),read(m),read(r),cnt=n,is[r]=r; for(int i(1),u,v,w;i<=m;++i) read(u),read(v),read(w), L.a[i]={0,0,0,w,0,u}, L.merge(L.r[v],u=i); for(int i(1);i<=n<<1;++i)fa[i]=i; for(int i(1),j(i);i<=n;j=++i) while(!is[j]){ while(!is[j]) is[j]=i,j=(temp=L.top(j))->to, ans+=temp->v;if(is[j]^i)break; while(~is[j]) is[j]=-1,j=pre[j]=(temp=L.top(j))->to, temp->t-=temp->v,temp->v=0;++cnt; while(is[j]^i)is[j]=i,Union(j,cnt),j=pre[j]; j=cnt; }return printf("%d",ans),0; } il void Union(int u,int v){ if((u=find(u))^(v=find(v))) L.merge(L.r[v],L.r[u]),fa[u]=v; } il int find(int x){ return x^fa[x]?fa[x]=find(fa[x]):x; } il void read(int&x){ x^=x;ri char c;while(c=getchar(),c<'0'||c>'9'); while(c>='0'&&c<='9')x=(x<<1)+(x<<3)+(c^48),c=getchar(); }
给道裸题吧:E - Command Network POJ - 3164

#include <iostream> #include <cstdio> #include <cstring> #include <cmath> using namespace std; typedef long long ll; const int maxn = 200+50; const int maxm = 50000+50; const int inf = 1e10; inline int read() { int a=0;char c=getchar(); while(c<'0'||c>'9') c=getchar(); while(c>='0'&&c<='9') a=(a<<1)+(a<<3)+c-'0',c=getchar(); return a; } struct edge{int u,v;double w;} e[maxm]; int cnt,fa[maxn],scc[maxn],top[maxn]; double minw[maxn]; //cnt当前图环的数量 //scc[u]代表u节点在第scc[u]个环中 //top[u]代表u所在链的代表元素 类似并查集 //minw[u]为当前连到u点的最短边的边权 fa[v]当前连到v点的最短边的u double zhuliu(int r, int n, int m) { double ans = 0; while(1) { for (int i = 1; i <= n; ++i) minw[i] = inf; for (int i = 1; i <= m; ++i) if (e[i].u != e[i].v && e[i].w < minw[e[i].v]) //不是自环 并且边权比选定的还小 fa[e[i].v] = e[i].u, minw[e[i].v] = e[i].w; int u, cnt = 0; minw[r] = 0; for (int i = 1; i <= n; ++ i) { scc[i] = top[i] = 0; if (minw[i] == inf) return -1; } for (int i = 1; i <= n; ++i) { ans += minw[i]; for (u = i; u != r && top[u] != i && !scc[u]; u = fa[u]) top[u]=i; //找环 //找到包含不在环中的点最多的链 打上标记 if(u != r && !scc[u]) { //这时候还满足条件说明vis[u]==i 即成环,将在环里的点打上环的编号 scc[u] = ++cnt; for (int v = fa[u]; v != u; v = fa[v]) scc[v] = cnt; } } if (!cnt) return ans; //没环就是找到答案了 for (int i = 1; i <= n; ++i) if (!scc[i]) scc[i] = ++cnt;//i节点不存在当前树中 就给他自己成一个环 for (int i = 1; i <= m; ++i) { double last = minw[e[i].v]; //last等于当前连进v点的边的最小权值 if ((e[i].u=scc[e[i].u]) != (e[i].v=scc[e[i].v])) e[i].w -= last; //当前边的两个端点不在同一个环内 } n = cnt, r = scc[r];//缩完点后 当前点数就为环数 根节点就是根节点所在的环 } } double dist(double x1, double y1, double x2, double y2){ return sqrt((x1-x2)*(x1-x2)+(y1-y2)*(y1-y2)); } int main() { int n, m; while (scanf("%d%d", &n, &m) != EOF) { double x[n+1], y[n+1]; for (int i = 1; i <= n; i++) scanf("%lf%lf", &x[i], &y[i]); for(int i = 1,u,v; i <= m; i++) { scanf("%d%d", &u, &v); e[i] = {u,v,dist(x[u],y[u],x[v],y[v])}; } double ans = zhuliu(1,n,m); if (ans != -1) printf("%.2f\n",ans); else printf("poor snoopy\n"); } return 0; }
还有道:F - Teen Girl Squad UVA - 11183

#include <iostream> #include <cstdio> #include <cstring> #include <cmath> using namespace std; typedef long long ll; const int maxn = 1000+50; const int maxm = 50000+50; const int inf = 0x3f3f3f3f; inline int read() { int a=0;char c=getchar(); while(c<'0'||c>'9') c=getchar(); while(c>='0'&&c<='9') a=(a<<1)+(a<<3)+c-'0',c=getchar(); return a; } struct edge{int u,v;ll w;} e[maxm]; int cnt,fa[maxn],scc[maxn],top[maxn]; ll minw[maxn]; //cnt当前图环的数量 //scc[u]代表u节点在第scc[u]个环中 //top[u]代表u所在链的代表元素 类似并查集 //minw[u]为当前连到u点的最短边的边权 fa[v]当前连到v点的最短边的u inline ll zhuliu(int r, int n, int m) { ll ans = 0; while(1) { for (int i = 1; i <= n; ++i) minw[i] = inf; for (int i = 1; i <= m; ++i) if (e[i].u != e[i].v && e[i].w < minw[e[i].v]) {//不是自环 并且边权比选定的还小 fa[e[i].v] = e[i].u, minw[e[i].v] = e[i].w; } int cnt = 0, u; minw[r] = 0; for (int i = 1; i <= n; ++ i) { scc[i] = top[i] = 0; if (minw[i] == inf) return -1; } for (int i = 1; i <= n; ++i) { ans += minw[i]; for (u = i; u != r && top[u] != i && !scc[u]; u = fa[u]) top[u]=i; //找环 //找到包含不在环中的点最多的链 打上标记 if(u != r && !scc[u]) { //这时候还满足条件说明vis[u]==i 即成环,将在环里的点打上环的编号 scc[u] = ++cnt; for (int v = fa[u]; v != u; v = fa[v]) scc[v] = cnt; } } if (!cnt) return ans; //没环就是找到答案了 for (int i = 1; i <= n; ++i) if (!scc[i]) scc[i] = ++cnt;//i节点不存在当前树中 就给他自己成一个环 for (int i = 1; i <= m; ++i) { ll last = minw[e[i].v]; //last等于当前连进v点的边的最小权值 if ((e[i].u=scc[e[i].u]) != (e[i].v=scc[e[i].v])) e[i].w -= last; //当前边的两个端点不在同一个环内 } n = cnt, r = scc[r];//缩完点后 当前点数就为环数 根节点就是根节点所在的环 } } int main() { int T; scanf("%d", &T); for (int Ti = 1; Ti <= T; Ti++) { int n, m; scanf("%d%d", &n, &m); for(int i = 1,u,v,w; i <= m; i++) { scanf("%d%d%lld", &u, &v, &w); e[i] = {u+1,v+1,w}; } printf("Case #%d: ", Ti); ll ans = zhuliu(1,n,m); if (ans != -1) printf("%lld\n", ans); else printf("Possums!\n"); } return 0; }
之后給道无根最小树形图的题目:G - Ice_cream’s world II HDU - 2121
感谢这道题目让我发现了板子有点点问题,现在已经更正了(板子upupup)

#include <iostream> #include <cstdio> #include <cstring> #include <cmath> using namespace std; typedef long long ll; const int maxn = 2000+50; const int maxm = 50000+50; const ll inf = 1e17; int rr; //记录实根位置 ll ans; inline int read() { int a=0;char c=getchar(); while(c<'0'||c>'9') c=getchar(); while(c>='0'&&c<='9') a=(a<<1)+(a<<3)+c-'0',c=getchar(); return a; } struct edge{int u,v;ll w;} e[maxm]; int cnt,fa[maxn],scc[maxn],top[maxn]; ll minw[maxn]; //cnt当前图环的数量 //scc[u]代表u节点在第scc[u]个环中 //top[u]代表u所在链的代表元素 类似并查集 //minw[u]为当前连到u点的最短边的边权 fa[v]当前连到v点的最短边的u inline ll zhuliu(int r, int n, int m) { ll ans = 0; while(1) { for (int i = 1; i <= n; ++i) minw[i] = inf; for (int i = 1; i <= m; ++i) if (e[i].u != e[i].v && e[i].w < minw[e[i].v]) {//不是自环 并且边权比选定的还小 fa[e[i].v] = e[i].u, minw[e[i].v] = e[i].w; //因为有缩点,每个点的下标发生了变化,但新加的边的指向未变, //所以可以通过这个边找到原来点的下标 if (e[i].u == r) rr = i; } int cnt = 0, u; minw[r] = 0; for (int i = 1; i <= n; ++ i) { scc[i] = top[i] = 0; if (minw[i] == inf) return -1; } for (int i = 1; i <= n; ++i) { ans += minw[i]; for (u = i; u != r && top[u] != i && !scc[u]; u = fa[u]) top[u]=i; //找环 //找到包含不在环中的点最多的链 打上标记 if(u != r && !scc[u]) { //这时候还满足条件说明vis[u]==i 即成环,将在环里的点打上环的编号 scc[u] = ++cnt; for (int v = fa[u]; v != u; v = fa[v]) scc[v] = cnt; } } if (!cnt) return ans; //没环就是找到答案了 for (int i = 1; i <= n; ++i) if (!scc[i]) scc[i] = ++cnt;//i节点不存在当前树中 就给他自己成一个环 for (int i = 1; i <= m; ++i) { ll last = minw[e[i].v]; //last等于当前连进v点的边的最小权值 if ((e[i].u=scc[e[i].u]) != (e[i].v=scc[e[i].v])) e[i].w -= last; //当前边的两个端点不在同一个环内 } n = cnt, r = scc[r];//缩完点后 当前点数就为环数 根节点就是根节点所在的环 } } int main() { int n, m; while (~scanf("%d%d", &n, &m)) { ll sum = 0; for(int i = 1,u,v; i <= m; i++) { ll w; scanf("%d%d%lld", &u, &v, &w); e[i] = {u+1,v+1,w}; sum += w; } sum += 1; //设虚根为n+1 for (int i = 1; i <= n; ++ i) e[i+m] = {n+1,i,sum}; //cout << sum << endl; ll ans = zhuliu(n+1,n+1, n+m); //cout << ans << endl; if (ans == -1 || ans >= (2*sum)) { //cout << ans << " " << 2*sum << endl; printf("impossible\n\n"); //不存在 } else { printf("%lld %d\n\n", ans-sum, rr-m-1) ; } } return 0; }
还有一道类似于无根MDST的:H - Transfer water HDU - 4009
这个建点非常酷。

#include <iostream> #include <cstdio> #include <cstring> #include <cmath> using namespace std; typedef long long ll; const int maxn = 1000+50; const int maxm = 1000000+50; const int inf = 0x3f3f3f3f; inline int read() { int a=0;char c=getchar(); while(c<'0'||c>'9') c=getchar(); while(c>='0'&&c<='9') a=(a<<1)+(a<<3)+c-'0',c=getchar(); return a; } struct edge{int u,v;ll w;} e[maxm]; int cnt,fa[maxn],scc[maxn],top[maxn]; ll minw[maxn]; //cnt当前图环的数量 //scc[u]代表u节点在第scc[u]个环中 //top[u]代表u所在链的代表元素 类似并查集 //minw[u]为当前连到u点的最短边的边权 fa[v]当前连到v点的最短边的u inline ll zhuliu(int r, int n, int m) { ll ans = 0; while(1) { for (int i = 1; i <= n; ++i) minw[i] = inf; for (int i = 1; i <= m; ++i) if (e[i].u != e[i].v && e[i].w < minw[e[i].v]) {//不是自环 并且边权比选定的还小 fa[e[i].v] = e[i].u, minw[e[i].v] = e[i].w; } int cnt = 0, u; minw[r] = 0; for (int i = 1; i <= n; ++ i) { scc[i] = top[i] = 0; if (minw[i] == inf) return -1; } for (int i = 1; i <= n; ++i) { ans += minw[i]; for (u = i; u != r && top[u] != i && !scc[u]; u = fa[u]) top[u]=i; //找环 //找到包含不在环中的点最多的链 打上标记 if(u != r && !scc[u]) { //这时候还满足条件说明vis[u]==i 即成环,将在环里的点打上环的编号 scc[u] = ++cnt; for (int v = fa[u]; v != u; v = fa[v]) scc[v] = cnt; } } if (!cnt) return ans; //没环就是找到答案了 for (int i = 1; i <= n; ++i) if (!scc[i]) scc[i] = ++cnt;//i节点不存在当前树中 就给他自己成一个环 for (int i = 1; i <= m; ++i) { ll last = minw[e[i].v]; //last等于当前连进v点的边的最小权值 if ((e[i].u=scc[e[i].u]) != (e[i].v=scc[e[i].v])) e[i].w -= last; //当前边的两个端点不在同一个环内 } n = cnt, r = scc[r];//缩完点后 当前点数就为环数 根节点就是根节点所在的环 } } int cal(int x1, int y1, int z1, int x2, int y2, int z2) { return abs(x1-x2)+abs(y1-y2)+abs(z1-z2); } int main() { int n,X,Y,Z; while(scanf("%d%d%d%d", &n, &X, &Y, &Z) != EOF && (n||X||Y||Z)){ int x[n+1], y[n+1], z[n+1]; for (int i = 1; i <= n; ++ i) scanf("%d%d%d", x+i, y+i, z+i); int m = 0; for(int u = 1,num,v; u <= n; ++ u){ scanf("%d", &num); while(num--){ scanf("%d", &v); e[++m].w = cal(x[u],y[u],z[u],x[v],y[v],z[v]) * Y; if (z[v] > z[u]) e[m].w += Z; e[m].u = u; e[m].v = v; } } for (int i = 1; i <= n; ++i){ e[++m].u = n+1; e[m].v = i; e[m].w = z[i] * X; } ll ans = zhuliu(n+1,n+1,m); printf("%lld\n", ans); } return 0; }
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· AI与.NET技术实操系列:向量存储与相似性搜索在 .NET 中的实现
· 基于Microsoft.Extensions.AI核心库实现RAG应用
· Linux系列:如何用heaptrack跟踪.NET程序的非托管内存泄露
· 开发者必知的日志记录最佳实践
· SQL Server 2025 AI相关能力初探
· 震惊!C++程序真的从main开始吗?99%的程序员都答错了
· winform 绘制太阳,地球,月球 运作规律
· 【硬核科普】Trae如何「偷看」你的代码?零基础破解AI编程运行原理
· 上周热点回顾(3.3-3.9)
· 超详细:普通电脑也行Windows部署deepseek R1训练数据并当服务器共享给他人