【模板】最小生成树
prim算法
最小生成树的 prim \text{prim} prim类似于最短路的 dijkstra \text{dijkstra} dijkstra
本质是:贪心
-
首先随便钦定一个点为根,(一般来说我们习惯是 1 1 1)
定义 d i s i dis_i disi表示现在最小生成树中某个点到 i i i的最短距离
初始化全是最大值(除了钦定的点是 0 0 0),最小生成树还是个空树
-
然后每一次选择还没有加入最小生成树的距离最小的点,加入最小生成树,统计记录距离贡献
-
利用这个点更新剩下的没有进入最小生成树的点的距离
-
每一次都会加入一个点, n n n次遍历后,还没有加入所有的点,意味着根本没有最小生成树
显然,这与 dijkstra \text{dijkstra} dijkstra是一样的,严格 O ( n 2 ) 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 − 1 n-1 n−1条边后,意味着最小生成树已经构建成功,提前跳出循环
时间复杂度的瓶颈在排序身上, O ( m log 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)算法
Bor u ˚ vka (Sollin) \text{Borůvka (Sollin)} Boru˚vka (Sollin) 可以堪称是 prim \text{prim} prim的多源扩展版
-
初始时,每个点独立看成一个连通块
-
枚举所有的边,用还未使用的边更新不同连通块之间的最短边,或者说是更新每个连通块连出去的最短边
-
然后枚举每个有最短边连出的连通块,将最短边加入最小生成树,合并边连接的两点所在的不同连通块
由于有些连通块的最短边是一条边,前面可能已经操作了,用布尔数组标记一下即可,不要重复操作边
-
重复操作直到只剩一个连通块
每次合并,连通块个数都会 / 2 /2 /2,所以外层是 log n \log n logn的,这也是为什么略优于 kruskal \text{kruskal} kruskal的原因
时间复杂度 O ( m log 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
Bor
u
˚
vka (Sollin)
\text{Borůvka (Sollin)}
Boru˚vka (Sollin)很少见,但是遇到0/1
异或的题目,思想可用于启发式合并
接下来就是在最小生成树的基础上各种提高的最小**生成树系列
次小生成树
最小生成树性质1 :往最小生成树上加一条边,就会形成一个环,环为 u ↔ l c a ( u , v ) ↔ v u\leftrightarrow lca(u,v)\leftrightarrow v u↔lca(u,v)↔v
最小生成树性质2 :新加边的边权一定大于等于最小生成树的所有边权
最小生成树性质3 :如果用新边替换最小生成树的一条边,最小生成树的边权和一定增大或不变
最小生成树性质4 :如果用新边替换,为了不形成环,必须替换的是 u ↔ l c a ( u , v ) ↔ v u\leftrightarrow lca(u,v)\leftrightarrow v u↔lca(u,v)↔v路径中的一条边。显然,假设加了新边,那就形成了一个环,需要断掉这个环上的另外一条边
次小生成树,顾名思义,一定是边权和第二小的另一个生成树
但是,新加哪条边呢??——不知道欸——那就枚举吧!
枚举不在最小生成树上的边,用该边替换掉原生成树上的某条边
问题又来了?替换哪条边??——不知道欸——再枚举吧
不行!你怕是T傻了,肯定是替换最大边权的边
为了不形成环,只能是 u ↔ l c a ( u , v ) ↔ v u\leftrightarrow lca(u,v)\leftrightarrow v u↔lca(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;
}
最小生成树计数
最小生成树性质5 : 若 T 1 , T 2 T_1,T_2 T1,T2都是最小生成树,则 T 1 , T 2 T_1,T_2 T1,T2的各边权是相同的(可能连接的边不同),换言之,若边权各不相同,则最小生成树唯一
这个模板题,相同权值边不超过 10 10 10条,完全符合爆搜的条件
先随便求一个最小生成树,然后记录每个权值使用的边数,然后爆搜使用哪几条该权值的边,同样不会出现环的情况
用矩阵原理乘起来即可
当然可以用矩阵树定理,但这明显串台了
#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;
}
最优比率生成树
最优比率生成树类似最短路的 0 / 1 0/1 0/1分数规划
令 k = ∑ E i benefit[i] ∑ E i cost[i] k=\frac{\sum_{E_i}\text{benefit[i]}}{\sum_{E_i}\text{cost[i]}} k=∑Eicost[i]∑Eibenefit[i],则 ∑ E i benefit[i] ≥ k ∗ ∑ E i cost[i] \sum_{E_i}\text{benefit[i]}\ge k*\sum_{E_i}\text{cost[i]} ∑Eibenefit[i]≥k∗∑Eicost[i]
即, ∑ E i ( benefit[i]-k*cost[i] ) ≥ 0 \sum_{E_i}(\text{benefit[i]-k*cost[i]})\ge 0 ∑Ei(benefit[i]-k*cost[i])≥0
显然这是具有单调性,最优比率就是 k k k
直接二分最优比率,然后重新定义每条边的边权为 benefit[i]-k*cost[i] \text{benefit[i]-k*cost[i]} benefit[i]-k*cost[i]
再求个最小生成树的边权和,如果 ≥ 0 \ge 0 ≥0证明这个比率是可取的,且有可能更高
否则就下调二分的比率
#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 | 最小乘积生成树
最优比率生成树是 ∑ E i benefit[i] ∑ E i cost[i] \frac{\sum_{E_i}\text{benefit[i]}}{\sum_{E_i}\text{cost[i]}} ∑Eicost[i]∑Eibenefit[i]最大的最小生成树
而最小乘积生成树是 ∑ E i benefit[i] ∗ ∑ E i cost[i] \sum_{E_i}\text{benefit[i]}*\sum_{E_i}\text{cost[i]} ∑Eibenefit[i]∗∑Eicost[i]的最小值的最小生成树
对于这种模型,我们选择放到二维平面上考虑
即, ∑ E i benefit[i] \sum_{E_i}\text{benefit[i]} ∑Eibenefit[i]为横坐标, ∑ E i cost[i] \sum_{E_i}\text{cost[i]} ∑Eicost[i]为纵坐标
那么这个点的横纵坐标积就是这个乘积生成树的价值了
所以我们需要想办法使得这个积越小越好
先找出两个特殊的乘积生成树,一个是横坐标最小的点,一个是纵坐标最小的点
将这两个点连线,显然在这条线的左下方的点的积更小
怎么找C?
利用计算机几何用叉积进行判定,显然
A
B
⃗
×
A
C
⃗
<
0
\vec{AB}\times \vec{AC}<0
AB×AC<0证明
C
C
C在
A
B
AB
AB的左下角
A
B
⃗
×
A
C
⃗
=
(
X
B
−
X
A
,
Y
B
−
Y
A
)
×
(
X
C
−
X
A
,
Y
C
−
Y
A
)
\vec{AB}\times \vec{AC}=(X_B-X_A,Y_B-Y_A)\times (X_C-X_A,Y_C-Y_A)
AB×AC=(XB−XA,YB−YA)×(XC−XA,YC−YA)
=
(
X
B
−
X
A
)
(
Y
C
−
Y
A
)
−
(
Y
B
−
Y
A
)
(
X
C
−
X
A
)
=(X_B-X_A)(Y_C-Y_A)-(Y_B-Y_A)(X_C-X_A)
=(XB−XA)(YC−YA)−(YB−YA)(XC−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
=(X_B-X_A)Y_C+(Y_A-Y_B)X_C-(X_B-X_A)Y_A+(Y_B-Y_A)X_A
=(XB−XA)YC+(YA−YB)XC−(XB−XA)YA+(YB−YA)XA
发现后面两项是常数项,我们想尽可能地寻找到左下角最远的最小积
C
C
C也就是说需要最小化
(
X
B
−
X
A
)
Y
C
+
(
Y
A
−
Y
B
)
X
C
(X_B-X_A)Y_C+(Y_A-Y_B)X_C
(XB−XA)YC+(YA−YB)XC
把边权变成 ( X B − X A ) b [ i ] + ( Y A − Y B ) a [ i ] (X_B-X_A)b[i]+(Y_A-Y_B)a[i] (XB−XA)b[i]+(YA−YB)a[i]
然后求出最小生成树,最小生成树的边的 a a a值和就是新点的横坐标, b b b值和就是纵坐标了
#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;
}
最小度限制生成树
当限制某个物品恰好/至少/至多选 k k k个的限制性问题就是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;
}
最小方差树
方差其实与标准差是一样的
直接枚举一个假想的平均值 × ( n − 1 ) \times (n-1) ×(n−1),即枚举总和
注意,此题不能枚举平均值,因为观察数据边权在 100 100 100内,然后精度又达到了四位浮点小数,直接平均值枚举丢精严重;因为显然我们只能枚举整数,而不是小数(那得到天荒地老,有精无时)
然后为了标准差小一点,肯定是选择边权与平均值越接近越好
一个小 t r i c k trick trick:将边权先排个序,然后根据枚举的边权平均值,将边分为左(权值小于枚举值)右(权值大于等于枚举值)的两类边,类似归并的左边(从后往前取),右边(从前往后取),选择最优的可选择的边
然后就可以算出真正的平均值,记录一下真正使用的最小生成树的边,就是个计算问题了
#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;
}