[NOIP2017 提高组] 宝藏
参与考古挖掘的小明得到了一份藏宝图,藏宝图上标出了 n n n 个深埋在地下的宝藏屋, 也给出了这 n n n 个宝藏屋之间可供开发的 m m m 条道路和它们的长度。
小明决心亲自前往挖掘所有宝藏屋中的宝藏。但是,每个宝藏屋距离地面都很远,也就是说,从地面打通一条到某个宝藏屋的道路是很困难的,而开发宝藏屋之间的道路则相对容易很多。
小明的决心感动了考古挖掘的赞助商,赞助商决定免费赞助他打通一条从地面到某个宝藏屋的通道,通往哪个宝藏屋则由小明来决定。
在此基础上,小明还需要考虑如何开凿宝藏屋之间的道路。已经开凿出的道路可以 任意通行不消耗代价。每开凿出一条新道路,小明就会与考古队一起挖掘出由该条道路所能到达的宝藏屋的宝藏。另外,小明不想开发无用道路,即两个已经被挖掘过的宝藏屋之间的道路无需再开发。
新开发一条道路的代价是 L × K \mathrm{L} \times \mathrm{K} L×K。其中 L L L 代表这条道路的长度, K K K 代表从赞助商帮你打通的宝藏屋到这条道路起点的宝藏屋所经过的宝藏屋的数量(包括赞助商帮你打通的宝藏屋和这条道路起点的宝藏屋) 。
请你编写程序为小明选定由赞助商打通的宝藏屋和之后开凿的道路,使得工程总代价最小,并输出这个最小值。
第一行两个用空格分离的正整数 n , m n,m n,m,代表宝藏屋的个数和道路数。
接下来 m m m 行,每行三个用空格分离的正整数,分别是由一条道路连接的两个宝藏屋的编号(编号为 1 − n 1-n 1−n),和这条道路的长度 v v v。
一个正整数,表示最小的总代价。
4 5
1 2 1
1 3 3
1 4 1
2 3 4
3 4 1
4
4 5
1 2 1
1 3 3
1 4 1
2 3 4
3 4 2
5
【样例解释 1 1 1】
小明选定让赞助商打通了 1 1 1 号宝藏屋。小明开发了道路 1 → 2 1 \to 2 1→2,挖掘了 2 2 2 号宝藏。开发了道路 1 → 4 1 \to 4 1→4,挖掘了 4 4 4 号宝藏。还开发了道路 4 → 3 4 \to 3 4→3,挖掘了 3 3 3 号宝藏。
工程总代价为 1 × 1 + 1 × 1 + 1 × 2 = 4 1 \times 1 + 1 \times 1 + 1 \times 2 = 4 1×1+1×1+1×2=4。
【样例解释 2 2 2】
小明选定让赞助商打通了 1 1 1 号宝藏屋。小明开发了道路 1 → 2 1 \to 2 1→2,挖掘了 2 2 2 号宝藏。开发了道路 1 → 3 1 \to 3 1→3,挖掘了 3 3 3 号宝藏。还开发了道路 1 → 4 1 \to 4 1→4,挖掘了 4 4 4 号宝藏。
工程总代价为 1 × 1 + 3 × 1 + 1 × 1 = 5 1 \times 1 + 3 \times 1 + 1 \times 1 = 5 1×1+3×1+1×1=5。
【数据规模与约定】
对于 20 % 20\% 20% 的数据: 保证输入是一棵树, 1 ≤ n ≤ 8 1 \le n \le 8 1≤n≤8, v ≤ 5 × 1 0 3 v \le 5\times 10^3 v≤5×103 且所有的 v v v 都相等。
对于 40 % 40\% 40% 的数据: 1 ≤ n ≤ 8 1 \le n \le 8 1≤n≤8, 0 ≤ m ≤ 1 0 3 0 \le m \le 10^3 0≤m≤103, v ≤ 5 × 1 0 3 v \le 5\times 10^3 v≤5×103 且所有的 v v v 都相等。
对于 70 % 70\% 70% 的数据: 1 ≤ n ≤ 8 1 \le n \le 8 1≤n≤8, 0 ≤ m ≤ 1 0 3 0 \le m \le 10^3 0≤m≤103, v ≤ 5 × 1 0 3 v \le 5\times 10^3 v≤5×103。
对于 100 % 100\% 100% 的数据: 1 ≤ n ≤ 12 1 \le n \le 12 1≤n≤12, 0 ≤ m ≤ 1 0 3 0 \le m \le 10^3 0≤m≤103, v ≤ 5 × 1 0 5 v \le 5\times 10^5 v≤5×105。
根据题目描述,小明需要考虑如何开凿宝藏屋之间的道路,并且两个已经被挖掘过的宝藏屋之间的道路无需再开发,也就是说题目求的是一棵生成树,使得代价和最小。
由于开发一条道路的代价与道路的长度 L L L和从起点到宝藏屋所经过的宝藏屋的数量(即宝藏屋的深度) K K K有关,因此不能使用最小生成树的算法进行计算。
考虑到结点数 n n n的范围较小( 1 ≤ n ≤ 12 1 \le n \le 12 1≤n≤12),考虑使用状态压缩动态规划解决。
用 S S S表示当前生成树的状态,其二进制位上的 0 0 0和 1 1 1表示是否包含相应的宝藏屋。例如,当有 0 、 1 、 2 、 3 、 4 0、1、2、3、4 0、1、2、3、4一共 5 5 5个宝藏屋时,目前生成树已经包含了 0 、 3 、 4 0、3、4 0、3、4,那么 s t a t e = ( 11001 ) 2 state=(11001)_2 state=(11001)2。 s t a t e state state的范围从 0 ∼ 2 n − 1 0\sim 2^n-1 0∼2n−1。
状态除了跟当前生成树的情况有关,还与树的深度有关,用 i i i表示当前生成树的深度,其中起点的深度为 0 0 0。
因此, f [ s t a t e ] [ i ] f[state][i] f[state][i]表示当前生成树状态为 s t a t e state state、并且树的深度为 i i i时,工程总代价的最小值。
最终结果为包含所有宝藏屋时,对于不同深度的生成树取最小值,即 m i n { f [ 2 n − 1 ] [ i ] } min\{f[2^n-1][i]\} min{f[2n−1][i]},其中 0 ≤ i < n 0\le i
要计算当前状态 f [ s t a t e ] [ i ] f[state][i] f[state][i],从最后一步分析,即从第 i − 1 i-1 i−1层可以转移到第 i i i层
因此:
f [ s t a t e ] [ i ] = m i n { f [ t ] [ i − 1 ] + c o s t } f[state][i] = min\{f[t][i-1]+cost\} f[state][i]=min{f[t][i−1]+cost}
其中 c o s t cost cost表示第 i i i层所有点(宝藏屋)到第 i − 1 i-1 i−1层的最小代价。那么 c o s t = L × K cost = L\times K cost=L×K,其中 L L L表示第 i i i层所有点到第 i − 1 i-1 i−1层的长度之和; K K K代表从起点到第 i i i的深度。
为了能够快速计算 L L L,可以预处理得到任一点到所有集合的最小长度,不妨设 g [ i ] [ s t a t e ] g[i][state] g[i][state]表示点 i i i到点集 s t a t e state state的最小长度。那么 g [ i ] [ s t a t e ] g[i][state] g[i][state]等于点 i i i到点集 s t a t e state state中任意一点的最短距离。
枚举所有要计算的点集 s t a t e state state,在计算当前状态 f [ s t a t e ] [ i ] f[state][i] f[state][i]时:
时间复杂度包含两部分:
二项式定理 ( x + y ) n = C n 0 x n y 0 + C n 1 x n − 1 y 1 + C n 2 x ( n − 2 ) y 2 + . . . + C n n − 1 x 1 y n − 1 + C n n x 0 y n (x+y)^n=C_n^0x^ny^0+C_n^1x^{n-1}y^1+C_n^2x^(n-2)y^2+...+C_n^{n-1}x^1y^{n-1}+C_n^nx^0y^n (x+y)n=Cn0xny0+Cn1xn−1y1+Cn2x(n−2)y2+...+Cnn−1x1yn−1+Cnnx0yn:
#include
#include
using namespace std;
const int N = 12, M = 1 << N, INF = 0x3f3f3f3f;
//g[i][state]表示点i到集合state的最小长度
int w[N][N], g[N][M];
//f[state][i]表示当前生成树状态为state、并且树的深度为i时,工程总代价的最小值
int f[M][N];
int main()
{
int n, m;
cin >> n >> m;
memset(w, 0x3f, sizeof w);
while(m --)
{
int a, b, c;
cin >> a >> b >> c;
//点的编号从0开始,以便处理点集
a --, b --;
//有重边,所以取最小值
w[a][b] = w[b][a] = min(w[a][b], c);
}
//预处理任一点到所有集合的最小长度
memset(g, 0x3f, sizeof g);
for(int i = 0; i < n; i ++)
for(int state = 0; state < 1 << n; state ++)
//枚举state中的点
for(int k = 0; k < n; k ++)
if(state >> k & 1)
//更新i到集合state长度的最小值
g[i][state] = min(g[i][state], w[i][k]);
//初始状态
memset(f, 0x3f, sizeof f);
//以i点为起点到达深度0的最小代价为0
for(int i = 0; i < n; i ++) f[1 << i][0] = 0;
//状态计算
for(int state = 1; state < 1 << n; state ++)
{
//枚举state的子集s
for(int s = state - 1 & state; s != 0; s = s - 1 & state)
{
//t表示前i-1层的点集状态
int t = state ^ s, L = 0;
//枚举第i层所有点,计算第i层的所有点到第i-1层的最小长度之和L
for(int k = 0; k < n; k ++)
{
if(s >> k & 1) //点k在第i层的集合中
{
L += g[k][t]; //累加最后一层所有点到上一层的长度
if(L >= INF) break; //点k到不了第i-1层的所有点
}
}
if(L >= INF) continue; //子集s中存在点无法到达第i-1层
//枚举深度,计算当前状态f[state][i]
for(int i = 1; i < n; i ++)
f[state][i] = min(f[state][i], f[t][i - 1] + L * i);
}
}
//结果为包含所有宝藏屋时,对于不同深度的生成树取最小值
int ans = INF;
for(int i = 0; i < n; i ++)
ans = min(ans, f[(1 << n) - 1][i]);
cout << ans;
return 0;
}