斯坦纳树简介
斯坦纳树问题是组合优化问题,与最小生成树相似,是最短网络的一种。最小生成树是在给定的点集和边中寻求最短网络使所有点连通。而最小斯坦纳树允许在给定点外增加额外的点,使生成的最短网络开销最小。-------- 百度百科
斯坦纳树问题的最初形式是这样的:给定平面上的一系列点 P 1 , P 2 , P 3 , . . . , P n P_1,P_2,P_3,...,P_n P1,P2,P3,...,Pn,现在要想办法用边将这 n n n 个点连通。允许在平面上添加一些新的点(这些点称为“斯坦纳点”)作为辅助,添加的边的端点可以是原本存在的点,也可以是斯坦纳点,要求这个网络的总长度最小。
例如,当
n
=
3
n=3
n=3 时,记三个点为
A
,
B
,
C
A,\ B,\ C
A, B, C, 有如下结论:如果三角形
A
B
C
ABC
ABC 的每个内角都小于
120
°
120\degree
120°,那么增加一个斯坦纳点
S
S
S,当
S
A
,
S
B
,
S
C
SA\ ,SB,\ SC
SA ,SB, SC 的夹角都为
120
°
120\degree
120° 时,
S
A
+
S
B
+
S
C
SA+SB+SC
SA+SB+SC 最小。如果三角形
A
B
C
ABC
ABC 有一个角大于等于
120
°
120\degree
120°,那么斯坦纳点与大于
120
°
120\degree
120° 度的角的顶点重合。
斯坦纳树问题的定义随着历史的发展不断地推广,后来出现了带点权的斯坦纳树问题。但算法竞赛中似乎没有见到过这种几何意义上的斯坦纳树问题,而是斯坦纳树的思想在图论中的一种体现。
图论中的斯坦纳树问题
图论中的斯坦纳树问题失去了原本的几何意义,实在离散的图上定义的。
给定 N N N 个结点和 M M M 条边的连通图,选取 K K K 个关键点,需要保留一些边使得 K K K 个关键点连通。要求边权和最小。这 K K K 个点可能不会直接连通,所以需要利用剩下 N − K N-K N−K 个非关键点,这些点就是斯坦纳点。
求解斯坦纳树的方法是,状态压缩动态规划。
状态的定义是 f ( i , S ) f(i, S) f(i,S), i i i 表示以 i i i 结点为根。 S S S 是一个整数表示的状态,表示的是 K K K 个关键点的连通信息。也就是 f ( i , S ) f(i, S) f(i,S) 表示以 i i i 为根,包含集合 S S S 中所有点的最小边权值和。
状态转移需要经历两个过程,分别是子集转移和当前状态下的边松弛操作。
- f ( i , S ) = m i n { f ( i , S ) , f ( i , T ) + f ( i , S − T ) } f(i, S)=min\{f(i,S)\ ,\ f(i,T)+f(i,\ S-T)\} f(i,S)=min{f(i,S) , f(i,T)+f(i, S−T)}
其中 T T T 是 S S S 的一个子集, S − T S-T S−T 表示 T T T 的补集。子集转移需要枚举状态 S S S 的所有子集,枚举子集可以用如下方法:
for(int sub=S&(S-1);sub;sub=S&(sub-1))
{
//sub 就是从大到小枚举的 S的子集
//S^sub 就是sub的补集
}
可以证明这样做的正确性:首先显然
S
&
(
s
u
b
−
1
)
S\&(sub-1)
S&(sub−1) 一定是
S
S
S 的子集,所以只需证明
s
u
b
sub
sub 和
(
s
u
b
−
1
)
&
S
(sub-1) \& S
(sub−1)&S 之间没有
S
S
S 的子集。
减一相当于将
s
u
b
sub
sub 最右边的 1 变成 0,同时将这个 1 左边的 0 都变成 1。所以
s
u
b
&
(
s
u
b
−
1
)
sub\&(sub-1)
sub&(sub−1) 一定是小于
s
u
b
sub
sub 的最大的
S
S
S 的子集。
- f ( i , S ) = m i n { f ( i , S ) , f ( j , S ) + w ( i , j ) } f(i, S)=min\{f(i,S)\ \ ,\ \ f(j,S)+w(i,j)\} f(i,S)=min{f(i,S) , f(j,S)+w(i,j)}
可以发现这部分的转移是在同一个状态 S S S 下进行的,参与转移的只有边权,实际上就转化为了最短路问题,可以直接用 D i j k s t r a Dijkstra Dijkstra 或 S P F A SPFA SPFA 来转移。
算法核心代码如下:
memset(dp, INF, sizeof(dp));
for(int i=1;i<=K;i++)
{
scanf("%d", &key[i]); //第i个关键点
dp[key[i]][1<<(i-1)] = 0;
}
for(int S=1;S<(1<<K);S++) //枚举K个关键点的所有状态
{
for(int i=1;i<=N;i++) //枚举根节点
{
for(int sub=S&(S-1);sub;sub=S&(sub-1)) //枚举 S 的所有子集
dp[i][S] = min(dp[i][S], dp[i][sub]+dp[i][S^sub]);
if(dp[i][S]!=INF)
q.push((t_Node){dp[i][S], i}); //最短路的优先队列
}
Dijkstra(S); //对状态 S 进行状态内转移
}
实际问题中 K K K 不会太大,我猜这个问题可能是 N P NP NP 的,没有关于 K K K 的多项式时间算法。
斯坦纳森林
有时候问题并不要求 K K K 个关键点全部连通,而是满足某些要求的点连通即可,也就是说连通后的图为多棵斯坦纳树组成的斯坦纳森林。
求解斯坦纳森林首先要求出正常的斯坦纳树的结果,然后对斯坦纳树再进行一次状态压缩。
令 f ( S ) f(S) f(S) 表示关键点连通状态为 S S S 的最小边权和。枚举 S S S 的子集进行转移,但前提是 S S S 需要满足题目所给的要求。
memset(f, INF, sizeof(f));
for(int S=1;S<1<<K;S++)
{
if(check(S)) //检查状态S是否满足要求
{
for(int i=1;i<=N;i++)
f[S] = min(f[S], dp[i][S]);
}
}
for(int S=1;S<S<1<<K);S++)
{
for(int sub=S&(S-1);sub;sub=S&(sub-1))
{
if(check(sub)&&check(S^sub))
f[S] = min(f[S], f[sub]+f[S^sub]);
}
}
printf("%d\n", f[(1<<K)-1]);
例题
模板:
洛谷P6192 【模板】最小斯坦纳树.
思维题+斯坦纳树:
2018-2019 ACM-ICPC Brazil Subregional Programming Contest J - Joining Capitals.
点权斯坦纳树,带路径输出:
洛谷P4294 [WC2008]游览计划.
斯坦纳森林:
UVA1496 Peach Blossom Spring.