【模板】斯坦纳树
模拟赛出了一个斯坦纳森林的题,全军覆没。
于是赶紧过来学习。
\(1.\)概述
先来看这道题:一张无向图,求它的最小生成树。
不会有人不会吧,Kruskal或者Prim一下就好了。
然后是它的进阶版:
一张无向图 \(G\),\(|V| \leq 100,|E| \leq 500\),给定一个大小为 \(k(k \leq 10)\) 集合 \(S\),求一个子图 \(G'(V',E')\),使得 \(S⊆V',G'\) 连通,且最小化 \(E'\) 权值和。
可以发现最小生成树就是 \(S=V\) 时的特殊情况。那么这个东西怎么做呢?
\(2.\)解法
首先,\(G'\) 一定是一颗树。
要不然如果有环的话随便断掉一条边还是符合的。
于是我们钦定一个树根,设 \(dp[i][mask]\) 表示以 \(i\) 为根的一棵树,包含了 \(mask\) 中所有点的最小权值(\(mask\) 是状压 \(S\) 中的点,所以 \(i\)可能不在 \(mask\) 中)。
然后转移的时候分两种情况:
- 树中 \(i\) 的度不为 1。
那么一定可以将 \(mask\) 分为两个子集 \(s,mask-s\),使得它们代表的集合分别在 \(i\) 的两个不同的子树中。
但我们不知道 \(s\) 是什么,于是我们考虑暴力,用 \(O(3^k)\) 的复杂度枚举子集,暴力转移。
所以有
- 树中\(i\) 的度为 1。
那么这时与 \(i\) 相邻的那个点 \(j\) 的答案加上 \(dis(i,j)\) 一定比 \(i\) 优。
但是我们仍然不知道 \(j\) 是那个点,也不知道哪些 \(i\) 度为 1,于是我们继续考虑暴力,用 \(O(n^2)\) 的复杂度枚举两个点,暴力转移。
所以又有
可以证明,只有 \(i,j\) 相邻时 \(dp[i][mask]\) 会改变。
\(upd:\) 好像有点问题。建议不要管这个暴力,直接看下面最短路的方法 😃
于是做完了。
注意事项:
\(dis[i][j]\) 的求法可以 \(O(n^3)Floyd\)。
总复杂度 \(O(n^3+n^2 \times 2^k + n \times 3^k)\)
但是我们还有一种做法。
有些恶心的出题壬会把 \(n\) 开到 1000。这时怎么办呢?
观察第二种情况的式子:
既然要优化,我们就限制一下 \(j\) 的范围:
然后我们发现:
这和最短路的松弛操作长的一模一样。
于是我们就可以愉快的用 dijkstra 或 SPFA(只有正权)优化了。
总复杂度 \(O(nlogn \times 2^k + n \times 3^k)\)
例题:
就是板子。
\(n\) 只有 100,用Floyd
\(Code:\)
// Problem: P6192 【模板】最小斯坦纳树
// Contest: Luogu
// URL: https://www.luogu.com.cn/problem/P6192
// Memory Limit: 250 MB
// Time Limit: 1000 ms
#include<bits/stdc++.h>
using namespace std;
#define ll long long
#define f(i,a,b) for(ll i=a;i<=b;i++)
#define wt int tt=d;while(tt--)
#define py puts("Yes")
#define pn puts("No")
#define fe(i,e) for(int i=0;i<e.size();i++)
#define vi vector<ll>
inline ll rd() {
	ll x=0,f=1;
	char c=getchar();
	while(!isdigit(c)){if(c=='-')f=-1;c=getchar();}
	while(isdigit(c))x=x*10+c-'0',c=getchar();
	return x*f;
}
#define d rd()
#define pb push_back
const ll N=300010;
ll dis[110][110];
ll qp(ll a,ll b,ll p){
	ll ans=1;while(b){
		if(b&1)ans=ans*a%p;
		a=a*a%p;b>>=1;
	}return ans;
}ll n,m,k;
ll s,dp[110][2010];
int main(){
	memset(dis,0x3f,sizeof(dis));
	n=d,m=d,k=d;
	f(i,1,m){
		ll u=d,v=d,w=d;
		dis[u][v]=min(dis[u][v],w);
		dis[v][u]=min(dis[v][u],w);
	}f(kk,1,n)f(i,1,n)f(j,1,n)dis[i][j]=min(dis[i][j],dis[i][kk]+dis[kk][j]);
	memset(dp,0x3f,sizeof(dp));
	f(i,1,k)s=d,dp[s][1<<(i-1)]=0;
	f(msk,0,(1<<k)-1){
		f(i,1,n)for(int ms=msk;ms;ms=(ms-1)&msk)dp[i][msk]=min(dp[i][msk],dp[i][ms]+dp[i][msk^ms]);
		f(i,1,n)f(j,1,n)dp[i][msk]=min(dp[i][msk],dp[j][msk]+dis[i][j]);
	}printf("%lld\n",dp[s][(1<<k)-1]);
	
	return 0;
}
题意:一张图,给定 4 对关键点(一共 8 个点),求最小斯坦纳森林。
发现按照上面的那样做不行,因为这个不用保证所有点连通。
因为所有的点都是按照顺序读进来的,所以只有第一对点在 \(mask\) 中连通表示为 \((00000011)_2\),只有第二对点表示为 \((00001100)_2\),第一对和第二对同时连通为 \((00001111)_2\)。
所以只有 \(mask[0]\)(表示 \(mask\) 的第 0 位)\(=mask[1],mask[2]=mask[3].mask[4]=mask[5],mask[6]=mask[7]\) 时这个 \(mask\) 合法。
然后就可以写一个 check 来判断是否合法。
我们再设一个数组 \(f[mask]\),表示合法的 \(mask\) 状态的点对分别连通时的最小权值。
所以一开始 \(f[mask]=\min_{i=1}^n dp[i][mask]\)。
假设 \(mask=(11001111)_2\),所以
于是可以发现
于是又是一个枚举子集。
然后好了。
\(Code:\)
// Problem: Ticket to Ride
// Contest: POJ - Northwestern Europe 2006
// URL: http://poj.org/problem?id=3123
// Memory Limit: 65 MB
// Time Limit: 2000 ms
#include<cstring>
#include <algorithm>
#include <bitset>
#include <complex>
#include <deque>
#include <exception>
#include <fstream>
#include <functional>
#include <iomanip>
#include <ios>
#include <iosfwd>
#include <iostream>
#include <istream>
#include <iterator>
#include <limits>
#include <list>
#include <locale>
#include <map>
#include <memory>
#include <new>
#include <numeric>
#include <ostream>
#include <queue>
#include <set>
#include <sstream>
#include <stack>
#include <stdexcept>
#include <streambuf>
#include <string>
#include <typeinfo>
#include <utility>
#include <valarray>
#include <vector>
using namespace std;
#define ll long long
#define f(i,a,b) for(ll i=a;i<=b;i++)
#define wt int tt=d;while(tt--)
#define py puts("Yes")
#define pn puts("No")
#define fe(i,e) for(int i=0;i<e.size();i++)
#define vi vector<ll>
inline ll rd() {
	ll x=0,f=1;
	char c=getchar();
	while(!isdigit(c)){if(c=='-')f=-1;c=getchar();}
	while(isdigit(c))x=x*10+c-'0',c=getchar();
	return x*f;
}
#define d rd()
#define pb push_back
const ll N=300010;
struct edge{ll v,w,nx;}e[N<<1];
ll hd[N],cnt;
void add(ll u,ll v,ll w){e[++cnt]=(edge){v,w,hd[u]};hd[u]=cnt;}
ll qp(ll a,ll b,ll p){
	ll ans=1;while(b){
		if(b&1)ans=ans*a%p;
		a=a*a%p;b>>=1;
	}return ans;
}ll n,m,k;
string s,t;
ll dis[50][50],ans;
map<string,ll>mp;
ll dp[50][500];
ll dpp[500];
bool ch(ll x){
	f(i,1,4){
		ll p=x-(x>>2<<2);
		if(p==3||p==0);
		else return 0;
		x>>=2;
	}return 1;
}
int main(){
	while(1){
		n=d,m=d,k=8;if(n+m==0)break;mp.clear();
		f(i,1,n)f(j,1,n)dis[i][j]=9999999;
		f(i,1,n)cin>>s,mp[s]=i;
		f(i,1,m){
			cin>>s>>t;
			ll u=mp[s],v=mp[t],w=d;
			dis[u][v]=min(dis[u][v],w);
			dis[v][u]=min(dis[v][u],w);
		}f(kk,1,n)f(i,1,n)f(j,1,n)dis[i][j]=min(dis[i][j],dis[i][kk]+dis[kk][j]);
		memset(dp,0x3f,sizeof(dp));
		f(i,1,k){cin>>s;
			dp[mp[s]][1<<(i-1)]=0;
		}memset(dpp,0x3f,sizeof(dpp));
		f(msk,0,(1<<k)-1){
			f(i,1,n)for(int ms=msk;ms;ms=(ms-1)&msk)dp[i][msk]=min(dp[i][msk],dp[i][ms]+dp[i][msk^ms]);
			f(i,1,n)f(j,1,n)dp[i][msk]=min(dp[i][msk],dp[j][msk]+dis[i][j]);
		}
		f(i,0,(1<<k)-1)f(j,1,n)dpp[i]=min(dpp[i],dp[j][i]);
		f(i,0,(1<<k)-1)if(ch(i))
			for(int j=i;j;j=(j-1)&i)if(ch(j))dpp[i]=min(dpp[i],dpp[j]+dpp[i^j]);
		printf("%lld\n",dpp[(1<<k)-1]);
	}
	
	return 0;
}
都是板子,加个输出方案就行了。

 
                
            
         
         浙公网安备 33010602011771号
浙公网安备 33010602011771号