最小生成树的拓展

本文会随着弟弟我的学习进度来进行更新

度限制最小生成树

  1. 最小度生成树:np-hard
  2. 最小k度限制生成树:经典问题

做法(以下皆假设固定的点为rt):

  1. 删除rt点,并用剩下的点建一个最小生成树森林
  2. 如果有p个联通块,要满足最小k度限制生成树必须有p<=kp<=k。对于每个联通块,找出与rt相连的最小的一条边。这样就用了一个度数为p的解法
  3. 考虑如何从p转移到p+1
    假设
    fa[v]为v的父亲,
    mx[v]为从rt到v路径上不与rt直接相连的边的最大值,
    w[u][v]为v与u的边的权值
    mx[v]=max(mx[fa[v]],w[v][fa[v]])mx[v]=max(mx[fa[v]],w[v][fa[v]])
    那么p+1的解法就有ans[p+1]=ans[p]+min{mx[v]+w[v][rt]}ans[p+1]=ans[p]+min\{-mx[v]+w[v][rt]\}

例题:poj-1639

最小直径生成树

最小直径生成树就是n个点m条边的无向图G里面直径最小的生成树T。

做法:
其实找到图G的绝对中心(就是树上到各点距离最大值最小的点,这个绝对中心可以在边上e)。
那绝对中心怎么找呢?

  1. 枚举绝对中心所在的边e(u,v,w)
  2. 对于每个点i,我们定义函数$$f(i)=min(dist(u,i)+x,dist(v,i)+w-x)$$这个函数代表的是绝对中心在边(u,v,w)上x这个位置的时候与i点的距离。然后对于所有点将函数画在图上并取max(因为是直径要取最大的值),就可是画出这个经典的图像啦。然后对于绝对中心在这条边(u,v,w)时候的直径就是整个函数的最小值
    在这里插入图片描述
  3. 有了这个图像之后我们要怎么求最低点呢?
    可以发现在去掉多余的点(例如:对于两个点i,j有dist(u,i)>dist(u,j)&&dist(v,i)>dist(v,j),那么j点就是多余的),并对dist(u,i)dist(u,i)从小到大排序之后,对应的dist(v,i)一定是降序(证明也很简单就不证明了)因此交点一定是两个排序后相邻函数的交点。因此直接枚举就好。
  4. **算法时间复杂度O(n3)O(n^3)**

代码:

#include<bits/stdc++.h>
using namespace std;
const int maxn=1e5+5;
int n,m,ans,dist[maxn][maxn],rank[maxn][maxn];
int read(){
    int x=0,f=1;char ch;
    for(ch=getchar();ch<'0'||ch>'9';ch=getchar())if(ch=='-')f=-1;
    for(;ch>='0'&&ch<='9';ch=getchar())x=x*10+ch-'0';
    return x*f;
}
int main(){
    n=read();m=read();memset(dist,63,sizeof(dist));
    for(int i=1;i<=n;i++)dist[i][i]=0;
    for(int i=1,x,y,z;i<=m;i++){
        x=read();y=read();z=read();
        dist[x][y]=dist[y][x]=min(dist[x][y],z);
    }
    for(int i=1;i<=n;i++)for(int j=1;j<=m;j++)rank[i][j]=j;
    //预处理两点间最短路径 
    for(int k=1;k<=n;k++)
    for(int i=1;i<=n;i++)if(k!=i)
    for(int j=1;j<=n;j++)if(i!=j&&k!=j)
    dist[i][j]=min(dist[i][j],dist[i][k]+dist[k][j]);
    //对dist(i,j)进行排序 
    for(int i=1;i<=n;i++)
    for(int j=1;j<=n;j++)
    for(int k=j+1;k<=n;k++)
    if(dist[i][rank[i][j]]<dist[i][rank[i][k]])swap(rank[i][j],rank[i][k]);
    //处理答案 
	ans=1e9;
    for(int i=1;i<=n;i++)
    for(int j=1;j<=n;j++)if(i!=j){
        ans=min(ans,dist[i][rank[i][1]]+dist[i][rank[i][2]]);
        ans=min(ans,dist[j][rank[j][1]]+dist[j][rank[j][2]]);
        for(int b=1,a=2;a<=n;a++)
        if(dist[j][rank[i][b]]<dist[j][rank[i][a]])
        ans=min(ans,dist[j][rank[i][b]]+dist[i][rank[i][a]]+dist[i][j]),b=a;
    }
    printf("%d\n",ans);
    return 0;
}

例题:https://www.spoj.com/problems/MDST/

最小乘积生成树

n个点,m条边,每条边有两个权值a,b,最小乘积生成树就是ab\sum{a}*\sum{b}最小的生成树

做法:
首先我们定义生成树T的a=x\sum{a}=xb=y\sum{b}=y,那么每一棵生成树可以看作一个点(x,y)(x,y),现在问题就变成了我要求一个生成树有xy=kx*y=k最小。变换形式后有y=kxy=\frac{k}{x}
也就是说要找一个最贴近坐标轴点,而这些点可能的点肯定是一个下凸包的形状。
接下来的过程可以分为3步:

  1. 求出与 x 轴和 y 轴最近的点

aia_i作为边权,求出的最小生成树就是与 x 轴最近的点。

bib_i作为边权,求出的最小生成树就是与 yy 轴最近的点。

为了下面讲解的方便,设与 xx 轴最近的点为 A ,与 yy 轴最近的点为 B 。

  1. 找到一个在 AB 的左下方且离 AB 最远的点

图大概是这样的
在这里插入图片描述
实际上我们要的C点就是在AB下方并使得SABCS_{ABC}最大的点,对于SABCS_{ABC}我们可以通过叉乘得到,公式如下:
SABC=ABAC=(xBxA)(yCyA)(xCxA)(yByA)=(xBxA)yC(xBxA)yA(yByA)xC+(yByA)xA=(xBxA)yC(yByA)xC(xBxA)yA+(yByA)xAS_{ABC}=\vec{AB}*\vec{AC} \\\qquad\enspace\thinspace=(x_B−x_A)(y_C−y_A)−(x_C−x_A)(y_B−y_A) \\\qquad\enspace\thinspace=(x_B−x_A)y_C-(x_B−x_A)y_A-(y_B−y_A)x_C+(y_B−y_A)x_A \\\qquad\enspace\thinspace=(x_B−x_A)y_C-(y_B−y_A)x_C-(x_B−x_A)y_A+(y_B−y_A)x_A
后面两项是一个常熟,关键是前面两项怎么算。
实际上,前面两项的最小值是将边权设为(xBxA)bi(yByA)ai(x_B−x_A)b_i-(y_B−y_A)a_i之后得到的最小生成树。
这样C就可以求出来了。

  1. 递归处理AC和CB

分别把AC和CB丢进去第二步做即可,终止条件为不存在点在当前边的下方。
AB×AC>=0{\vec{AB}}\times{ \vec{AC}}>=0

  1. 因为决策点构成了一个含有n!n!个点的凸包。因为在nn个点的凸包上的点数期望是lnn\sqrt {\ln n}的,所以时间复杂度为O(nlognlnn!)\mathrm{O}(n\log n\sqrt {\ln n!}))。时间

代码:

#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
const int maxn=200+10;
const int maxm=10000+10;
const int inf=0x3f3f3f3f;
int n,m,T;

inline int read() {
    int X=0,w=1; char a=getchar();
    while (a<'0'||a>'9') { if (a=='-') w=-1; a=getchar(); }
    while (a>='0'&&a<='9') X=X*10+a-'0',a=getchar();
    return X*w;
}


struct UFS { //并查集
    int f[maxn];
    inline void init(int n) { for (int i=1;i<=n;++i) f[i]=i; }
    inline int find(int x) { return x==f[x]?x:f[x]=find(f[x]); }
    inline void merge(int x,int y) { x=find(x),y=find(y); if (x!=y) f[x]=y; }
    inline int check(int x,int y) { return find(x)==find(y); }
};

struct Edge { int u,v,w,a,b; };
inline int cmp(Edge a,Edge b) { return a.w<b.w; }

//计算几何相关
struct Point { int x,y; };
typedef Point Vector;
Vector operator -(Point a,Point b) { return (Point){a.x-b.x,a.y-b.y}; }
inline int cross(Vector a,Vector b) { return a.x*b.y-a.y*b.x; }

UFS S;
Point ans=(Point){inf,inf};
Edge e[maxm];

//Kruskal,返回找到的点
inline Point Kruskal() {
    Point res=(Point){0,0}; int tot=0;
    sort(e+1,e+m+1,cmp);
	S.init(n);
    for (int i=1,u,v,a,b;i<=m;++i) {
        u=e[i].u,v=e[i].v;
		a=e[i].a,b=e[i].b;
        if (S.check(u,v)) continue;
        S.merge(u,v);
		res.x+=a,res.y+=b;
		++tot;
        if (tot==n-1) break;
    }
    /*更新答案*/
    ll Ans=1ll*ans.x*ans.y;
	ll now=1ll*res.x*res.y;
    if (Ans>now||(Ans==now&&ans.x>res.x)) ans=res;
    return res;
}

//递归处理
inline void solve(Point A,Point B) {
    for (int i=1;i<=m;++i) 
		e[i].w=e[i].b*(B.x-A.x)+e[i].a*(A.y-B.y);
    Point C=Kruskal();
    if (cross(B-A,C-A)>=0) 
		return; //边界
    solve(A,C);
	solve(C,B);
}

int main() {
    n=read(),m=read();
    for (int i=1;i<=m;++i)
        e[i].u=read()+1,e[i].v=read()+1,e[i].a=read(),e[i].b=read();
    
	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);
    return 0;
}


Q.E.D.