最小生成树的拓展
本文会随着弟弟我的学习进度来进行更新
度限制最小生成树
- 最小度生成树:np-hard
- 最小k度限制生成树:经典问题
做法(以下皆假设固定的点为rt):
- 删除rt点,并用剩下的点建一个最小生成树森林
- 如果有p个联通块,要满足最小k度限制生成树必须有。对于每个联通块,找出与rt相连的最小的一条边。这样就用了一个度数为p的解法
- 考虑如何从p转移到p+1。
假设
fa[v]为v的父亲,
mx[v]为从rt到v路径上不与rt直接相连的边的最大值,
w[u][v]为v与u的边的权值
则
那么p+1的解法就有
例题:poj-1639
最小直径生成树
最小直径生成树就是n个点m条边的无向图G里面直径最小的生成树T。
做法:
其实找到图G的绝对中心(就是树上到各点距离最大值最小的点,这个绝对中心可以在边上e)。
那绝对中心怎么找呢?
- 枚举绝对中心所在的边e(u,v,w)
- 对于每个点i,我们定义函数$$f(i)=min(dist(u,i)+x,dist(v,i)+w-x)$$这个函数代表的是绝对中心在边(u,v,w)上x这个位置的时候与i点的距离。然后对于所有点将函数画在图上并取max(因为是直径要取最大的值),就可是画出这个经典的图像啦。然后对于绝对中心在这条边(u,v,w)时候的直径就是整个函数的最小值。
- 有了这个图像之后我们要怎么求最低点呢?
可以发现在去掉多余的点(例如:对于两个点i,j有dist(u,i)>dist(u,j)&&dist(v,i)>dist(v,j),那么j点就是多余的),并对从小到大排序之后,对应的dist(v,i)一定是降序(证明也很简单就不证明了)因此交点一定是两个排序后相邻函数的交点。因此直接枚举就好。 - **算法时间复杂度
代码:
#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,最小乘积生成树就是最小的生成树
做法:
首先我们定义生成树T的,,那么每一棵生成树可以看作一个点,现在问题就变成了我要求一个生成树有最小。变换形式后有。
也就是说要找一个最贴近坐标轴点,而这些点可能的点肯定是一个下凸包的形状。
接下来的过程可以分为3步:
- 求出与 x 轴和 y 轴最近的点
把 作为边权,求出的最小生成树就是与 x 轴最近的点。
把 作为边权,求出的最小生成树就是与 yy 轴最近的点。
为了下面讲解的方便,设与 xx 轴最近的点为 A ,与 yy 轴最近的点为 B 。
- 找到一个在 AB 的左下方且离 AB 最远的点
图大概是这样的
实际上我们要的C点就是在AB下方并使得最大的点,对于我们可以通过叉乘得到,公式如下:
后面两项是一个常熟,关键是前面两项怎么算。
实际上,前面两项的最小值是将边权设为之后得到的最小生成树。
这样C就可以求出来了。
- 递归处理AC和CB
分别把AC和CB丢进去第二步做即可,终止条件为不存在点在当前边的下方。
即
- 因为决策点构成了一个含有个点的凸包。因为在个点的凸包上的点数期望是的,所以时间复杂度为)。时间
代码:
#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.