• 【题解】P3959 宝藏


     P3959 宝藏

    题目描述

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

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

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

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

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

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

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

    输入格式

    第一行两个用空格分离的正整数 n,m,代表宝藏屋的个数和道路数。

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

    输出格式

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


    Solution

    这道题和 灰原 一起想了一下午还是没有想出来(我们果然还是太菜了),去某谷看了这篇题解,觉得写的很棒想法也很巧,想记录一下这道题来加深一下记忆。

    我们已经想到的:1.用一个数来表示走过的点的集合

            2.往这个集合里面加一个点时,枚举此集合里的所有点,取

             代价最小的连边

    卡住我们的点是:1.我们无法证明 当往一个集合里新添一个点的时候,只用在

             原来的连边方式上新连一条边即为最优解

            2.无法仅通过一个集合来计算出代价,因为代价中含有边的

             权值和两点之间的距离(或者说是在树上的两点之间的高

             度

    题解比我们多想的是:1.引进了一个新的变量-树的高度,这样的好处是,可以

                 方便得出代价

              2.在转移时,对于一个已经走了了 i 个点的集合,不一

                 定仅从走了 i - 1 个点的集合转移过来,可以一次性从

                 i 的子集转移过来,加很多点

              3.正因为一次性可以加多个点,那么每一次我们都是

                 添的点全部与高度最高的点连这样子得出的状态不

                 一定是正确的,但是正确的答案一定会被算出来(原

                 因请看4.)

              4.我们假设有集合 i高度为 h,由集合 j 转移过来,

                 此时需要在 j 里面新添 k 个点。我们先用枚举的方

                 法,求出sum 表示 k 个点与集合里的点的最短边之

                 和,然后将 sum 乘以 h,得出代价。很明显的可以

                 看出,k 个点,每个点向集合里连的最短边,不一定

                 是高度为 h 的点,所以这个状态所存的代价不一定是

                 正确的。但是,总有一个树的高度 h0 可以满足,这

               k 个点,所连的最短边相对应的另外 k 个点,全部是

                高度为 h0 的点(可以自己 举具体的例子一步一步往

                前推),则f[i][h0]所存的代价是正确的,而且是最优

                的,这样子原来的 f[i][h] 所存的,虽然不是正确的,

              但它一定不是最优的,所以取答案时绝对不会取到它

    Code

    #include<bits/stdc++.h>
    #define F(i, x, y) for(int i = x; i <= y; ++ i)
    using namespace std;
    int read();
    const int S = (1 << 12);
    const int N = 15;
    const int inf = 0x3f3f3f3f;
    int n, m, ans = inf, all;
    int u, v, e;
    int d[N][N]; //直接用邻接矩阵存加快访问速度
    int s[S]; //预处理出对于集合 i 此时还可以往外连那些点
    int f[S][N]; //f[i][j]表示集合 i 高度为 j 时的代价(其实也许是个假的233)
    int main()
    {
        n = read(), m = read(), -- n, all = (1 << n + 1) - 1; //为了方便 将点设为 0 ~ n - 1;all 为总状态数
        F(i, 0, n) F(j, 0, n) d[i][j] = inf;
        F(i, 0, all) F(j, 0, n) f[i][j] = inf;
        F(i, 0, n) f[(1 << i)][0] = 0; //因为可以自选起点,所以这些状态是合法的
        F(i, 1, m) 
        {
            u = read(), v = read(), e = read();
            -- u, -- v, d[u][v] = d[v][u] = min(d[u][v], e); //最多给了1000条边,而实际上最多只有72条,取最短的
        }
        F(i, 1, all)
            F(j, 0, n)
                if((i | (1 << j)) != i) 
                    F(k, 0, n) 
                        if((i | (1 << k)) == i && d[j][k] != inf)
                            s[i] |= (1 << j); //预处理出 s[]
        F(i, 1, all)
            for(int s0 = (i - 1) & i; s0; s0 = (s0 - 1) & i) //保证 s0 是 i 的子集
                if((s0 | s[s0] | i) == (s[s0] | s0)) //判断 s0 是否可以经过连边变成 i
                {
                    int sum = 0; //求出每条边的权值
                    F(k, 0, n)
                        if(((1 << k) | i) == i && ((1 << k) | s0) != s0) //如果 k 不属于 s0 而属于 i,则需要新连边
                        {
                            int tmp = inf;
                            F(h, 0, n)
                                if(((1 << h) | s0) == s0) //如果 h 属于 s0 则可以连边
                                    tmp = min(tmp, d[h][k]);
                            sum += tmp;
                        }
                    F(j, 1, n) 
                        if(f[s0][j - 1] != inf)
                            f[i][j] = min(f[i][j], f[s0][j - 1] + sum * j); //最关键的地方,一定要弄懂!!
                }
        F(i, 0, n) ans = min(ans, f[all][i]); //在全集中的每个高度中找最小值
        printf("%d
    ", ans);
        return 0;
    }
    int read()
    {
        int x = 0;
        char c = getchar();
        while(c < '0' || c > '9') c = getchar();
        while(c >= '0' && c <= '9') x = x * 10 + c - '0', c = getchar();
        return x;
    }
  • 相关阅读:
    java实现前n项和,要求不使用循环、乘除法、判断标识
    java 线程池 带返回值
    java 多线程 数据通信
    jedis使用分布式锁
    记一次自定义管理工厂使用spring自动装载bean
    面试题玩数组
    记一次随便排序算法
    九九乘法表打印记一次al面试
    多线程操作共享变量顺序输出abc 记一次al面试题
    博客迁移
  • 原文地址:https://www.cnblogs.com/Bn_ff/p/12160569.html
Copyright © 2020-2023  润新知