参考自:https://www.cnblogs.com/ECJTUACM-873284962/p/7643445.html
斯坦纳树问题是组合优化学科中的一个问题。将指定点集合中的所有点连通,且边权总和最小的生成树称为最小斯坦纳树(Minimal Steiner Tree),其实最小生成树是最小斯坦纳树的一种特殊情况。而斯坦纳树可以理解为使得指定集合中的点连通的树,但不一定最小。
求解最小斯坦纳树是 N P NP NP问题,没有有效的多项式算法。这个部分个人感觉算是比较生僻的内容,适用范围很局限,大体上是最短路算法和状态压缩转移的结合。
由于题目中给出要求联通的 k k k点集一般比较小,考虑把每个点的联通情况设为状态。设 d p [ i ] [ s t a ] dp[i][sta] dp[i][sta] 为以 i i i 为根,联通状态为 s t a sta sta的最小开销。
考虑如下两种转移
通过子集 s s s进行转移
则有 d p [ i ] [ s t a ] = m i n ( d p [ i ] [ s ] , d p [ i ] [ s 1 ] + d p [ i ] [ s 2 ] ) dp[i][sta] = min(dp[i][s] ,dp[i][s1] + dp[i][s2]) dp[i][sta]=min(dp[i][s],dp[i][s1]+dp[i][s2]),其中 s 1 ⋂ s 2 = ∅ s1\bigcap s2 = \emptyset s1⋂s2=∅
枚举子集可以通过 s n x t = ( s c u r − 1 ) s_{nxt} = (s_{cur} - 1) snxt=(scur−1) & s t a sta sta 避免无用状态
通过类似最短路算法的操作进行松弛操作,一般采用 s p f a spfa spfa。也见过dijkstra 的写法
d p [ v ] [ s t a ] = m i n ( d p [ v ] [ s t a ] , d p [ u ] [ s t a ] + c o s t [ u ] [ v ] ) dp[v][sta] = min(dp[v][sta],dp[u][sta] + cost[u][v]) dp[v][sta]=min(dp[v][sta],dp[u][sta]+cost[u][v])
设联通点集大小为 k k k,总点集大小为 n n n,则算法总体复杂度为 O ( n 3 k + 2 k ∗ c ∗ E ) O(n3^k + 2^k * c*E) O(n3k+2k∗c∗E)。
后面一项为 s p f a spfa spfa 玄学的 复杂度,前一项则可由 ∑ ( k i ) ∗ 2 i = ( 1 + 2 ) k \sum \binom {k}{i}*2^i = (1+2)^k ∑(ik)∗2i=(1+2)k 推得。
bzoj 2595: [Wc2008]游览计划
sol:经典斯坦纳树题目
zz题面看了老半天。题目要求还原路径,需要记录转移过程。这道题中比较特殊的是给的是点权,所以枚举子集的时候需要减掉根的点权,否则根的权值会被统计两次。
#include
using namespace std;
typedef long long ll;
const int maxn = 1e5+10;
const int inf = 1e9;
#define MP make_pair
#define fi first
#define se second
#define pii pair
int dp[11][11][1<<11];
int a[11][11],vis[11][11];
int tot,n,m;
vector<pii> run;
queue<pii> q;
void init(){
run.resize(4);
run[0] = MP(-1,0); run[1] = MP(1,0);
run[2] = MP(0,-1); run[3] = MP(0,1);
memset(dp,0x3f,sizeof(dp));
tot = 0;
}
struct PRE{
int x,y,S;
}pre[11][11][1<<11];
void SPFA(int cur){
while(!q.empty()){
pii p = q.front(); q.pop();
vis[p.fi][p.se] = 0;
// cout<<"p = "<
for(int i = 0;i<4;i++){
int wx = p.fi + run[i].fi,wy = p.se + run[i].se;
if(wx < 1 || wx > n || wy < 1 || wy > m) continue;
// cout<<"wx = "<if (dp[wx][wy][cur] > dp[p.fi][p.se][cur] + a[wx][wy]){
// cout<<"???????????????????"<
dp[wx][wy][cur] = dp[p.fi][p.se][cur] + a[wx][wy];
pre[wx][wy][cur] = (PRE){p.fi,p.se,cur};
if(!vis[wx][wy])
vis[wx][wy] = 1,q.push(MP(wx,wy));
}
}
}
}
void dfs(int x,int y,int cur){
// cout<
vis[x][y] = 1;
PRE ret = pre[x][y][cur];
if(ret.x == 0 && ret.y == 0) return ;
dfs(ret.x,ret.y,ret.S);
if(ret.x == x && ret.y == y) dfs(ret.x,ret.y,cur - ret.S);
}
int main(){
init();
scanf("%d%d",&n,&m);
for(int i = 1;i<=n;i++)
for(int j = 1;j<=m;j++) {
scanf("%d",&a[i][j]);
if(!a[i][j]) dp[i][j][1<<tot] = 0,tot++;
}
int S = (1 << tot ) - 1;
// cout<<"tot = "<for (int sta = 0 ;sta<= S;sta++){
for(int i = 1;i<=n;i++)
for(int j = 1;j<=m;j++){
for(int s = sta;s;s = (s-1) & sta){
if(dp[i][j][s] + dp[i][j][sta - s] - a[i][j] < dp[i][j][sta])
dp[i][j][sta] = dp[i][j][s] + dp[i][j][sta - s] - a[i][j],
pre[i][j][sta] = (PRE){i,j,s};
}
if(dp[i][j][sta]<inf){
// cout<
q.push(MP(i,j)),vis[i][j] = 1;
}
}
SPFA(sta);
}
int x,y,flag = 0;
for(int i = 1;i<=n;i++)
for(int j = 1;j<=m;j++)
if(!a[i][j]){
x = i,y = j;
flag = 1;
break;
}
// cout<
printf("%d\n",dp[x][y][S]);
memset(vis,0,sizeof(vis));
dfs(x,y,S);
for(int i = 1;i<=n;i++){
for(int j = 1;j<=m;j++){
// cout<<"i j a[i][j] vis[i][j] "<
if(a[i][j] == 0) putchar('x');
else if(vis[i][j] == 1) putchar('o');
else putchar('_');
}
puts("");
}
return 0;
}
hdu 4085 ( Peach Blossom Spring
sol: 11年北京网络赛,给出n个点,要求前k个点和后k个点一一对应且联通。按斯坦纳树的搞法搞一通后会发现答案可能是个森林,考虑用状压dp处理。题目要求k对点一一对应,则一个合法的联通子集中前k个点和后k个点的数目应该相等。用和上文类似的做法枚举合法的子集的进行转移。
将spfa入队操作写在枚举子集里面,然后T了个爽。
#include
using namespace std;
typedef long long ll;
const int maxn = 55;
const int inf = 0x3f3f3f3f;
#define MP make_pair
#define fi first
#define se second
#define pdd pair
int f[maxn][1<<12];
int n,k;
int vis[maxn];
int dp[1<<12];
queue<int> Q;
struct edge{
int v,w;
edge(int _v,int _w):v(_v),w(_w){}
};
vector<edge> G[maxn];
void SPFA(int cur){
while(!Q.empty()){
int u = Q.front(); Q.pop();
for(int i = 0;i<G[u].size();i++){
int v = G[u][i].v;
int w = G[u][i].w;
if(f[v][cur] > f[u][cur] + w){
f[v][cur] = f[u][cur] + w;
if(!vis[v]){
vis[v] = 1;
Q.push(v);
}
}
}
vis[u] = 0;
}
}
bool check(int sta){
int cnt = 0;
for(int i = 0;i<k;i++) {
if(sta & (1<<i)) cnt++;
if(sta & (1<<(k+i))) cnt--;
}
return cnt == 0;
}
int main(){
int T;
scanf("%d",&T);
while(T--){
int m;
scanf("%d%d%d",&n,&m,&k);
for(int i = 0;i<=n;i++) G[i].clear();
int S = (1<<(k*2)) - 1;
for(int i = 0;i<=n;i++)
for(int j = 0;j<=S;j++) f[i][j] = inf;
while(m--){
int u,v,w;
scanf("%d%d%d",&u,&v,&w);
G[u].push_back(edge{v,w});
G[v].push_back(edge{u,w});
}
for(int i = 1;i<=k;i++){
f[i][1<<(i-1)] = 0;
f[n - k + i][1<<(k + i -1)] = 0;
}
memset(vis,0,sizeof(vis));
for(int sta = 0;sta <= S;sta++){
for(int i = 1;i<=n;i++){
for(int s = sta;s;s = (s-1) & sta){
f[i][sta] = min(f[i][sta],f[i][s] + f[i][sta^s]);
}
if(f[i][sta] < inf){
vis[i] = 1;
Q.push(i);
}
}
SPFA(sta);
}
for(int i = 0;i<=S;i++){
dp[i] = inf;
for(int j = 1;j<=n;j++) dp[i] = min(dp[i],f[j][i]);
}
for(int sta = 0;sta<=S;sta++){
if(!check(sta)) continue;
for(int s = sta;s;s = (s-1) & sta){
if(!check(s)) continue;
dp[sta] = min(dp[sta],dp[s] + dp[sta ^ s]);
}
}
if(dp[S] == inf) puts("No solution");
else printf("%d\n",dp[S]);
}
return 0;
}
Gym - 101908J
sol: 巴西区域赛?
限制变成了这k个点只能连出去一条边,也就是说这k个点在树上只能是叶子。为了保证合法性,我们只更新根节点不是这k个点的斯坦纳树。对于这k个点,只允许单个点的联通块更新其他斯坦纳树,保证连出去的边只有一条。有种特殊情况是 k = 2 k=2 k=2的时候,直接连起来就是最优解,但数据规定 k > = 3 k>=3 k>=3规避了种情况,就不用特判了。
#include
using namespace std;
typedef long long ll;
const int maxn = 105;
const double inf = 1e18;
#define MP make_pair
#define fi first
#define se second
#define pdd pair
double dp[maxn][1<<11];
int tot,n,k;
int vis[maxn];
double len[maxn][maxn];
pdd p[maxn];
queue<int> Q;
inline double dis(pdd a,pdd b){
return sqrt((a.fi - b.fi) * (a.fi - b.fi) + (a.se - b.se) * (a.se - b.se));
}
void SPFA(int cur){
while(!Q.empty()){
int u = Q.front(); Q.pop();
for(int v = 1;v<=n;v++){
if(v == u || v <= k) continue;
if(dp[v][cur] > dp[u][cur] + len[u][v]){
dp[v][cur] = dp[u][cur] + len[u][v];
if(!vis[v]){
vis[v] = 1;
Q.push(v);
}
}
}
vis[u] = 0;
}
}
int main(){
scanf("%d%d",&n,&k);
int S = (1<<k) - 1;
for(int i = 1;i<=n;i++)
for(int s = 0;s <= S; s++) dp[i][s] = inf;
for(int i = 1;i<=k;i++){
scanf("%lf%lf",&p[i].fi,&p[i].se);
dp[i][1<<(i-1)] = 0;
}
for(int i = k+1;i<=n;i++){
scanf("%lf%lf",&p[i].fi,&p[i].se);
}
for(int i = 1;i<=n;i++)
for(int j = 1;j<=n;j++) {
len[i][j] = dis(p[i],p[j]);
}
for(int sta = 0;sta <=S;sta++){
for(int i = k+1;i<=n;i++){
for(int s = sta;s;s = (s-1) & sta){
dp[i][sta] = min(dp[i][sta],dp[i][s] + dp[i][sta^s]);
}
}
for(int i = 1;i<=n;i++){
if(dp[i][sta]<inf - 1) Q.push(i),vis[i] = 1;
}
SPFA(sta);
}
double ans = inf;
for(int i = k+1;i<=n;i++) ans = min(ans,dp[i][S]);
printf("%.5f\n",ans);
return 0;
}