斯坦那树

/*
斯坦那树:一个无向图,n个点m条边,有k个关键点,求联通这k个点的最小代价
由于最后的结果一定是一棵树,所以dp[i][s]表示以i为根,
当前连通状态为s的代价,注意每个点都可能为根. 
dp[i][s]可以由s的子状态转移过来,也可以由别的点j通过i,j这条边与i相连形成以i为根的s
即dp[i][s]=min(dp[i][j]+dp[i][s^j]) j为s的子集
dp[i][s]=min(dp[i][s],dp[j][s]+val[i][j]) 
那么这个式子本质上就是最短路了,对于每次的s,更新起点i了以后,都跑一次dij松弛其它点的s 
思考点:为什么连了i,j这条边,而j的状态还是s呢,新加入的i点难道不会对状态产生影响吗
如果s这个状态i已经存在了,那么说明dp[j][s]中已经有一条边连向i了,
这条边是多余的,并不会更新dp[i][s]
如果s这个状态i不存在,那么可能会更新到dp[i][s],这是i并没有表示连通,
但是dp[i][1<<(i-1)]的代价是0,它可以通过转移1来实现把i加入连通中 
复杂度为3^k*n+2^k*ElogE
*/ 
#include <iostream>
#include <vector>
#include <queue>
using namespace std;

typedef long long ll;
const int maxn = 105;

struct node{
	int id;
	ll v;
	node(int a,ll b)
	{
		id = a;
		v = b;
	}
	bool operator<(const node&n)const
	{
		return v > n.v;
	}
}; 

vector<node> g[maxn];
int a[maxn],vis[maxn]; 
ll dp[maxn][1<<10];

void dij(int begin,int s,int n)
{
	priority_queue<node> q;
	for (int i = 1; i <= n; i++) vis[i] = 0;
	q.push(node(begin,dp[begin][s]));
	while( !q.empty() )
	{
		int x = q.top().id;
		q.pop();
		vis[x] = 1;
		for (int i = 0; i < g[x].size(); i++)
		{
			int t = g[x][i].id;
			if( dp[t][s] > dp[x][s] + g[x][i].v )   //松弛相邻点对应的s 
			{
				dp[t][s] = dp[x][s] + g[x][i].v;
				q.push(node(t,dp[t][s]));
			}
		}
	}
}

int main()
{
	ios::sync_with_stdio(false);
	cin.tie(0);
	cout.tie(0);
	int n,m,k;
	cin >> n >> m >> k;
	for (int i = 1; i <= m; i++)
	{
		int x,y;
		ll v;
		cin >> x >> y >> v;
		g[x].push_back(node(y,v));
		g[y].push_back(node(x,v));
	} 
	for (int i = 1; i <= n; i++)   //初始化代价无穷大 
	{
		for (int j = 0; j < (1<<k); j++) dp[i][j] = 1e18;
	}
	for (int i = 1; i <= k; i++) 
	{
		cin >> a[i];
		dp[a[i]][1<<(i-1)] = 0;    //对于关键点,它存在对应的状态代价为0 
	}
	for (int s = 1; s < (1<<k); s++)  //枚举状态 
	{
		for (int i = 1; i <= n; i++)  //枚举根 
		{
			for (int sub = s&(s-1); sub; sub=s&(sub-1))  //子集枚举 
				dp[i][s] = min(dp[i][s],dp[i][sub]+dp[i][s^sub]);
			if( dp[i][s] != 1e18 ) dij(i,s,n);   //最短路更新 
		}
	}
	ll ans = 1e18;
	for (int i = 1; i <= k; i++) ans = min(ans,dp[a[i]][(1<<k)-1]);
	cout << ans << '\n';
	return 0;
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值