• AcWing 529. 宝藏


    AcWing题目传送门

    洛谷-题目传送门

    一、看懂题意

    给出一个无向图,要求找一棵生成树,使得代价和最小。

    (1)从哪个点出发都可以。

    (2)代价和有自己的计算办法:增加一个节点的代价=选择的边权*树的高度。

    二、解题思路

    1、状态压缩DP

    看到数据范围就知道大概是个状压了,\(n\)值很小嘛,\(2^n\)数量可控!

    2、状态设计原则

    进行状态设计时,考虑对于每个时刻需要知道的信息,以此设计状态维度。

    每个时刻,我们关心的只有我们已经把多少点加进生成树了,以及加入新的节点时的树高是多少

    \(f[S][i]\)为:当前生成树已经包含集合\(S\)中的点,并且树高是\(i\)的情况下,花费费用的最小值。

    状态转移方程:

    \[f[S][i]=min(f[S_{pre}][i-1]+cost) \]

    其中\(S_{pre}\)\(S\)的前序子集,通过\(S_{pre}\)加边(一条或多条都可以,是状态的转化,而不是某个点、某条边的转化,是一类的转化)一定可以连接成\(S\),\(cost\)是这次加边的花费。

    问题1:如何获得\(S_{pre}\)这个集合?
    怎么判断\(S_{pre}\)在转移中是否合法呢?设\(G[S_{pre}]\)\(S_{pre}\)能拓展到的状态集合,显然\(G\)是可以预处理出来的。如果\(G[S_{pre}]\)可以包含\(S\),那么就说明\(S_{pre}\)可以一步转化为\(S\)

    问题2:如何计算这个费用\(cost\)
    \(S_{remain}=S\ xor\ S_{pre}\) ,即\(S_{remain}\)是在\(S\)中存在,但在\(S_{pre}\)中不存在的点,也就是本轮增加进来的节点。这里\(cost\)的计算显然是对于每个\(S_{remain}\)中的节点,取\(S_{pre}\)中的元素向它连一条最短的边求和后\(\times i\)

    现在来整理一下思路:

    • 枚举集合,对于每个集合的子集,通过\(G[S]\)判断该子集是否合法,如果合法,枚举所有需要被连向的点的最小边权求和乘深度,作为答案。

    • 答案就是全集在\(1~n\)深度的最小值。

    三、实现代码

    #include <bits/stdc++.h>
    
    using namespace std;
    const int N = 20;       //节点个数上限
    const int M = 1 << N;   //节点的状态表示 2^N种状态,状压DP的二进制表示法
    const int INF = 0x3f3f3f3f;//正无穷
    int d[N][N];    //d[i][j]表示i号点和j号点之间边的长度,如果两点之间没有边可以连通,则长度定义为为正无穷
    int n;          //节点数量
    int m;          //边数量
    
    int g[M];       //g[i]表示状态i+状态i一步可达的状态
    int f[M][N];    //f[i][j]表示在状态i、树高度为j时的最小花费
    
    int main() {
        //优化输入
        ios::sync_with_stdio(false);
        //读入节点数量和边的数量
        cin >> n >> m;
        //1、距离数组初始化
        memset(d, 0x3f, sizeof d);      //初始化距离d数组为正无穷,描述最开始所有节点之间均未连通
        for (int i = 0; i < n; i++) d[i][i] = 0;    //自己到自己的距离是0,注意这里的数组下标是从0开始的,
        // 因为状态压缩DP是基于二进制的,二进制喜欢的是下标从0开始
    
        //2、DP数组初始化
        memset(f, 0x3f, sizeof f);//为了统计最小值,初始化正无穷
        //"赞助商决定免费赞助他打通一条从地面到某个宝藏屋的通道"
        //选择打通哪一条都是一样的,都不要钱,所以在选择每一个结点为起点时,费用都是0
        for (int i = 0; i < n; i++) f[1 << i][0] = 0;
    
        //读入边及权值
        for (int i = 1; i <= m; i++) {//m条边
            int a, b, c;
            cin >> a >> b >> c;       //起点,终点,权值
            //因为我们要用状态压缩,故所有点下标从0开始比较好,方便进行二进制运算,所以都进行了--
            //若从1开始,第一个点就是2的1次方,即10,明显不好统计
            a--, b--;
            //(1)无向图 (2)防止重边,留最小值
            d[a][b] = d[b][a] = min(d[a][b], c);
        }
    
        //思考:为什么需要引入g数组,它的存在价值是什么?
        //答:经过一次预处理,以后再求i的后续可用状态时,可以实现O(1)级判定
    
        //核心1:预处理出g数组,即i状态一步可达的状态g[i]
        for (int i = 0; i < 1 << n; i++)    //枚举每个状态,例:当i为1010时,表示此状态有第2、4个节点
            for (int j = 0; j < n; j++)     //枚举状态i中的每一位
                if (i >> j & 1) {           //判定状态i中第j位是否为1,即判定状态i中是否存在j这个节点
                    //尝试从j点出发,通过j的连接边,找到所以未加入到集合S中的点,即j节点一步可达的所有点
                    for (int k = 0; k < n; k++)//枚举每一个节点
                        if (d[j][k] != INF)    //判断宝藏点j能否一步到达宝藏点k
                            g[i] |= 1 << k;    //利用位运算,将g[i]的第k位设置为1,描述i这个状态,可以一步转移到第k位是1的状态
                    //这里边包含处理了j==k的情况,因为初始时,d[i][i]=0,不是INF,也是可达的
                }
    
        //状态转移
        for (int i = 0; i < 1 << n; i++)//枚举每个状态
            //枚举状态i的所有状态子集j,i必然是由j转化而来,但不一定是一步转化,还需要再次的检查
            for (int j = (i - 1) & i; j; j = (j - 1) & i) {
                //g[j]表示j能一步到达的状态
                //若g[j]包含状态i的所有点,则表示状态j能一步到达状态i
                if ((g[j] & i) == i) {
                    //因为j是i的子集,remain表示i中j的补集,
                    //即状态j到达状态i过程中新增的宝藏点的状态
                    int remain = i ^ j;//这一轮补充哪些节点
                    //下面是为j -> i新增的节点找到最小边的操作,同时统计最小花费
                    int cost = 0;//cost表示在状态j到达状态i过程中用到的最小花费
                    for (int k = 0; k < n; k++) //枚举remain状态每一位
                        if (remain >> k & 1) {  //找出remain状态中的节点
                            int t = INF;//t表示remain中每一位(即新增的节点)到达j中某一点的最小花费
                            for (int u = 0; u < n; u++)//枚举j状态的每一位
                                if (j >> u & 1)//找出j状态中的节点
                                    t = min(t, d[k][u]);
                            cost += t;
                        }
                    //状态转移
                    for (int k = 1; k < n; k++)//枚举树的高度
                        f[i][k] = min(f[i][k], f[j][k - 1] + cost * k);
                }
            }
    
        //枚举答案最小值
        int ans = INF;
        //最小值可能出现在任意一层,比如这道题的样例,从1号点开始挖,途径1-2,1-4,4-3,最大层是2层
        for (int i = 0; i < n; i++)
            ans = min(ans, f[(1 << n) - 1][i]);//(1 << n) - 1表示全选状态,即111111...
        //输出
        printf("%d", ans);
        return 0;
    }
    
  • 相关阅读:
    微博转发的内容如何实现点击人名跳转到个人主页
    解决json_encode中文UNICODE转码问题
    ***git自动化部署总结
    **Git本地仓库图解
    我 Git 命令列表 (1)【转】
    Git
    git pull使用【转】
    git merge简介【转】
    获得内核函数地址的四种方法
    【笔记】一些linux实用函数技巧【原创】
  • 原文地址:https://www.cnblogs.com/littlehb/p/15761600.html
Copyright © 2020-2023  润新知