P5540 【模板】最小乘积生成树

题目链接https://www.luogu.org/problem/P5540


题意

给出一个 n n n 个点 m m m 条边的无向图,第 i i i 条边有两个权值 a i a_i ai b i b_i bi
求该图的一棵生成树 T T T ,使得( ∑ a e ) ∗ ( ∑ b e ) \sum a_e)*(\sum b_e) ae)(be)最小。


题解

https://www.luogu.org/problemnew/solution/P5540

∑ a e \sum a_e ae看作 x x x ∑ b e \sum b_e be看作 y y y,答案就变成了平面上的一个坐标点 ( x , y ) (x,y) (x,y)。这样平面上最多一共有 n n − 2 n^{n-2} nn2个点(生成树的个数),现在要求出一个点使得 x × y x\times y x×y最小。很容易想到答案在凸包的左下角上,现在就是想办法求出该点。

如果点 A A A x x x最小的点, B B B y y y最小的点,那么点 A A A和点 B B B一定是在凸包上。假设点 C C C是在 A B AB AB的左下处,那点 C C C一定更优,所以我们要找到点一个在 A B AB AB左下处的点 C C C使得三角形 A B C ABC ABC面积最大,或者 A B × A C AB\times AC AB×AC最小。这样求出来的点 C C C也是在凸包上的。

推一下式子: A B × A C = ( x B − x A ) y C + ( y A − y B ) x C − ( x B − x A ) y A + ( y B − y A ) x A AB\times AC=(x_B-x_A)y_C+(y_A-y_B)x_C-(x_B-x_A)y_A+(y_B-y_A)x_A AB×AC=(xBxA)yC+(yAyB)xC(xBxA)yA+(yByA)xA

后面两项是常数,所以我们只要让 ( x B − x A ) y C + ( y A − y B ) x C (x_B-x_A)y_C+(y_A-y_B)x_C (xBxA)yC+(yAyB)xC最小即可。对此我们只要对每条边设一个权值为: ( x B − x A ) a [ i ] + ( y A − y B ) b [ i ] (x_B-x_A)a[i]+(y_A-y_B)b[i] (xBxA)a[i]+(yAyB)b[i],跑一边最小生成树就可以求出点 C C C

求出来点 C C C后,只需要递归求在 A C AC AC C B CB CB左下方的点即可。

因为一张 n n n个点的图,凸包上的点数期望是 ln ⁡ n \sqrt{\ln n} lnn ,我们最多有 n n − 2 n^{n-2} nn2个点,所以最后的复杂度是 O ( m l o g ( m ) ln ⁡ n n − 2 ) O(mlog(m)\sqrt{\ln n^{n-2}}) O(mlog(m)lnnn2 )


#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
const int N=4e5+7;
struct Edge{
    int u,v,a,b,w;
    bool operator<(const Edge k)const{return w<k.w;}
}e[N];
int n,m;
struct Point{
    int x,y;
    Point operator - (const Point k)const{return (Point){x-k.x,y-k.y};}
}ans;
int cross(Point k1,Point k2){return k1.x*k2.y-k2.x*k1.y;}
int fa[N];
int fd(int x){return x==fa[x]?x:fa[x]=fd(fa[x]);}
Point kruskal(){
    Point res=(Point){0,0};
    sort(e+1,e+1+m);
    for(int i=0;i<n;i++) fa[i]=i;
    for(int i=1;i<=m;i++){
        int u=e[i].u,v=e[i].v;
        int fu=fd(u),fv=fd(v);
        if(fu==fv) continue;
        fa[fu]=fv;
        res.x+=e[i].a;
        res.y+=e[i].b;
    }
    ll now=1ll*ans.x*ans.y;
    ll tmp=1ll*res.x*res.y;
    if(now>tmp||(now==tmp&&ans.x>res.x)) ans=res;
    return res;
}

void solve(Point A,Point B){
    for(int i=1;i<=m;i++) e[i].w=(A.y-B.y)*e[i].a+(B.x-A.x)*e[i].b;
    Point C=kruskal();
    if(cross(B-A,C-A)>=0) return;
    solve(A,C);solve(C,B);
}
int main()
{
    ans.x=ans.y=256*200;
    scanf("%d%d",&n,&m);
    for(int i=1;i<=m;i++){
        scanf("%d%d%d%d",&e[i].u,&e[i].v,&e[i].a,&e[i].b);
    }
    for(int i=1;i<=m;i++) e[i].w=e[i].a;
    Point A=kruskal();
    for(int i=1;i<=m;i++) e[i].w=e[i].b;
    Point B=kruskal();
    solve(A,B);
    printf("%d %d\n",ans.x,ans.y);
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值