【学习笔记】最小生成树系列的必做经典题

最小生成树系列

  • 【模板】最小生成树
    • prim算法
    • kruskal算法
    • Borůvka (Sollin)算法
  • 次小生成树
  • 最小生成树计数
  • 最优比率生成树
  • 最小乘积生成树
  • 最小度限制生成树
  • 最小方差树

【模板】最小生成树

prim算法

最小生成树的prim\text{prim}prim类似于最短路的dijkstra\text{dijkstra}dijkstra

本质是:贪心

  • 首先随便钦定一个点为根,(一般来说我们习惯是111)

    定义disidis_idisi表示现在最小生成树中某个点到iii的最短距离

    初始化全是最大值(除了钦定的点是000),最小生成树还是个空树

  • 然后每一次选择还没有加入最小生成树的距离最小的点,加入最小生成树,统计记录距离贡献

  • 利用这个点更新剩下的没有进入最小生成树的点的距离

  • 每一次都会加入一个点,nnn次遍历后,还没有加入所有的点,意味着根本没有最小生成树

显然,这与dijkstra\text{dijkstra}dijkstra是一样的,严格O(n2)O(n^2)O(n2)

适用于稠密图

#include <cstdio>
#include <cstring>
#include <iostream>
using namespace std;
#define maxn 200005
int n, m, cnt = 1;
int to[maxn << 1], nxt[maxn << 1], head[maxn], cost[maxn << 1], dis[maxn];
bool vis[maxn];int main() {scanf( "%d %d", &n, &m );for( int i = 1, u, v, w;i <= m;i ++ ) {scanf( "%d %d %d", &u, &v, &w );to[cnt] = v, nxt[cnt] = head[u], cost[cnt] = w, head[u] = cnt ++;to[cnt] = u, nxt[cnt] = head[v], cost[cnt] = w, head[v] = cnt ++;}memset( dis, 0x7f, sizeof( dis ) );cnt = dis[1] = 0; int ans = 0;for( int k = 1;k <= n;k ++ ) {int now = 0;for( int i = 1;i <= n;i ++ )if( dis[i] < dis[now] and ! vis[i] ) now = i;if( ! now ) break;else vis[now] = 1, cnt ++, ans += dis[now];for( int i = head[now];i;i = nxt[i] )dis[to[i]] = min( dis[to[i]], cost[i] );}if( cnt ^ n ) printf( "orz\n" );else printf( "%d\n", ans );return 0;
}

kruskal算法

本质:贪心

辅助工具:并查集

  • 将所有边按边权排序

  • 贪心的,选择边权最小的操作

    但是可能这条边操作后会导致环的出现,所以需要并查集判断,边的两个点是否已经连接

  • trick\text{trick}trick:当成功加入n−1n-1n1条边后,意味着最小生成树已经构建成功,提前跳出循环

时间复杂度的瓶颈在排序身上,O(mlog⁡m)O(m\log m)O(mlogm),适用于稀疏图

#include <cstdio>
#include <cstring>
#include <iostream>
#include <algorithm>
using namespace std;
#define maxn 200005
int n, m;
int f[maxn];
struct node { int u, v, w; }E[maxn];int find( int x ) { return f[x] == x ? x : f[x] = find( f[x] ); }int main() {scanf( "%d %d", &n, &m );for( int i = 1, u, v, w;i <= m;i ++ ) {scanf( "%d %d %d", &u, &v, &w );E[i] = { u, v, w };}sort( E + 1, E + m + 1, []( node x, node y ) { return x.w < y.w; } );for( int i = 1;i <= n;i ++ ) f[i] = i;int cnt = 0, ans = 0;for( int i = 1;i <= m;i ++ ) {int u = find( E[i].u ), v = find( E[i].v ), w = E[i].w;if( u ^ v ) {f[v] = u, ans += w, cnt ++;if( cnt == n - 1 ) break;}}if( cnt != n - 1 ) printf( "orz\n" );else printf( "%d\n", ans );return 0;
}

Borůvka (Sollin)算法

Boru˚vka (Sollin)\text{Borůvka (Sollin)}Boru˚vka (Sollin) 可以堪称是prim\text{prim}prim的多源扩展版

  • 初始时,每个点独立看成一个连通块

  • 枚举所有的边,用还未使用的边更新不同连通块之间的最短边,或者说是更新每个连通块连出去的最短边

  • 然后枚举每个有最短边连出的连通块,将最短边加入最小生成树,合并边连接的两点所在的不同连通块

    由于有些连通块的最短边是一条边,前面可能已经操作了,用布尔数组标记一下即可,不要重复操作边

  • 重复操作直到只剩一个连通块

每次合并,连通块个数都会/2/2/2,所以外层是log⁡n\log nlogn的,这也是为什么略优于kruskal\text{kruskal}kruskal的原因

时间复杂度O(mlog⁡n)O(m\log n)O(mlogn)

#include <cstdio>
#define maxn 200005
struct node { int u, v, w; }E[maxn];
int n, m, ans, cnt;
int f[maxn], g[maxn];
bool vis[maxn];int find( int x ) { return f[x] == x ? x : f[x] = find( f[x] ); }void merge( int u, int v ) { u = find( u ), v = find( v ), f[v] = u; }int main() {scanf( "%d %d", &n, &m );for( int i = 1, u, v, w;i <= m;i ++ ) {scanf( "%d %d %d", &u, &v, &w );E[i] = { u, v, w };}for( int i = 1;i <= n;i ++ ) f[i] = i;while( 1 ) {bool flag = 1;for( int i = 1;i <= n;i ++ ) g[i] = 0;for( int i = 1;i <= m;i ++ ) {if( vis[i] ) continue;int u = find( E[i].u ), v = find( E[i].v );if( u == v ) continue;if( ! g[u] or E[g[u]].w > E[i].w ) g[u] = i;if( ! g[v] or E[g[v]].w > E[i].w ) g[v] = i;}for( int i = 1;i <= n;i ++ ) if( g[i] and ! vis[g[i]] ) {cnt ++;flag = 0;vis[g[i]] = 1;merge( E[g[i]].u, E[g[i]].v );ans += E[g[i]].w;}if( flag ) break;}if( cnt != n - 1 ) printf( "orz\n" );else printf( "%d\n", ans );return 0;
}

一般根据边数量的范围决定是prim/kruskal\text{prim}/\text{kruskal}prim/kruskal

Boru˚vka (Sollin)\text{Borůvka (Sollin)}Boru˚vka (Sollin)很少见,但是遇到0/1异或的题目,思想可用于启发式合并


接下来就是在最小生成树的基础上各种提高的最小**生成树系列

次小生成树

luoguP4180 [BJWC2010]严格次小生成树

最小生成树性质1 :往最小生成树上加一条边,就会形成一个环,环为u↔lca(u,v)↔vu\leftrightarrow lca(u,v)\leftrightarrow vulca(u,v)v

最小生成树性质2 :新加边的边权一定大于等于最小生成树的所有边权

最小生成树性质3 :如果用新边替换最小生成树的一条边,最小生成树的边权和一定增大或不变

最小生成树性质4 :如果用新边替换,为了不形成环,必须替换的是u↔lca(u,v)↔vu\leftrightarrow lca(u,v)\leftrightarrow vulca(u,v)v路径中的一条边。显然,假设加了新边,那就形成了一个环,需要断掉这个环上的另外一条边

次小生成树,顾名思义,一定是边权和第二小的另一个生成树

但是,新加哪条边呢??——不知道欸——那就枚举吧!

枚举不在最小生成树上的边,用该边替换掉原生成树上的某条边

问题又来了?替换哪条边??——不知道欸——再枚举吧

不行!你怕是T傻了,肯定是替换最大边权的边

为了不形成环,只能是u↔lca(u,v)↔vu\leftrightarrow lca(u,v)\leftrightarrow vulca(u,v)v上的一条边权最大的边

不会有人想着每次都去走一遍吧

倍增提前预处理即可

但是万一最大边权更新加边权一样大,不就不能满足严格大于了吗

这就是你在倍增爬树时候,需要维护的啦,不仅要有最大边权,还要有严格次大边权,这就是代码能力问题了

#include <cstdio>
#include <vector>
#include <iostream>
#include <algorithm>
using namespace std;
#define maxn 300005
#define int long long
struct node {int u, v, w;node() {}node( int U, int V, int W ) {u = U, v = V, w = W;}
}edge[maxn];
vector < pair < int, int > > G[maxn];
int n, m, cnt, ans;
bool vis[maxn];
int f[maxn], dep[maxn];
int fa[maxn][20], w[maxn][20];int find( int x ) { return f[x] == x ? x : f[x] = find( f[x] ); }void kruskal() {sort( edge + 1, edge + cnt + 1, []( node x, node y ) { return x.w < y.w; } );for( int i = 1;i <= n;i ++ ) f[i] = i;int used = 0;for( int i = 1;i <= cnt;i ++ ) {int u = edge[i].u, v = edge[i].v, w = edge[i].w;int fu = find( u ), fv = find( v );if( fu == fv ) continue;else {vis[i] = 1, used ++, ans += w, f[fv] = fu;G[u].push_back( make_pair( v, w ) );G[v].push_back( make_pair( u, w ) );}if( used == n - 1 ) return;}
}void dfs( int u, int father ) {dep[u] = dep[father] + 1;for( int i = 1;i < 20;i ++ ) {fa[u][i] = fa[fa[u][i - 1]][i - 1];w[u][i] = max( w[u][i - 1], w[fa[u][i - 1]][i - 1] );}for( int i = 0;i < G[u].size();i ++ ) {int v = G[u][i].first, dis = G[u][i].second;if( v == father ) continue;fa[v][0] = u, w[v][0] = dis;dfs( v, u );}
}pair < int, int > lca( int u, int v ) {int maxx = -1, temp = -1;if( dep[u] < dep[v] ) swap( u, v );for( int i = 19;~ i;i -- )if( dep[fa[u][i]] >= dep[v] ) {if( w[u][i] > maxx ) temp = maxx, maxx = w[u][i];if( w[u][i] < maxx && w[u][i] > temp ) temp = w[u][i];u = fa[u][i];}if( u == v ) return make_pair( maxx, temp );for( int i = 19;~ i;i -- )if( fa[u][i] != fa[v][i] ) {if( w[u][i] > maxx ) temp = maxx, maxx = w[u][i];if( w[u][i] < maxx && w[u][i] > temp ) temp = w[u][i];if( w[v][i] > maxx ) temp = maxx, maxx = w[v][i];if( w[v][i] < maxx && w[v][i] > temp ) temp = w[v][i];u = fa[u][i], v = fa[v][i];}if( w[u][0] > maxx ) temp = maxx, maxx = w[u][0];if( w[u][0] < maxx && w[u][0] > temp ) temp = w[u][0];if( w[v][0] > maxx ) temp = maxx, maxx = w[v][0];if( w[v][0] < maxx && w[v][0] > temp ) temp = w[v][0];return make_pair( maxx, temp ); 
}signed main() {scanf( "%lld %lld", &n, &m );for( int i = 1, u, v, w;i <= m;i ++ ) {scanf( "%lld %lld %lld", &u, &v, &w );if( u == v ) continue;else edge[++ cnt] = node( u, v, w );}kruskal();dfs( 1, 0 );int result = 1ll << 60;for( int i = 1;i <= cnt;i ++ )if( vis[i] ) continue;else {pair < int, int > tmp = lca( edge[i].u, edge[i].v );if( tmp.first != edge[i].w )result = min( result, ans - tmp.first + edge[i].w );else if( tmp.second != -1 )result = min( result, ans - tmp.second + edge[i].w );}printf( "%lld\n", result );return 0;
}

最小生成树计数

luoguP4208 [JSOI2008]最小生成树计数

最小生成树性质5 : 若T1,T2T_1,T_2T1,T2都是最小生成树,则T1,T2T_1,T_2T1,T2的各边权是相同的(可能连接的边不同),换言之,若边权各不相同,则最小生成树唯一

这个模板题,相同权值边不超过101010条,完全符合爆搜的条件

先随便求一个最小生成树,然后记录每个权值使用的边数,然后爆搜使用哪几条该权值的边,同样不会出现环的情况

用矩阵原理乘起来即可

当然可以用矩阵树定理,但这明显串台了

#include <cstdio>
#include <algorithm>
using namespace std;
#define mod 31011
#define maxn 1005
struct node { int u, v, w; }E[maxn];
struct noded { int l, r, w, used; }G[maxn];
int n, m, cnt, tot, ans;
int f[maxn];int find( int x ) { return x == f[x] ? x : find( f[x] ); }void dfs( int End, int now, int used, int need ) {if( now == End ) {if( used == need ) tot ++;return;}int u = E[now].u, v = E[now].v;int fu = find( u ), fv = find( v );if( fu ^ fv ) {f[fv] = fu;dfs( End, now + 1, used + 1, need );f[fu] = fu, f[fv] = fv;}dfs( End, now + 1, used, need );
}int main() {scanf( "%d %d", &n, &m );for( int i = 1, u, v, w;i <= m;i ++ ) {scanf( "%d %d %d", &u, &v, &w );E[i] = { u, v, w };}sort( E + 1, E + m + 1, []( node x, node y ) { return x.w < y.w; } );for( int i = 1;i <= n;i ++ ) f[i] = i;for( int i = 1;i <= m;i ++ ) {int u = E[i].u, v = E[i].v, w = E[i].w;if( w ^ E[i - 1].w ) {G[cnt].r = i - 1;G[++ cnt].l = i;G[cnt].w = w;}int fu = find( u ), fv = find( v );if( fu ^ fv ) {f[fv] = fu;G[cnt].used ++;tot ++;}}if( tot != n - 1 ) return ! printf( "0\n" );for( int i = 1;i <= n;i ++ ) f[i] = i;ans = 1; G[cnt].r = m;for( int i = 1;i <= cnt;i ++ ) {tot = 0;dfs( G[i].r + 1, G[i].l, 0, G[i].used );ans = ans * tot % mod;for( int j = G[i].l;j <= G[i].r;j ++ ) {int u = E[j].u, v = E[j].v;int fu = find( u ), fv = find( v );if( fu ^ fv ) f[fv] = fu;}}printf( "%lld\n", ans );return 0;
}

最优比率生成树

POJ Desert King

最优比率生成树类似最短路的0/10/10/1分数规划

k=∑Eibenefit[i]∑Eicost[i]k=\frac{\sum_{E_i}\text{benefit[i]}}{\sum_{E_i}\text{cost[i]}}k=Eicost[i]Eibenefit[i],则∑Eibenefit[i]≥k∗∑Eicost[i]\sum_{E_i}\text{benefit[i]}\ge k*\sum_{E_i}\text{cost[i]}Eibenefit[i]kEicost[i]

即,∑Ei(benefit[i]-k*cost[i])≥0\sum_{E_i}(\text{benefit[i]-k*cost[i]})\ge 0Ei(benefit[i]-k*cost[i])0

显然这是具有单调性,最优比率就是kkk

直接二分最优比率,然后重新定义每条边的边权为benefit[i]-k*cost[i]\text{benefit[i]-k*cost[i]}benefit[i]-k*cost[i]

再求个最小生成树的边权和,如果≥0\ge 00证明这个比率是可取的,且有可能更高

否则就下调二分的比率

#include <cstdio>
#include <cmath>
#include <iostream>
using namespace std;
#define maxn 1005
#define eps 1e-5
int n;
double x[maxn], y[maxn], h[maxn], w[maxn];
double dist[maxn][maxn], cost[maxn][maxn];
bool vis[maxn];bool check( double x ) {for( int i = 0;i <= n;i ++ ) vis[i] = 0, w[i] = 1e18;w[1] = 0; double ans = 0;for( int k = 1;k <= n;k ++ ) {int now = 0;for( int i = 1;i <= n;i ++ )if( ! vis[i] and w[i] < w[now] ) now = i;if( ! now ) break;else vis[now] = 1;ans += w[now];for( int i = 1;i <= n;i ++ )w[i] = min( w[i], cost[now][i] - x * dist[now][i] );}return ans >= 0;
}int main() {while( scanf( "%d", &n ) and n ) {for( int i = 1;i <= n;i ++ )scanf( "%lf %lf %lf", &x[i], &y[i], &h[i] );for( int i = 1;i <= n;i ++ )for( int j = i + 1;j <= n;j ++ ) {dist[i][j] = dist[j][i] = sqrt( ( x[i] - x[j] ) * ( x[i] - x[j] ) + ( y[i] - y[j] ) * ( y[i] - y[j] ) );cost[i][j] = cost[j][i] = fabs( h[i] - h[j] );}double l = 0, r = 1e7;while( r - l > eps ) {double mid = ( l + r ) / 2;if( check( mid ) ) l = mid;else r = mid;}printf( "%.3f\n", l );}return 0;
}

最小乘积生成树

luoguP5540 [BalkanOI2011] timeismoney | 最小乘积生成树

最优比率生成树是∑Eibenefit[i]∑Eicost[i]\frac{\sum_{E_i}\text{benefit[i]}}{\sum_{E_i}\text{cost[i]}}Eicost[i]Eibenefit[i]最大的最小生成树

而最小乘积生成树是∑Eibenefit[i]∗∑Eicost[i]\sum_{E_i}\text{benefit[i]}*\sum_{E_i}\text{cost[i]}Eibenefit[i]Eicost[i]的最小值的最小生成树

对于这种模型,我们选择放到二维平面上考虑

即,∑Eibenefit[i]\sum_{E_i}\text{benefit[i]}Eibenefit[i]为横坐标,∑Eicost[i]\sum_{E_i}\text{cost[i]}Eicost[i]为纵坐标

那么这个点的横纵坐标积就是这个乘积生成树的价值了

所以我们需要想办法使得这个积越小越好

先找出两个特殊的乘积生成树,一个是横坐标最小的点,一个是纵坐标最小的点

将这两个点连线,显然在这条线的左下方的点的积更小

在这里插入图片描述

怎么找C?

利用计算机几何用叉积进行判定,显然AB⃗×AC⃗<0\vec{AB}\times \vec{AC}<0AB×AC<0证明CCCABABAB的左下角
AB⃗×AC⃗=(XB−XA,YB−YA)×(XC−XA,YC−YA)\vec{AB}\times \vec{AC}=(X_B-X_A,Y_B-Y_A)\times (X_C-X_A,Y_C-Y_A)AB×AC=(XBXA,YBYA)×(XCXA,YCYA)=(XB−XA)(YC−YA)−(YB−YA)(XC−XA)=(X_B-X_A)(Y_C-Y_A)-(Y_B-Y_A)(X_C-X_A)=(XBXA)(YCYA)(YBYA)(XCXA)=(XB−XA)YC+(YA−YB)XC−(XB−XA)YA+(YB−YA)XA=(X_B-X_A)Y_C+(Y_A-Y_B)X_C-(X_B-X_A)Y_A+(Y_B-Y_A)X_A =(XBXA)YC+(YAYB)XC(XBXA)YA+(YBYA)XA
发现后面两项是常数项,我们想尽可能地寻找到左下角最远的最小积CCC也就是说需要最小化(XB−XA)YC+(YA−YB)XC(X_B-X_A)Y_C+(Y_A-Y_B)X_C(XBXA)YC+(YAYB)XC

把边权变成(XB−XA)b[i]+(YA−YB)a[i](X_B-X_A)b[i]+(Y_A-Y_B)a[i](XBXA)b[i]+(YAYB)a[i]

然后求出最小生成树,最小生成树的边的aaa值和就是新点的横坐标,bbb值和就是纵坐标了

#include <cstdio>
#include <algorithm>
using namespace std;
#define maxn 10005
#define int long long
#define inf 0x7f7f7f7f
struct point {int x, y;friend point operator - ( point s, point t ) { return { s.x - t.x, s.y - t.y }; }friend int cross( point s, point t ) { return s.x * t.y - s.y * t.x; }
}ans;
struct node {int u, v, a, b, w;
}E[maxn];
int n, m;
int f[maxn];int find( int x ) { return f[x] == x ? x : f[x] = find( f[x] ); }point kruskal() {point p = { 0, 0 };for( int i = 1, cnt = 0;i <= m;i ++ ) {int u = E[i].u, v = E[i].v;int fu = find( u ), fv = find( v );if( fu ^ fv ) {f[fv] = fu;p.x += E[i].a;p.y += E[i].b;cnt ++;if( cnt == n - 1 ) break;}}if( ans.x * ans.y > p.x * p.y or ( ans.x * ans.y == p.x * p.y and ans.x > p.x ) )ans = p;return p;
}void solve( point A, point B ) {for( int i = 1;i <= m;i ++ )E[i].w = ( B.x - A.x ) * E[i].b + ( A.y - B.y ) * E[i].a;sort( E + 1, E + m + 1, []( node x, node y ) { return x.w < y.w; } );for( int i = 1;i <= n;i ++ ) f[i] = i;point C = kruskal();if( cross( B - A, C - A ) >= 0 ) return;solve( A, C ), solve( C, B );
}signed main() {scanf( "%lld %lld", &n, &m );for( int i = 1, u, v, a, b;i <= m;i ++ ) {scanf( "%lld %lld %lld %lld", &u, &v, &a, &b );E[i] = { u + 1, v + 1, a, b };}ans = { inf, inf };sort( E + 1, E + m + 1, []( node x, node y ) { return x.a < y.a; } );for( int i = 1;i <= n;i ++ ) f[i] = i;point A = kruskal();sort( E + 1, E + m + 1, []( node x, node y ) { return x.b < y.b; } );for( int i = 1;i <= n;i ++ ) f[i] = i;point B = kruskal();solve( A, B );printf( "%lld %lld\n", ans.x, ans.y );return 0;
} 

最小度限制生成树

luoguP5633

当限制某个物品恰好/至少/至多选kkk个的限制性问题就是WQS二分解决的了

WQS二分,其实就是二分了一个新权重,赋加给与指定物品相关联的部分

再根据算法进行选择,最后判断是否达到了限制再调整权重,最后结果去除掉权重造成的影响即可

本题要求指定的某个点连的边恰好选择指定条数的最小生成树

那么就二分一个权重,加给所有指定点连接的特殊边,然后再跑最小生成树,记录下使用的特殊边的数量,再调整权重即可

#include <cstdio>
#include <algorithm>
using namespace std;
#define int long long
#define maxn 500005
const int inf = 1e9;
int n, m, s, k, cnt1, cnt2, cost;
struct node { int u, v, w; }G[maxn], O[maxn], E[maxn];
int f[maxn];int find( int x ) { return x == f[x] ? x : f[x] = find( f[x] ); }void msort( int x ) {for( int i = 1;i <= cnt1;i ++ ) G[i].w += x;int i = 1, j = 1, k = 1;while( i <= cnt1 and j <= cnt2 ) {if( O[j].w < G[i].w ) E[k ++] = O[j ++];else E[k ++] = G[i ++];}while( i <= cnt1 ) E[k ++] = G[i ++];while( j <= cnt2 ) E[k ++] = O[j ++];for( int i = 1;i <= cnt1;i ++ ) G[i].w -= x;
} bool check( int x ) {msort( x );for( int i = 1;i <= n;i ++ ) f[i] = i;int used = 0, cnt = 0;for( int i = 1;i <= m;i ++ ) {int u = E[i].u, v = E[i].v;int fu = find( u ), fv = find( v );if( fu ^ fv ) {f[fv] = fu, cnt ++, used += ( u == s or v == s );if( cnt == n - 1 ) break;}}return cnt == n - 1 and used >= k;
}bool calc( int x ) {msort( x );for( int i = 1;i <= n;i ++ ) f[i] = i;int cnt = 0, used = 0;for( int i = 1;i <= m;i ++ ) {int u = E[i].u, v = E[i].v, w = E[i].w;int fu = find( u ), fv = find( v );if( fu ^ fv ) {f[fv] = fu, cnt ++, cost += w, used += ( u == s or v == s );if( cnt == n - 1 ) break;}}return cnt == n - 1 and used == k;
}signed main() {scanf( "%lld %lld %lld %lld", &n, &m, &s, &k );for( int i = 1, u, v, w;i <= m;i ++ ) {scanf( "%lld %lld %lld", &u, &v, &w );if( u == s or v == s ) G[++ cnt1] = { u, v, w };else O[++ cnt2] = { u, v, w };	}sort( G + 1, G + cnt1 + 1, []( node x, node y ) { return x.w < y.w; } );sort( O + 1, O + cnt2 + 1, []( node x, node y ) { return x.w < y.w; } );int l = -inf, r = inf, ans = 1e18;while( l <= r ) {int mid = ( l + r ) >> 1;if( check( mid ) ) ans = mid, l = mid + 1;else r = mid - 1;}if( ans == 1e18 or ! calc( ans ) ) printf( "Impossible\n" );else printf( "%lld\n", cost - ans * k );return 0;
}

最小方差树

BZOJ Tree之最小方差树

方差其实与标准差是一样的

直接枚举一个假想的平均值×(n−1)\times (n-1)×(n1),即枚举总和

注意,此题不能枚举平均值,因为观察数据边权在100100100内,然后精度又达到了四位浮点小数,直接平均值枚举丢精严重;因为显然我们只能枚举整数,而不是小数(那得到天荒地老,有精无时)

然后为了标准差小一点,肯定是选择边权与平均值越接近越好

一个小tricktricktrick:将边权先排个序,然后根据枚举的边权平均值,将边分为左(权值小于枚举值)右(权值大于等于枚举值)的两类边,类似归并的左边(从后往前取),右边(从前往后取),选择最优的可选择的边

然后就可以算出真正的平均值,记录一下真正使用的最小生成树的边,就是个计算问题了

#include <cstdio>
#include <iostream>
#include <algorithm>
using namespace std;
#define eps 1e-6
#define maxn 105
#define maxm 2005
struct node { int u, v; double w; } E[maxm];
int n, m;
int f[maxn];
bool vis[maxm];int find( int x ) { return f[x] == x ? x : f[x] = find( f[x] ); }void merge( int id ) { f[find( E[id].v )] = find( E[id].u ); }int main() {scanf( "%d %d", &n, &m );for( int i = 1;i <= m;i ++ ) {int u, v; double w;scanf( "%d %d %lf", &u, &v, &w );E[i] = { u, v, w }; }sort( E + 1, E + m + 1, []( node x, node y ) { return x.w < y.w; } );double ans = 2e9;for( int k = 0, ip = 1;k <= ( n - 1 ) * 100;k ++ ) {while( ip <= m and E[ip].w * ( n - 1 ) < k ) ip ++;int l = ip - 1, r = ip;for( int i = 1;i <= n;i ++ ) f[i] = i;for( int i = 1;i <= m;i ++ ) vis[i] = 0;int cnt = 0; double sum = 0;while( ++ cnt != n ) {while( l >= 1 and find( E[l].u ) == find( E[l].v ) ) l --;while( r <= m and find( E[r].u ) == find( E[r].v ) ) r ++;if( l >= 1 and r <= m ) {if( k - E[l].w * ( n - 1 ) < E[r].w * ( n - 1 ) - k ) vis[l] = 1, sum += E[l].w, merge( l ), -- l;else vis[r] = 1, sum += E[r].w, merge( r ), ++ r;}else if( l >= 1 ) vis[l] = 1, sum += E[l].w, merge( l ), -- l;else vis[r] = 1, sum += E[r].w, merge( r ), ++ r;}double ave = sum / ( n - 1 );double ret = 0;for( int i = 1;i <= m;i ++ ) if( vis[i] ) ret += ( E[i].w - ave ) * ( E[i].w - ave );ans = min( ans, ret );}printf( "%.4f\n", sqrt( ans / ( n - 1 ) ) );return 0;
} 

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/316967.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

通过 Azure Pipelines 实现持续集成之docker容器化

IntroAzure DevOps Pipeline 现在对于公开的项目完全免费&#xff0c;这对于开源项目来讲无疑是个巨大的好消息&#xff0c;在 Github 的 Marketplace 里有个 Azure Pipeline&#xff0c;就是微软的 Azure DevOps Pipeline。实现 Docker 容器化的持续集成实现的目标&#xff1a…

Acwing 1082. 数字游戏

Acwing 1082. 数字游戏 题意&#xff1a; 现在大家决定玩一个游戏&#xff0c;指定一个整数闭区间 [a,b]&#xff0c;问这个区间内有多少个不降数。 题解&#xff1a; 利用数位dp的套路来做 我们还是利用前缀和来做 我们先求1~n中满足情况的个数 对于一个n位数&#xff0c;…

【招聘(南京)】南京纳龙科技有限公司招高级.net开发工程师

南京纳龙科技有限公司成立于2002年12月&#xff0c;隶属纳龙科技在南京成立的研发中心&#xff0c;坐落于南京市雨花台区。公司立志以守护人类心脏健康为使命&#xff0c;专注推动心电信息化技术的发展&#xff0c;为全国各级医疗机构提供心电检查、诊断一体化的解决方案。公司…

Acwing 1081. 度的数量(以及本人对数位dp的浅薄理解)

题意&#xff1a; 求给定区间 [X,Y] 中满足下列条件的整数个数&#xff1a;这个数恰好等于 K 个互不相等的 B 的整数次幂之和。 题解&#xff1a; 数位DP 技巧1&#xff1a;[X,Y]>f(Y)-f(X-1) 技巧2&#xff1a;用树的方式来考虑。 在本题中&#xff0c;题意是问[X,Y]中…

EFCore动态切换Schema

最近做个分库分表项目&#xff0c;用到schema的切换感觉还是有些坑的&#xff0c;在此分享下。 先简要说下我们的分库分表分库分表规则我定的规则是&#xff0c;订单号&#xff08;数字&#xff09;除以16&#xff0c;得出的结果为这个订单所在的数据库&#xff0c;然后他的余数…

东莞.NET俱乐部线下技术沙龙-活动报名

自广州.NET技术俱乐部在2018年12月08日线下活动顺利开展后&#xff0c;东莞作为兄弟城市&#xff0c;也想通过线下活动的方式&#xff0c;点燃东莞.NET技术的熊熊之火。现决定先借助广州、深圳兄弟城市的帮助下&#xff0c;开展一场东莞方主办的线下活动&#xff0c;聚集东莞本…

如何撰写较受欢迎的技术文章

本来我这篇文章的标题是 “如何撰写受欢迎的技术文章”&#xff0c;但反复斟酌之下&#xff0c;还是加了一个“较”字&#xff0c;这主要是考虑我不是什么知名作者&#xff0c;写的文章大多也谈不上很受欢迎&#xff0c;贸然地谈“受欢迎” 是有点忐忑的&#xff0c;而改成现在…

Recursive sequence HDU - 5950

Recursive sequence HDU - 5950 题意&#xff1a; 给你一个式子&#xff1a;f[n]2f[n-2]f[n-1]n4 给你f[1]和f[2]&#xff0c;给你一个n&#xff0c;求f[n] f[1],f[2],n<231 题解&#xff1a; 很明显&#xff0c;矩阵快速幂&#xff0c;但是太久没做这种题&#xff0c;我…

使用Http-Repl工具测试ASP.NET Core 2.2中的Web Api项目

今天&#xff0c;Visual Studio中没有内置工具来测试WEB API。使用浏览器&#xff0c;只能测试http GET请求。您需要使用Postman&#xff0c;SoapUI&#xff0c;Fiddler或Swagger等第三方工具来执行WEB API的完整测试。在ASP.NET Core 2.2中&#xff0c;引入了一个名为“http-r…

Docker最全教程之使用TeamCity来完成内部CI、CD流程(十七)

本篇教程主要讲解基于容器服务搭建TeamCity服务&#xff0c;并且完成内部项目的CI流程配置。教程中也分享了一个简单的CI、CD流程&#xff0c;仅作探讨。不过由于篇幅有限&#xff0c;完整的DevOps&#xff0c;我们后续独立探讨。 为了降低容器的使用门槛以及便于大家将容器技…

自动将 NuGet 包的引用方式从 packages.config 升级为 PackageReference

在前段时间我写了一篇迁移 csproj 格式的博客 将 WPF、UWP 以及其他各种类型的旧 csproj 迁移成基于 Microsoft.NET.Sdk 的新 csproj&#xff0c;不过全过程是手工进行的&#xff0c;而且到最后处理 XAML 问题也非常头疼。现在&#xff0c;我们可以利用工具自动地完成这个过程。…

ASP.NET Core 自定义认证方式--请求头认证

Intro最近开始真正的实践了一些网关的东西&#xff0c;最近写几篇文章分享一下我的实践以及遇到的问题。本文主要介绍网关后面的服务如何进行认证。解决思路网关可以做一部分的认证和授权&#xff0c;服务内部有时候也会需要用户的信息&#xff0c;这时该怎么办呢&#xff0c;我…

P7516 [省选联考 2021 A/B 卷] 图函数

解析 纯纯的人类智慧题。 关键性质&#xff1a;vvv 可以在计算 f(u,G)f(u,G)f(u,G) 时产生贡献&#xff0c;当且仅当 GGG 中 u,vu,vu,v 之间可以通过 [v,n][v,n][v,n] 的点互相到达。 充分性较为显然&#xff0c;编号更大的点不会比 vvv 先删去&#xff0c;所以必然在 vvv 时…

开发语言大爆炸的时代,究竟谁主沉浮?

开发语言大爆炸的时代&#xff0c;究竟谁主沉浮&#xff1f;当这个系列本来打算是写人物的&#xff0c;而且是写我们身边那些优秀的开发者&#xff0c;然而当第一篇文章&#xff0c;写的是关于我们长沙.NET社区的优秀开发者邹琼俊&#xff0c;发表在博客园之后&#xff0c;有一…

Asp.NETCore轻松学系列阅读指引目录

前言耗时两个多月&#xff0c;坚持写这个入门系列文章&#xff0c;就是想给后来者更好更快的上手体验&#xff0c;这个系列可以说是从入门到进阶&#xff0c;适合没有 .NETCore 编程经验到小白同学&#xff0c;也适合从 .NET Framework 迁移到 .NETCore 的朋友。本系列从安装环…

eShopOnContainers 知多少[9]:Ocelot gateways

引言客户端与微服务的通信问题永远是一个绕不开的问题&#xff0c;对于小型微服务应用&#xff0c;客户端与微服务可以使用直连的方式进行通信&#xff0c;但对于对于大型的微服务应用我们将不得不面对以下问题&#xff1a;如何降低客户端到后台的请求数量&#xff0c;并减少与…

Acwing 1072. 树的最长路径

Acwing 1072. 树的最长路径 题意&#xff1a; 每个边有权值&#xff0c;求树的直径 题解&#xff1a; 两遍dfs可以求&#xff0c;这里用树形dp的方法&#xff0c;我们将1作为根节点来看这棵树 我们可以将点看作是钉子&#xff0c;边就是挂在钉子上的绳子&#xff0c;我们只…

.NET/C# 获取一个正在运行的进程的命令行参数

在自己的进程内部&#xff0c;我们可以通过 Main 函数传入的参数&#xff0c;也可以通过 Environment.GetCommandLineArgs 来获取命令行参数。但是&#xff0c;可以通过什么方式来获取另一个运行着的程序的命令行参数呢&#xff1f;进程内部获取传入参数的方法&#xff0c;可以…

听说,霸都.NET技术社区准备搞线下聚会了?

.NET Core实战项目交流群日常交流嗨&#xff0c;你听说了没有&#xff1f;霸都.NET技术社区准备搞线下聚会了&#xff01;啥时候的事情啊&#xff1f;最近才知道的消息啊&#xff01;那你是从哪里知道的消息呢&#xff1f;.NET Core项目实战交流群&#xff08;637326624&#x…

P4383 [八省联考 2018] 林克卡特树(wqs二分、树形dp)

解析 它还真的不难。 乐。 这题没做出来有些谔谔。 外层wqs二分显而易见&#xff0c;里面不知道为啥我总觉得这个题可以贪心。 然后一直试图在原树直径上下功夫&#xff0c;一筹莫展。 看到题解“dp”两个字这题也就做完了… 就相当于要把一棵树分成若干条无交链&#xff0c;每…