P4784 [BalticOI 2016 Day2] 城市 题解

题意

给定一张 n n n 个点 m m m 条边的无向图,边有正边权。指定 k k k 个点,求这 k k k 点两两可达的情况下选择的边边权和的最小值。

解法

边权非负,显然最终选择的边组成一棵树,否则去掉环上一条边后答案更优且不影响连通性。于是本题转化为最小斯坦纳树的模板题,纯粹的模板题在这里。特别地,MST 则可以认为是将所有 n n n 个点连通的最小斯坦纳树。

可以发现 k k k 很小,可以容得下带 2 k 2^k 2k 的时空复杂度。考虑使用状态压缩 dp,设 f i , j f_{i,j} fi,j 表示以 i i i 为根, k k k 个点的包含信息为 j j j j j j 二进制下第 x x x 位为 1 1 1 k k k 个点中第 x x x 个点被包含)时的答案。初始皆为 + ∞ +\infty +,对于 k k k 中的每个点 u u u f u , 2 u − 1 ← 0 f_{u,2^{u-1}}\gets0 fu,2u10,即连通自己不需要连其他边。

假设当前 k k k 个点中包含的点集为 S S S,对于第 i i i 个点,分两种情况转移:

  • 如果 i i i 的度数大于 1 1 1,考虑从当前状态的子集和补集进行转移,即对于一个被 S S S 包含的 T T T f i , S ← min ⁡ ( f i , S , f i , T + f i , S \ T ) f_{i,S}\gets\min(f_{i,S},f_{i,T}+f_{i,S\backslash T}) fi,Smin(fi,S,fi,T+fi,S\T)
  • 如果 i i i 的度数等于 1 1 1,考虑从与 i i i 相邻的点 j j j 转移, f i , S ← min ⁡ ( f i , S , f j , S + w ( i , j ) ) f_{i,S}\gets\min(f_{i,S},f_{j,S}+w(i,j)) fi,Smin(fi,S,fj,S+w(i,j)),其中 w ( i , j ) w(i,j) w(i,j) 表示 ( i , j ) (i,j) (i,j) 这条边的边权。

前者直接循环计算,后者可以用类似于最短路的松弛操作计算。

实现

对于第一种情况,因为我们按照二进制思想压缩状态,所以对于状态 S S S,其最大的子集 T T T S and ⁡ ( S − 1 ) S\operatorname{and}(S-1) Sand(S1),其中 and ⁡ \operatorname{and} and 表示按位与;在 T T T 之后的下一个 S S S 的子集即为 ( T − 1 ) and ⁡ S (T-1)\operatorname{and}S (T1)andS T T T S S S 中的补集即为 S xor ⁡ T S\operatorname{xor}T SxorT,其中 xor ⁡ \operatorname{xor} xor 表示按位异或。

对于每一个枚举的状态 S S S,如果在第一种情况的转移完成后, f u , S f_{u,S} fu,S 有值,则 u u u 就可以作为第二种情况的一个起点。每一轮都以这些有值的点为可能的起点跑最短路即可。

最后在 k k k 个点中随意挑一个点 u u u,答案即为 f u , 2 k − 1 f_{u,2^k-1} fu,2k1。时间复杂度 O ( 3 k n + m log ⁡ n × 2 k ) O(3^kn+m\log n\times2^k) O(3kn+mlogn×2k)

代码

#include<bits/stdc++.h>
#define mk make_pair
#define ll long long
using namespace std;
const int maxn = 1e5 + 5,maxk = 5;
int n,m,k,im[maxn];
ll f[maxn][(1 << maxk) + 5];
vector<pair<int,ll> > mp[maxn];
void addEdge(int u,int v,ll w) {
	mp[u].push_back(mk(v,w));
}
priority_queue<pair<ll,int> > q;
bool vis[maxn];
void Dijkstra(int now) {
	memset(vis,false,sizeof(vis));
	while (!q.empty()) {
		int u = q.top().second; q.pop();
		if (vis[u]) continue;
		vis[u] = true;
		for (auto V : mp[u]) {
			int v = V.first; ll w = V.second;
			if (f[v][now] > f[u][now] + w) {
				f[v][now] = f[u][now] + w;
				if (!vis[v]) 
					q.push(mk(-f[v][now],v));
			}
		}
	}
}
int main() {
	scanf("%d%d%d",&n,&k,&m); ll w;
	for (int i = 1;i <= n;i ++)
		for (int s = 0;s <= (1 << k) - 1;s ++)
			f[i][s] = 1e18;
	for (int i = 1;i <= k;i ++) {
		scanf("%d",&im[i]);
		f[im[i]][1 << (i - 1)] = 0;
	}
	for (int i = 1,u,v;i <= m;i ++) {
		scanf("%d%d%lld",&u,&v,&w);
		addEdge(u,v,w); addEdge(v,u,w);
	}
	for (int s = 0;s <= (1 << k) - 1;s ++) {
		for (int i = 1;i <= n;i ++) {
			for (int t = s & (s - 1);t;t = (t - 1) & s)
				f[i][s] = min(f[i][s],f[i][t] + f[i][s ^ t]);
			if (f[i][s] < 1e18) q.push(mk(-f[i][s],i));
		}
		Dijkstra(s);
	}
	printf("%lld",f[im[1]][(1 << k) - 1]);
	return 0;
}
  • 31
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值