【学习笔记】最小乘积模型(凸包分治)

例题:P5540 [BalkanOI 2011] timeismoney

给出一个 \(n\) 个点 \(m\) 条边的无向图,第 \(i\) 条边有两个权值 \(a_i\)\(b_i\)

求该图的一棵生成树 \(T\) ,使得

\[\left(\sum_{e\in T}a_e\right)\times\left(\sum_{e\in T}b_e\right) \]

最小。

怎么说,感觉挺难想的。反正我做不出来

下面直接讲做法:

可以把 \(\sum a_e\)\(\sum b_e\) 看作是 \(x,y\),对应平面上的点。

那么如果要使 \(x\times y\) 最小,那么这个点一定位于平面的下凸壳上。

但是由于平面上的点很多,不能一个个的枚举点。

可以先求出两个点 \(A,B\),分别对应 \(x\) 最小,和 \(y\) 最小的点,那显然这两个点一定在凸包上。那么剩下的点一定在两个点连线的左下方。

我们可以找到距离直线最远一个点,那么它一定也在凸包上,然后往下递归直到找不到点为止。

然后考虑如何找到这个点。

我们将其设为点 \(C\),距离 \(\mathrm{AB}\) 最远的点与 \(A,B\) 连成的三角形的面积一定是最大的,即 \(S=-\overrightarrow{\mathrm{AB}}\times \overrightarrow{\mathrm{AC}}\)。相当于要求 \(\overrightarrow{\mathrm{AB}}\times \overrightarrow{\mathrm{AC}}\) 最小。

展开,\(\overrightarrow{\mathrm{AB}}\times \overrightarrow{\mathrm{AC}}=(x_B-x_A)(y_C-y_A)-(y_B-y_A)(x_B-x_A)=(y_A-y_B)x_C+(x_B-x_A)y_C+y_Bx_A-x_By_A\)

后一部分是常量,对大小关系没有影响,可以忽略掉。

可以转换为每一条边权是 \(w_C=(y_A-y_B)x_C+(x_B-x_A)y_C\) 然后跑一边最小生成树就好了。

代码:

#include<bits/stdc++.h>
#define IOS cin.tie(0),cout.tie(0),ios::sync_with_stdio(0)
#define ll long long
#define db double
#define pb push_back
#define eb emplace_back
#define MS(x,y) memset(x,y,sizeof(x))
#define MC(x,y) memcpy(x,y,sizeof(x))
#define PLL pair<ll,ll>
#define PII pair<int,int>
#define lb(x) ((x)&(-x))
using namespace std;
const int N=1e4+20,M=1e5+20;
const ll INF=1ll<<60,mod=998244353;
namespace H_H{
    int n,m;
    PII a[N];
    ll w[N];
    vector<int> to[N];
    struct vec{
        ll w,x,y;
        inline vec(){w=x=y=0;}
        inline vec(ll _w,ll _x,ll _y){
            w=_w,x=_x,y=_y;
        }
        inline vec operator + (const vec&xx) const{
            return vec(w+xx.w,x+xx.x,y+xx.y);
        }
        inline vec operator - (const vec&xx) const{
            return vec(w-xx.w,x-xx.x,y-xx.y);
        }
        inline bool operator < (const vec&xx) const{
            if(w==xx.w) return x<xx.x;
            return w<xx.w;
        }
        inline ll operator * (const vec&xx) const{
            return x*xx.y-y*xx.x;
        }
    };
    ll ans,sumx,sumy;
    struct edge{
        int x,y,id;
        inline bool operator < (const edge &xx) const{
            if(w[id]==w[xx.id]){
                if(a[id].first==a[xx.id].first) return a[id].second<a[xx.id].second;
                return a[id].first<a[xx.id].first;
            }
            return w[id]<w[xx.id];
        }
    }e[N];
    int ff[N];
    inline int find(int x){
        if(x==ff[x]) return x;
        return ff[x]=find(ff[x]);
    }
    inline vec calc(ll x,ll y){
        for(int i=1;i<=m;i++) w[i]=1ll*x*a[i].first+1ll*y*a[i].second;//转换点权
        sort(e+1,e+1+m);
        iota(ff,ff+n,0);
        vec res;
        for(int i=1;i<=m;i++){//最小生成树
            auto [u,v,id]=e[i];
            int fu=find(u),fv=find(v);
            if(fu==fv) continue;
            ff[fu]=fv;
            res=res+vec(w[id],a[id].first,a[id].second);
        }
        return res;
    }
    inline void solve(vec A,vec B){
        if(A.x*B.y>=ans) return ;//优化剪枝,会快很多
        if(A.y-B.y<=0 || B.x-A.x<=0) return ;
        vec C=calc(A.y-B.y,B.x-A.x);
        if(ans==C.x*C.y && sumx>C.x) sumx=C.x,sumy=C.y;
        if(ans>C.x*C.y) ans=C.x*C.y,sumx=C.x,sumy=C.y;
        if((B-A)*(C-A)>=0) return ;
        solve(A,C);solve(C,B);
    }
    int main(){
        cin>>n>>m;
        for(int i=1,u,v;i<=m;i++){
            cin>>u>>v>>a[i].first>>a[i].second;
            e[i]={u,v,i};
        }
        ans=sumx=sumy=INF;
        vec A=calc(1,0),B=calc(0,1);
        ans=min(A.x*A.y,B.x*B.y);
        if(ans==A.x*A.y && sumx>A.x) sumx=A.x,sumy=A.y;
        if(ans==B.x*B.y && sumx>B.x) sumx=B.x,sumy=B.y;
        solve(A,B);
        cout<<sumx<<" "<<sumy<<"\n";
        return 0;
    }
}
signed main(){ 
    IOS;H_H::main();
    return 0;
}
posted @ 2026-08-17 13:35  tyh_27  阅读(5)  评论(0)    收藏  举报