斯坦纳树
为什么我会写这个东西的博客呢?
某天我无意在洛谷里面找到了一道状压的题目,JLOI省选的然后当时兴冲冲开始做。想了3天3夜还是没有想出来,最后决定去搜题解……然后看到题解的时候——震惊!斯坦纳树是什么东西???于是去学习了斯坦纳树……
其实斯坦纳树很好理解,它像线段树一样用于解决一类问题。
想必一定听说过最小生成树,斯坦纳树和最小生成树很类似:在一个拥有N个点的图中,有M条边,每条边有各自的边权;指定某一些点,目的是建立最小的边使得这些边的边权和最小并且能够让指定点中的所有点连通。
注意,和最小生成树不同,斯坦纳树并不是要是图中所有点连通。
怎么办呢,如果单独的去spfa的话,我们只能够求出某一个点到另一个点的距离,但是我们并不能保证所有的点都被来连通了,怎么保证指定点的点集都连通呢?
我们发现最后的最优解一定是树,它一定是由若干个子树拼凑而成的,所以可以定义 dp[ S ][ i ] 表示以第 i 个点为根节点,并且指定点集 S0 (二进制状态)中有 S 状态的点连通。
注意:i 节点可以以任何点为根,S0 为指定节点的集合所有都被连通的状态(1111……11),而 S 是枚举的连通集合。
我们如何保证dp[ S ][ i ]中边的权值最小呢?
还是同样的原理:“最后的最优解一定是树,它一定是由若干个子树拼凑而成的”,那么当前状态 指定的点中连通子集 S 并且以 i 为根的最优边权和一定是由以 i 为根的两颗子树的合并,并且这两颗子树的所覆盖的指定点一定不会重合!(想一想,为什么)
因为这样的解一定不是最优的(你花了额外的边去连接重复的点)!
因此枚举子集 s,并且求出子集对于的 S 的补集 S^s ,min (dp [ s ][ i ] + dp [ S ^ s ] [ i ],dp [ S ] [ i ])更新 dp [ S ][ i ];
得到 <转移方程1>:
dp [ S ][ i ] = min(dp [ s ][ i ] + dp [ S ^ s ] [ i ],dp [ S ] [ i ]);
光这样不够,由于获得了当前以 i 为根的最优解,那么有可能会影响到与 i 相邻的所有点,甚至于所有点N对于只覆盖状态为S的点子集的最优解,也就是说:dp [ S ] [ i ] 比 dp [ S ] [ j ] + dis [ i ][ j ] 更优(j 属于 N,dis表示 i 和 j 的间接距离)的时候,我们可以用 dp [ S ] [ i ] 更新 dp [ S ] [ j ];
得到 <转移方程2>:
dp [ S ] [ v ] = min(dp [ S ] [ frt ] + dis [ i ],dp [ S ] [ v ]); (frt 为 spfa 的队首,v 与 frt 有边连接)
注意,对于单独的覆盖集合 S 的 i 只能更新覆盖集合为 S 的 j;不能更新其他集合的点;
代码:
#include <bits/stdc++.h>
std :: queue < int > q ;
const int N = 10000 + 5 , N_ = 10 + 5 , SS = ( 1 << N_ ) , inf = 1e8 + 7 ;
int head [ N ] , to [ N ] , dis [ N ] , nxt [ N ] , cn ;
int dp [ N ] [ SS ] , n , m , p , S0 , cnt , x , y , w ;
bool vis [ N ] ;
int minx ( int a , int b ) {
return a < b ? a : b ;
}
void create ( int u , int v , int d ) {
cn ++ ;
dis [ cn ] = d ;
to [ cn ] = v ;
nxt [ cn ] = head [ u ] ;
head [ u ] = cn ;
}
void spfa ( int S ) {
memset ( vis , false , sizeof ( vis ) ) ;
while ( ! q . empty ( ) ) {
int frt = q . front ( ) ; q . pop ( ) ;
vis [ frt ] = false ;
for ( int i = head [ frt ] ; i ; i = nxt [ i ] ) {
int v = to [ i ] ;
if ( dp [ S ] [ v ] > dp [ S ] [ frt ] + dis [ i ] ){
dp [ S ] [ v ] = dp [ S ] [ frt ] + dis [ i ] ;
if ( ! vis [ v ] ) {
q . push ( v ) ;
vis [ v ] = true ;
}
}
}
}
}
int main ( ) {
//斯坦纳树 Code ;
freopen ( "in.in" , "r" , stdin ) ;
scanf ( "%d%d%d" , & n , & m , & cnt ) ;
//有 cnt 个点需要连通,并且这 cnt 个点的编号构成一个 1 ~ cnt 的序列 ;
S0 = ( 1 << cnt ) - 1 ;
for ( int i = 1 ; i <= m ; i ++ ) {
scanf ( "%d%d%d" , & x , & y , & w ) ;
create ( x , y , w ) ;
create ( y , x , w ) ;
}
for ( int i = 1 ; i <= n ; i ++ )
for ( int s = 0 ; s <= S0 ; s ++ )
dp [ s ] [ i ] = inf ;
for ( int i = 1 ; i <= cnt ; i ++ )
dp [ 1 << i - 1 ] [ i ] = 0 ;
for ( int S = 0 ; S <= S0 ; S ++ ) {
for ( int s = ( S - 1 ) & S ; s ; s = ( s - 1 ) & S ) //s 是 S 的子集 ;
for ( int j = 1 ; j <= n ; j ++ )
if ( dp [ S ] [ j ] > dp [ s ] [ j ] + dp [ S ^ s ] [ j ] )
dp [ S ] [ j ] = dp [ s ] [ j ] + dp [ S ^ s ] [ j ] ;
for ( int j = 1 ; j <= n ; j ++ )
if ( dp [ S ] [ j ] < inf && ! vis [ j ] )
q . push ( j ) , vis [ j ] = true ;
spfa ( S ) ;
}
int ans = inf ;
for ( int i = 1 ; i <= n ; i ++ )
ans = minx ( ans , dp [ S0 ] [ i ] ) ;
printf ( "%d" , ans ) ;
return 0 ;
}
这份代码不适用于任何斯坦纳树的模板题目!!!
仅供理解!!!