宝藏「NOIP2017」

【题目描述】
参与考古挖掘的小明得到了一份藏宝图,藏宝图上标出了 \(n\) 个深埋在地下的宝藏屋,也给出了这 \(n\) 个宝藏屋之间可供开发的 \(m\) 条道路和它们的长度。

小明决心亲自前往挖掘所有宝藏屋中的宝藏。但是,每个宝藏屋距离地面都很远,也就是说,从地面打通一条到某个宝藏屋的道路是很困难的,而开发宝藏屋之间的道路则相对容易很多。

小明的决心感动了考古挖掘的赞助商, 赞助商决定免费赞助他打通一条从地面到某个宝藏屋的通道,通往哪个宝藏屋则由小明来决定。

在此基础上, 小明还需要考虑如何开凿宝藏屋之间的道路。已经开凿出的道路可以任意通行不消耗代价。每开凿出一条新道路,小明就会与考古队一起挖掘出由该条道路所能到达的宝藏屋的宝藏。另外,小明不想开发无用道路,即两个已经被挖掘过的宝藏屋之间的道路无需再开发。

新开发一条道路的代价是:

这条道路的长度 \(\times\) 从赞助商帮你打通的宝藏屋到这条道路起点的宝藏屋所经过的宝藏屋的数量(包括赞助商帮你打通的宝藏屋和这条道路起点的宝藏屋)。

请你编写程序为小明选定由赞助商打通的宝藏屋和之后开凿的道路,使得工程总代价最小,并输出这个最小值。

【输入格式】
第一行两个用空格分离的正整数 \(n\)\(m\),代表宝藏屋的个数和道路数。

接下来 \(m\) 行,每行三个用空格分离的正整数,分别是由一条道路连接的两个宝藏屋的编号(编号为 \(1\sim n\)),和这条道路的长度 \(v\)

【输出格式】
输出共一行,一个正整数,表示最小的总代价。

【数据范围】
\(1 \le n \le 12,0 \le m \le 1000,v \le 500000\)

第一眼看到\(n \le 12\) 就知道十有八九是个状压DP_(:з」∠)_
然而本蒟蒻推不出来 抄的网上子状态和转移方程过的

不妨用\(dp[S][i]\)表示当前连接了集合S中的所有点,当前树的高度是\(i\)

\(val[S][i]\)表示将 点\(i\) 连向 (集合\(S\)中的所有点所组成的连通块) 所需的最小花费
不难看出 \(val[S][i] = min_{j \in S}(e[i][j])\) e[i][j]代表边\((i, j)\)的边权

那转移方程是什么呢
假设我们现在正在求\(dp[S][i]\)
枚举\(S\)的子集\(S2\),设\(S\)中除\(S2\)的部分为\(S3\),我们假设前\(i-1\)层是由\(S2\)中的点构成的 则在第\(i\)层中 我们要把\(S3\)中的点放在第\(i\)层 代价是多少呢? 其实这个我们之前已经预处理过了 代价就是\(\sum_{j \in S3}val[S2][j] * i\) \(i\)就是深度
得到转移方程为 \(dp[S][i]=dp[S2][i-1]+\sum_{j \in S3}val[S2][j] * i\)
枚举免费挖掘的第一个宝藏\(k\) 边界为\(dp[1<<(k-1)][0]=0\)

代码

#include <iostream>
#include <cstring>
#include <cstdio>
using namespace std;
typedef long long ll;

ll ans = 0x7fffffff, n, m, maxn;
ll dis[30][30], val[(1<<12)+5][30], dp[(1<<12)+5][30];

int main() {
	scanf("%lld %lld", &n, &m);
	if (n == 1) {
		puts("0");
		return 0;
	}
	memset(dis, 0x7f, sizeof(dis));
	for (int i = 1; i <= m; i++) {
		ll u, v, w;
		scanf("%lld %lld %lld", &u, &v, &w);
		dis[u-1][v-1] = dis[v-1][u-1] = min(dis[u-1][v-1], w);
	}
	maxn = (1 << n) - 1;
	for (ll i = 1; i <= maxn; i++) {
		for (ll j = 0; j < n; j++) {
			val[i][j] = 0x7fffffff;
			if ((1 << j) & i) continue;
			for (ll k = 0; k < n; k++) {
				if ((1 << k) & i) val[i][j] = min(val[i][j], dis[j][k]);
			}
		}
	}
	for (ll i = 0; i < n; i++) {
		memset(dp, 0x7f, sizeof(dp));
		dp[(1 << i)][0] = 0;
		for (ll s1 = 1; s1 <= maxn; s1++) {
			for (ll s2 = s1 & (s1-1); s2; s2 = s1 & (s2-1)) {
				ll s3 = s1 - s2, sum = 0;
				for (ll j = 0; j < n; j++) {
					if ((1 << j) & s3) sum += val[s2][j];
				}
				for (ll j = 1; j <= n; j++) {
					dp[s1][j] = min(dp[s1][j], dp[s2][j-1] + j * sum);
					
				}
			}
		}
		for (ll j = 1; j <= n; j++) {
			ans = min(ans, dp[maxn][j]);
		}
	}
	printf("%lld\n", ans);
	return 0;
}

一个枚举子集的方法

for (int s2 = (s-1) & s; s2; s2 = (s2-1)&s) {
	//s2即为s的子集
}
posted @ 2019-10-12 20:24  AK_DREAM  阅读(156)  评论(0编辑  收藏  举报