最小生成树的拓展
本文会随着弟弟我的学习进度来进行更新
度限制最小生成树
- 最小度生成树:np-hard
- 最小k度限制生成树:经典问题
做法(以下皆假设固定的点为rt):
- 删除rt点,并用剩下的点建一个最小生成树森林
- 如果有p个联通块,要满足最小k度限制生成树必须有 p < = k p<=k p<=k。对于每个联通块,找出与rt相连的最小的一条边。这样就用了一个度数为p的解法
- 考虑如何从p转移到p+1。
假设
fa[v]为v的父亲,
mx[v]为从rt到v路径上不与rt直接相连的边的最大值,
w[u][v]为v与u的边的权值
则 m x [ v ] = m a x ( m x [ f a [ v ] ] , w [ v ] [ f a [ v ] ] ) mx[v]=max(mx[fa[v]],w[v][fa[v]]) mx[v]=max(mx[fa[v]],w[v][fa[v]])
那么p+1的解法就有 a n s [ p + 1 ] = a n s [ p ] + m i n { − m x [ v ] + w [ v ] [ r t ] } 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)。
那绝对中心怎么找呢?
- 枚举绝对中心所在的边e(u,v,w)
- 对于每个点i,我们定义函数
f
(
i
)
=
m
i
n
(
d
i
s
t
(
u
,
i
)
+
x
,
d
i
s
t
(
v
,
i
)
+
w
−
x
)
f(i)=min(dist(u,i)+x,dist(v,i)+w-x)
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点就是多余的),并对 d i s t ( u , i ) dist(u,i) dist(u,i)从小到大排序之后,对应的dist(v,i)一定是降序(证明也很简单就不证明了)因此交点一定是两个排序后相邻函数的交点。因此直接枚举就好。 - **算法时间复杂度 O ( n 3 ) ∗ ∗ O(n^3)** O(n3)∗∗
代码:
#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,最小乘积生成树就是 ∑ a ∗ ∑ b \sum{a}*\sum{b} ∑a∗∑b最小的生成树
做法:
首先我们定义生成树T的
∑
a
=
x
\sum{a}=x
∑a=x,
∑
b
=
y
\sum{b}=y
∑b=y,那么每一棵生成树可以看作一个点
(
x
,
y
)
(x,y)
(x,y),现在问题就变成了我要求一个生成树有
x
∗
y
=
k
x*y=k
x∗y=k最小。变换形式后有
y
=
k
x
y=\frac{k}{x}
y=xk。
也就是说要找一个最贴近坐标轴点,而这些点可能的点肯定是一个下凸包的形状。
接下来的过程可以分为3步:
- 求出与 x 轴和 y 轴最近的点
把 a i a_i ai作为边权,求出的最小生成树就是与 x 轴最近的点。
把 b i b_i bi作为边权,求出的最小生成树就是与 yy 轴最近的点。
为了下面讲解的方便,设与 xx 轴最近的点为 A ,与 yy 轴最近的点为 B 。
- 找到一个在 AB 的左下方且离 AB 最远的点
图大概是这样的
实际上我们要的C点就是在AB下方并使得
S
A
B
C
S_{ABC}
SABC最大的点,对于
S
A
B
C
S_{ABC}
SABC我们可以通过叉乘得到,公式如下:
S
A
B
C
=
A
B
⃗
∗
A
C
⃗
=
(
x
B
−
x
A
)
(
y
C
−
y
A
)
−
(
x
C
−
x
A
)
(
y
B
−
y
A
)
=
(
x
B
−
x
A
)
y
C
−
(
x
B
−
x
A
)
y
A
−
(
y
B
−
y
A
)
x
C
+
(
y
B
−
y
A
)
x
A
=
(
x
B
−
x
A
)
y
C
−
(
y
B
−
y
A
)
x
C
−
(
x
B
−
x
A
)
y
A
+
(
y
B
−
y
A
)
x
A
S_{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
SABC=AB∗AC=(xB−xA)(yC−yA)−(xC−xA)(yB−yA)=(xB−xA)yC−(xB−xA)yA−(yB−yA)xC+(yB−yA)xA=(xB−xA)yC−(yB−yA)xC−(xB−xA)yA+(yB−yA)xA
后面两项是一个常熟,关键是前面两项怎么算。
实际上,前面两项的最小值是将边权设为
(
x
B
−
x
A
)
b
i
−
(
y
B
−
y
A
)
a
i
(x_B−x_A)b_i-(y_B−y_A)a_i
(xB−xA)bi−(yB−yA)ai之后得到的最小生成树。
这样C就可以求出来了。
- 递归处理AC和CB
分别把AC和CB丢进去第二步做即可,终止条件为不存在点在当前边的下方。
即
A
B
⃗
×
A
C
⃗
>
=
0
{\vec{AB}}\times{ \vec{AC}}>=0
AB×AC>=0
- 因为决策点构成了一个含有 n ! n! n!个点的凸包。因为在 n n n个点的凸包上的点数期望是 ln n \sqrt {\ln n} lnn的,所以时间复杂度为 O ( n log n ln n ! ) \mathrm{O}(n\log n\sqrt {\ln n!}) O(nlognlnn!))。时间
代码:
#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;
}