1. 程式人生 > 其它 >筆記-最小斯坦納樹

筆記-最小斯坦納樹

模擬賽遇到又爆零了。

Template&題意

正文

給定 \(G=(V,E)\) 和點集 \(S\),讓你求一個連通塊包含 \(S\) 內所有點且邊權最小。

首先最後的連通塊一定是一棵樹,證明顯然。

所以我們不妨欽定一個根,考慮dp。設 \(f[i,s]\) 代表以 \(i\) 為根,考慮 \(s\) 裡的點形成一個連通塊(雖然是連通塊,但是因為保證了最小所以是棵樹)。

通過兩種方法轉移:

  1. 假設根的度數為 \(1\),那麼可以從別的根轉移過來 \(f[i,s]=\min f[j,s]+w(j,i)]\)

  2. 考慮合併 \(i\) 的兩的部分,\(f[i,s]=\min f[i,s]+f[i,s-t]\)

對於第二種操作我們列舉子集,所以要求我們從小到大列舉。

對於第一種操作很像最短路的鬆弛操作,所以直接跑最短路。

時間複雜度 \(O(3^{\mid S\mid}\mid V\mid^2+2^{\mid S\mid}\mid V\mid\mid E\mid)\)

卡常

一些卡常技巧可以讓你跑 \(9e8\)

首先讓 dp 陣列訪問連續,也就是第一維是 \(s\),第二維是 \(i\),其次用 spfa 可以讓平均複雜度降低。

Code

#include<bits/stdc++.h>
#define pii std::pair<int,int>
namespace _name{
	const int maxn=200,mod=998244353,inf=0x3f3f3f3f;
	template<typename T>
	inline void read(T &x){
		T flag=1;
		char ch=getchar();
		for(;!isdigit(ch);ch=getchar())if(ch=='-')flag=-1;
		for(x=0;isdigit(ch);ch=getchar())x=x*10+ch-'0';
		x*=flag;
	}
	inline void cmax(int &a,int b){if(b>a)a=b;}
	inline void cmin(int &a,int b){if(b<a)a=b;}
}using namespace _name;
int n,m,k,f[1<<14][200];
std::vector<pii>vec[maxn];
std::queue<int>q;
bool vis[maxn];
void spfa(int s){
	for(int i=0;i<n;i++)if(f[s][i]!=inf)q.emplace(i),vis[i]=1;
	while(!q.empty()){
		int u=q.front();q.pop();vis[u]=0;
		for(auto [v,w]:vec[u]){
			if(f[s][u]+w<f[s][v]){
				f[s][v]=f[s][u]+w;
				if(!vis[v])q.emplace(v),vis[v]=1;
			}
		}
	}
}
int main(){
	read(n);read(m);read(k);
	for(int i=1,u,v,w;i<=m;i++){
		read(u);read(v);read(w);u--;v--;
		vec[u].emplace_back(v,w);vec[v].emplace_back(u,w);
	}
	memset(f,0x3f,sizeof(f));
	for(int i=0,x;i<k;i++){
		read(x);x--;
		f[1<<i][x]=0;
	}
	for(int s=1;s<(1<<k);s++){
		for(int t=s&(s-1);t;t=(t-1)&s){
			if(t>(s^t)){
				for(int i=0;i<n;i++)cmin(f[s][i],f[t][i]+f[s^t][i]);
			}
		}
		spfa(s);
	}
	int ans=inf;
	for(int i=0;i<n;i++)ans=std::min(ans,f[(1<<k)-1][i]);
	printf("%d\n",ans);
	return 0;
}

Reference

https://www.luogu.com.cn/blog/ix-35/solution-p6192

https://www.luogu.com.cn/blog/xyf007/solution-p6192