乘积规划…神犇称其为隐式自适应凸包…
设每棵生成树为坐标系上的一个点,sigma(x[i])为横坐标,sigma(y[i])为纵坐标。则问题转化为求一个点,使得xy=k最小。
注意到这是一个反比例函数。所以显然的,有可能成为最优解的点集是一个凸包。
首先找到x最小的点A与y最小的点B,再找到离AB最远的点C,形成了一个三角形,这个三角形内部的点不可能成为最优答案(最优解的点集是一个凸包),所以我们继续递归AC,CB即可,对于找到的每一个点C尝试更新答案即可。
photo
怎么找到离一条直线最远的点呢?因为C离AB最远,所以S△ABC面积最大。即向量AB与向量AC的叉积最小(因为叉积是有向面积,是负数,所以取最小)
最小化:(B.x-A.x)(C.y-A.y)-(B.y-A.y)(C.x-A.x)
=(B.x-A.x)C.y+(A.y-B.y)*C.x - A.y(B.x-A.x)+A.x(B.y-A.y)
后面部分是常数,也就是最小化前面的部分。
将每条边的边权e[i].w设为e[i].t*(a.y-b.y)+e[i].c*(b.x-a.x);跑一遍最小生成树即可。
当叉积为正时,就是找不到一个那样的C点,return即可
有一个小技巧,kruskal记录下当前已经合并了的联通块的个数,是个小常数剪枝。还是比较有用的。
复杂度我也不知道…>_<…因为凸包上的点不会很多,所以速度应该还不错…如果谁能证明求告知…
#include<cstdio>
#include<cstring>
#include<iostream>
#include<algorithm>
//by:MirrorGray
using namespace std;
const int N=211111;
int n,m,fa[N];
struct P{
int x,y;
P(int a=0,int b=0){x=a;y=b;}
P operator -(const P&b){
return P(x-b.x,y-b.y);
}
friend int X(const P&a,const P&b){
return a.x*b.y-a.y*b.x;
}
bool operator <(const P&b)const{
if(x*y!=b.x*b.y)return x*y<b.x*b.y;
return x<b.x;
}
void op(){
printf("%d %d\n",x,y);
}
}ans;
struct edge{
int a,b,t,c,w;
void read(){
scanf("%d%d%d%d",&a,&b,&t,&c);
a++;b++;
}
bool operator <(const edge&b)const{
return w<b.w;
}
}e[N];
int find(int x){
return fa[x]==x?x:fa[x]=find(fa[x]);
}
P kruskal(){
P ret;int tmp=0;
sort(e+1,e+1+m);
for(int i=1;i<=n;i++)fa[i]=i;
for(int i=1;i<=m&&tmp<n-1;i++){
int fx=find(e[i].a),fy=find(e[i].b);
if(fx==fy)continue;
tmp++;
fa[fx]=fy;ret.x+=e[i].t;ret.y+=e[i].c;
}
ans=min(ans,ret);
return ret;
}
void solve(P a,P b){
for(int i=1;i<=m;i++)e[i].w=e[i].t*(a.y-b.y)+e[i].c*(b.x-a.x);
P c=kruskal();
if(X(b-a,c-a)>=0)return ;
solve(a,c);solve(c,b);
}
int main(){
scanf("%d%d",&n,&m);
ans=P(40000,40000);
for(int i=1;i<=m;i++)e[i].read();
for(int i=1;i<=m;i++)e[i].w=e[i].t;
P t1=kruskal();
for(int i=1;i<=m;i++)e[i].w=e[i].c;
P t2=kruskal();
solve(t1,t2);
ans.op();
return 0;
}