• 洛谷P1433 吃奶酪 题解 状态压缩DP


    题目链接:https://www.luogu.com.cn/problem/P1433

    题目大意

    房间里放着 (n) 块奶酪。一只小老鼠要把它们都吃掉,问至少要跑多少距离?老鼠一开始在 ((0,0)) 点处。

    输入格式

    第一行一个正整数 (n)
    接下来每行 (2) 个实数,表示第 (i) 块奶酪的坐标。
    两点之间的距离公式为 (sqrt{(x_1-x_2)^2+(y_1-y_2)^2})

    输出格式

    一个数,表示要跑的最少距离,保留 (2) 位小数。

    解题思路

    定义状态 (f[st][i]) 表示当前状态为 (st) ,且最后一个到达的点是 (i) 点时的最少距离。

    首先,因为 (st) 的二进制表示中的那些为 (1) 的位表示的是小老鼠已经到达的点,所以如果 (st) 的第 (i) 位不为 (1),则状态 (f[st][i]) 不合法。

    其次:

    如果状态 (st) 有且只有一位为 (1) (即 __builtin_popcount(st) == 1),并且我们假设为 (1) 的这一位为第 (i) 位,则 (f[st][i] = sqrt{x_i^2 + y_i^2}) (因为小老鼠一开始在 ((0,0)) 点,从 ((0,0)) 点到 ((x_i,y_i)) 点的距离是 (sqrt{x_i^2 + y_i^2}));

    否则(状态 (st)(1) 的位数 (gt 1)),说明状态 (f[st][i]) 是可以通过一个合法的状态 (f[st2][j]) 转换过来的。(其中 st2 = st^(1<<i)

    此时,我们可以得到状态转移方程为:

    [f[st][i] = min(f[st2][j] + sqrt{(x_i-x_j)^2+(y_i-y_j)^2}) ]

    其中,st2 = st^(1<<i)

    (sqrt{(x_i-x_j)^2+(y_i-y_j)^2}) 表示的就是点 ((x_j,y_j)) 到点 ((x_i,y_i)) 的距离。

    实现代码如下:

    #include <bits/stdc++.h>
    using namespace std;
    double dis(double x1, double y1, double x2, double y2) {
        return sqrt((x1-x2)*(x1-x2) + (y1-y2)*(y1-y2));
    }
    int n;
    double x[15], y[15], f[(1<<15)][15];
    bool vis[(1<<15)][15];
    int main() {
        cin >> n;
        for (int i = 0; i < n; i ++) cin >> x[i] >> y[i];
        for (int st = 0; st < (1<<n); st ++) {
            for (int i = 0; i < n; i ++) {
                if (!(st & (1<<i))) continue;
                if (__builtin_popcount(st) == 1) f[st][i] = dis(0, 0, x[i], y[i]);
                else {
                    int st2 = st ^ (1<<i);
                    for (int j = 0; j < n; j ++) {
                        if (!(st2 & (1<<j))) continue;
                        double tmp = f[st2][j] + dis(x[i], y[i], x[j], y[j]);
                        if (!vis[st][i] || f[st][i] > tmp) {
                            vis[st][i] = true;
                            f[st][i] = tmp;
                        }
                    }
                }
            }
        }
        double ans = f[(1<<n)-1][0];
        for (int i = 1; i < n; i ++) ans = min(ans, f[(1<<n)-1][i]);
        printf("%.2lf
    ", ans);
        return 0;
    }
    

    代码分析

    我们对这个代码中的主要片段进行一下分析:

    double dis(double x1, double y1, double x2, double y2) {
        return sqrt((x1-x2)*(x1-x2) + (y1-y2)*(y1-y2));
    }
    

    dis函数用于计算点 ((x_1,y_1)) 到点 ((x_2,y_2)) 之间的距离。

    int n;
    double x[15], y[15], f[(1<<15)][15];
    bool vis[(1<<15)][15];
    

    n用来表示点(或者说——奶酪)的数量。

    (x[i],y[i]) 用于表示点的距离。

    (f[st][i]) 的含义我们已经讲过了,这里就不再继续讲了。

    (vis[st][i]) 相当于我们记忆化的操作。

    我们以往的操作都会选择将 (f[st][i]) 赋为一家很大的值,或者将它赋值为-1来表示无穷大,但是我们开一个vis数组,通过 (vis[st][i]) 是否为 (true) 来判断状态 (f[st][i]) 有没有更新过也是可以的(没有更新过说明 (f[st][i]) 对应的状态还是无穷大,更新过说明 (f[st][i]) 已经被更新为了一个较小的值)。
    这部分逻辑在我们代码中 (f[st2][j]) 更新 (f[st][i]) 的时候有遇到:

    double tmp = f[st2][j] + dis(x[i], y[i], x[j], y[j]);
    if (!vis[st][i] || f[st][i] > tmp) {
        vis[st][i] = true;
        f[st][i] = tmp;
    }
    
    if (__builtin_popcount(st) == 1) f[st][i] = dis(0, 0, x[i], y[i]);
    

    这句话对应我们上面分析的第一种情况(如果状态 (st) 有且只有一位为 (1)),此时就直接更新 (f[st][i]) 为起点(((0,0))) 到点 (i)((x_i, y_i))) 的距离即可。

    否则,对于状态 (f[st][i]) ,需要找到所有它的前一步的状态 (f[st2][j]),并且通过如下代码求得 (f[st][i])

    int st2 = st ^ (1<<i);
    for (int j = 0; j < n; j ++) {
        if (!(st2 & (1<<j))) continue;
        double tmp = f[st2][j] + dis(x[i], y[i], x[j], y[j]);
        if (!vis[st][i] || f[st][i] > tmp) {
            vis[st][i] = true;
            f[st][i] = tmp;
        }
    }
    

    而最终的状态 (st) 肯定等于 (2^n-1)(2^n-1) 的后 (n) 位都为 (1),表示 (n) 个点都走过),所以答案即为

    [min_{i in [0,n-1]} f[2^n-1][i] ]

    我们是通过如下代码段来获得答案的:

    double ans = f[(1<<n)-1][0];
    for (int i = 1; i < n; i ++) ans = min(ans, f[(1<<n)-1][i]);
    printf("%.2lf
    ", ans);
    

    最后,也不要忘了输出我们的 ans,同时保留2位小数哦。

    最后的最后:

    关于数位DP,最好还是按照坐标从 (0)(n-1) 为好,因为这样的 (i) 刚好能跟状态在 ([0, 2^n-1]) 范围内的数字一一对应。所以希望还是能够按照坐标从 (0) 开始比较好。

  • 相关阅读:
    Python学习笔记(11)递归与匿名函数
    Python学习笔记(10)局部变量、全局变量、常量
    Python学习笔记(8)函数、位置参数、可变参数、关键字参数
    Pyhthon学习笔记(7)三元表达式及列表生成式
    Python自动化学习笔记(5)字符串常用的方法
    Python自动化学习(4)列表与字符串的切片操作
    python自动化学习笔记(3)列表list和字典dict
    python自动化学习笔记(2)字符串格式化输出与条件判断及循环语句
    Python自动化学习笔记(1)认识接口测试以及postman、Charles工具简单应用
    《软件测试的艺术》 阅读整理(二)
  • 原文地址:https://www.cnblogs.com/quanjun/p/12375333.html
Copyright © 2020-2023  润新知