NOIP2017提高组day2 - T2:宝藏

题目链接

[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 1n),和这条道路的长度 v v v

输出格式

一个正整数,表示最小的总代价。

样例 #1

样例输入 #1

4 5 
1 2 1 
1 3 3 
1 4 1 
2 3 4 
3 4 1

样例输出 #1

4

样例 #2

样例输入 #2

4 5 
1 2 1 
1 3 3 
1 4 1 
2 3 4 
3 4 2

样例输出 #2

5

提示

在这里插入图片描述

【样例解释 1 1 1

小明选定让赞助商打通了 1 1 1 号宝藏屋。小明开发了道路 1 → 2 1 \to 2 12,挖掘了 2 2 2 号宝藏。开发了道路 1 → 4 1 \to 4 14,挖掘了 4 4 4 号宝藏。还开发了道路 4 → 3 4 \to 3 43,挖掘了 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 12,挖掘了 2 2 2 号宝藏。开发了道路 1 → 3 1 \to 3 13,挖掘了 3 3 3 号宝藏。还开发了道路 1 → 4 1 \to 4 14,挖掘了 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 1n8 v ≤ 5 × 1 0 3 v \le 5\times 10^3 v5×103 且所有的 v v v 都相等。

对于 40 % 40\% 40% 的数据: 1 ≤ n ≤ 8 1 \le n \le 8 1n8 0 ≤ m ≤ 1 0 3 0 \le m \le 10^3 0m103 v ≤ 5 × 1 0 3 v \le 5\times 10^3 v5×103 且所有的 v v v 都相等。

对于 70 % 70\% 70% 的数据: 1 ≤ n ≤ 8 1 \le n \le 8 1n8 0 ≤ m ≤ 1 0 3 0 \le m \le 10^3 0m103 v ≤ 5 × 1 0 3 v \le 5\times 10^3 v5×103

对于 100 % 100\% 100% 的数据: 1 ≤ n ≤ 12 1 \le n \le 12 1n12 0 ≤ m ≤ 1 0 3 0 \le m \le 10^3 0m103 v ≤ 5 × 1 0 5 v \le 5\times 10^5 v5×105

算法思想

根据题目描述,小明需要考虑如何开凿宝藏屋之间的道路,并且两个已经被挖掘过的宝藏屋之间的道路无需再开发,也就是说题目求的是一棵生成树,使得代价和最小。

由于开发一条道路的代价与道路的长度 L L L和从起点到宝藏屋所经过的宝藏屋的数量(即宝藏屋的深度 K K K有关,因此不能使用最小生成树的算法进行计算。

考虑到结点数 n n n的范围较小( 1 ≤ n ≤ 12 1 \le n \le 12 1n12),考虑使用状态压缩动态规划解决。

状态表示

S S S表示当前生成树的状态,其二进制位上的 0 0 0 1 1 1表示是否包含相应的宝藏屋。例如,当有 0 、 1 、 2 、 3 、 4 0、1、2、3、4 01234一共 5 5 5个宝藏屋时,目前生成树已经包含了 0 、 3 、 4 0、3、4 034,那么 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 02n1

状态除了跟当前生成树的情况有关,还与树的深度有关,用 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[2n1][i]},其中 0 ≤ i < n 0\le i<n 0i<n

状态计算

状态转移

要计算当前状态 f [ s t a t e ] [ i ] f[state][i] f[state][i],从最后一步分析,即从第 i − 1 i-1 i1层可以转移到第 i i i

  • 不妨设第 i i i层点集(宝藏屋)的状态为 s s s s s s是状态 s t a t e state state的一个子集
  • 如果前 i − 1 i-1 i1层的点集的状态为 t t t,那么 t = s t a t e ⊕ s t= state\oplus s t=states(异或运算 x o r xor xor

因此:
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][i1]+cost}

计算代价

其中 c o s t cost cost表示第 i i i层所有点(宝藏屋)到第 i − 1 i-1 i1层的最小代价。那么 c o s t = L × K cost = L\times K cost=L×K,其中 L L L表示第 i i i层所有点到第 i − 1 i-1 i1层的长度之和; 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]时:

  • 要枚举 s t a t e state state的所有子集,即第 i i i层的点集状态 s s s
    • 计算出第 i i i层的所有点到第 i − 1 i-1 i1层的最小长度之和 L L L
    • 然后再枚举深度 i i i,计算 f [ s t a t e ] [ i ] f[state][i] f[state][i]

初始状态

  • 求的是工程总代价最小值,因此 f f f数组应初始化尽可能大
  • 可以打通任意一个宝藏屋到地面的通道到,因此对于任意一点 i i i,在生成树中只包含该点、且深度为 0 0 0时,其最小值应该为 0 0 0,即 f [ 1 < < i ] [ 0 ] = 0 f[1<<i][0]=0 f[1<<i][0]=0

时间复杂度

时间复杂度包含两部分:

  • 预处理 g [ i ] [ s t a t e ] g[i][state] g[i][state]的时间复杂度为 O ( n 2 × 2 n ) O(n^2\times2^n) O(n2×2n)
    • 状态数为 n × 2 n n\times2^n n×2n
    • 计算过程中需要枚举任意集合 s t a t e state state中任意点,时间复杂度 O ( n ) O(n) O(n)
  • 计算状态 f [ s t a t e ] [ i ] f[state][i] f[state][i]的时间复杂度为 O ( n × 3 n ) O(n\times3^n) O(n×3n)
    • 计算过程中需要枚举集合的所有子集。考虑对于元素个数为 k k k的子集,一共有 C n k C_n^k Cnk种情况,每个子集有 2 k 2^k 2k个子集,那么需要枚举的次数为 ∑ k = 0 n C n k × 2 k \sum_{k=0}^nC_n^k\times2^k k=0nCnk×2k。利用二项式定理: ∑ k = 0 n C n k × 2 k = ( 1 + 2 ) n = 3 n \sum_{k=0}^nC_n^k\times2^k=(1+2)^n=3^n k=0nCnk×2k=(1+2)n=3n
    • 对于每个子集需要 n n n次计算来算出剩余点到子集中的最小长度。

二项式定理 ( 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+Cn1xn1y1+Cn2x(n2)y2+...+Cnn1x1yn1+Cnnx0yn

代码实现

#include <iostream>
#include <cstring>
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;
}

  • 20
    点赞
  • 19
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

少儿编程乔老师

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值