noip2017 宝藏(状压dp

本文介绍了一种求解最小生成树的动态规划算法,通过优化生成树深度,利用子集枚举和贪心策略,实现了高效求解。算法复杂度为O(n3n),适用于大规模图的最小生成树问题。

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

分析

在变化的量是啥?已经打的点的集合,还有当前生成树的深度。
于是我们用 f[s][i]f[s][i]f[s][i] 表示已打的集合为 sss , 生成树深度为 iii 的最小代价。
转移的话,答案肯定是由 SSS 的子集转移而来,我们就枚举子集,剩下的点作为第 iii 层,以此得到转移方程:
f[S][i]=min(f[S0][i−1]+i∗v[S0][Sf[S][i] = min(f[S0][i-1] + i * v[S0][Sf[S][i]=min(f[S0][i1]+iv[S0][S^S0])S0])S0]) (S0S0S0SSS 的子集,v[S1][S2]v[S1][S2]v[S1][S2] 表示 将 S2S2S2 的点加入生成树 S1S1S1 的最小距离)
复杂度是O(n3n)O(n3^n)O(n3n)

证明

让我们思考一下为什么这样转移是对的吧。
首先,肯定要有点作为第 iii 层,我们就相当于枚举这些点,于是 f[S0][i−1]f[S0][i-1]f[S0][i1] 就好理解了。
然后,贪心的想,对于两个集合要连通,我们选择的边肯定都是小的边。
需要理解的是,为什么代价是 i∗v[S0][Si * v[S0][Siv[S0][S^S0]S0]S0] 呢?有疑惑的点在于为什么乘 iii。下面我们证明最优解一定会被覆盖到。
这其实考验对子集的理解。假如我们在转移的时候用的边不应该在第 iii 层,这条边连接的是 (a,b)(a, b)(a,b),其中 a∈S0,b∈Sa\in S0, b\in SaS0,bS^S0S0S0,那么我们在枚举到S+S +S+{bbb}的时候,这条边就不计入转移了,就不存在该问题了。也就是说,我们转移的时候得到的临时答案,都是大于等于最终答案的,不存在比答案小的情况,也不会漏解。
不知道讲的清不清楚,有问题可以与我交流哈!!

upd

有一些状态是错误的,有一些状态是正确的,但是正确的状态比错误的状态优,因此最终答案肯定正确的。

代码如下

//f[i][s] = max(f[i-1][s0] + i * v[s0][s^s0])
#include <bits/stdc++.h>
#define inf 2147483647
using namespace std;
int f[12][1 << 12], v[1 << 12][1 << 12], g[1 << 12][12], mp[12][12], ans = inf;
int main(){
	int i, j, k, n, m, a, b, c, s, s0, sum;
	memset(f, 1, sizeof(f));
	memset(v, 1, sizeof(v));
	memset(g, 1, sizeof(g));
	memset(mp, 1, sizeof(mp));
	scanf("%d%d", &n, &m);
	for(i = 1; i <= m; i++){
		scanf("%d%d%d", &a, &b, &c);
		a--, b--;
		if(mp[a][b] > c) mp[a][b] = mp[b][a] = c;
	}
	/*for(i = 0; i < n; i++)
		for(j = 0; j < n; j++)
			if(mp[i][j]) v[1 << i][1 << j] = mp[i][j];*/
	for(i = 0; i < (1 << n); i++){
		for(j = 0; j < n; j++){
			if(1 << j & i) continue;
			for(k = 0; k < n; k++) if(1 << k & i) g[i][j] = min(g[i][j], mp[j][k]);
			//printf("===%d %d %d\n", i, j, g[i][j]);
		}
	}
	for(i = 0; i < (1 << n); i++){
		s = (1 << n) - 1 - i;
		for(j = s; j; j = (j - 1) & s){
			sum = 0;
			for(k = 0; k < n; k++) if(1 << k & j) sum += g[i][k];
			v[i][j] = min(v[i][j], sum);
			//printf("===============%d %d %d\n", i, j, v[i][j]);
		}
	}
	for(i = 0; i < n; i++) f[0][1 << i] = 0;
	for(i = 1; i < n; i++){
		for(s = 0; s < (1 << n); s++){
			for(s0 = s; s0; s0 = (s0 - 1) & s) f[i][s] = min(f[i][s], f[i - 1][s0] + i * v[s0][s ^ s0]);
			//printf("%d %d %d\n", i, s, f[i][s]);
		}
	}
	for(i = 0; i < n; i++) ans = min(ans, f[i][(1 << n) - 1]);
	printf("%d", ans);
	return 0;
}
评论 2
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值