洛谷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\n", 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\n", ans);

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

最后的最后:

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

posted @ 2020-02-27 23:53  quanjun  阅读(408)  评论(0编辑  收藏  举报