P1433 吃奶酪题解

题目传递门

一、深度优先搜索

1、坐标

老鼠所在位置的用((x,y))来表示,所以需要声明一个结构体,用来描述坐标:

//坐标
struct Point {
    double x, y;
} a[N];

2、预处理

深度优先搜索最怕算的太多TLE,能预处理的一定要提前进行预处理。本题中,每个结点间都可能要计算距离,而且可能会重复计算(a->b)的距离,预处理就更划算~,同时计算(a->b)时,也就是同时算出了(b->a),一举多得啊!

   for (int i = 0; i < n; i++)
      for (int j = i + 1; j <= n; j++) {
          double x1 = a[i].x, y1 = a[i].y, x2 = a[j].x, y2 = a[j].y;
          dis[j][i] = dis[i][j] = sqrt(abs((x1 - x2) * (x1 - x2)) + abs((y1 - y2) * (y1 - y2)));
    }

3、深搜的参数

深度优先搜索中,最为关键的就是参数的设定。一般是依据题意,比如本题,需要解决如下几个关键点:
(1)、从哪个位置出发
(2)、已经吃完几个奶酪
(3)、已经走了多远的距离
向哪一个点出发,则不是参数,是因为它会作为下一个出发点,也就是关键点(1),重复使用概念没有意义。

/**
 *
 * @param pos  当前走的是第几个奶酪
 * @param num  已吃掉的奶酪个数
 * @param dis  当前距离的值
 */
void dfs(int pos, int num, double len) {
...
}

上面的代码返回值是void.那么,一定是在结果收集部分将本轮的最优解存入到一个全局变量中~,本题是变量(res)

4、回溯

因为是不能走回头路,所以肯定要使用(vis)数组保存走过了哪些点。走完了一条分支,还想走另一条,肯定要再将状态标识为可用。

5、C++代码

#include<bits/stdc++.h>

using namespace std;
const int INF = 0x3f3f3f3f;

const int N = 16;   //对于全部的测试点,保证 1<=n<=15,
int n;              //一共多少个奶酪
double res = INF;   //记录最短路径长度,也就是最终的答案
double dis[N][N];    //dis[i][j]记录第i个点到第j的点的距离.这个是预处理的二维数组,防止重复计算,预处理是搜索优化的重要手段
bool vis[N];         //记录i奶酪在当前的路径探索中是否已经尝试过(可以重复使用噢~)

//坐标
struct Point {
    double x, y;
} a[N];

/**
 *
 * @param pos  当前走的是第几个奶酪
 * @param num  已吃掉的奶酪个数
 * @param dis  当前距离的值
 */
void dfs(int pos, int num, double len) {
    //剪一下枝
    if (len >= res) return;

    //前n个都吃完了,可以进行路线长短对比了
    if (num == n) {
        res = min(len, res);
        return;
    }
    //从pos出发,向1~n个奶酪进军,去吃~
    for (int i = 1; i <= n; i++) {
        //回溯算法的套路,如果在本次寻找过程中,没有走过这个节点
        if (!vis[i]) {
            //标识使用过了
            vis[i] = true;
            //再走一步
            dfs(i, num + 1, len + dis[pos][i]);
            //反向标识
            vis[i] = false;
        }
    }
}

int main() {
    //老鼠的原始位置
    a[0].x = 0, a[0].y = 0;

    //读入奶酪的坐标
    cin >> n;
    for (int i = 1; i <= n; i++) cin >> a[i].x >> a[i].y;

    //预处理(dfs能进行预处理的尽量预处理)
    for (int i = 0; i < n; i++)
        for (int j = i + 1; j <= n; j++) {
            double x1 = a[i].x, y1 = a[i].y, x2 = a[j].x, y2 = a[j].y;
            dis[j][i] = dis[i][j] = sqrt(abs((x1 - x2) * (x1 - x2)) + abs((y1 - y2) * (y1 - y2)));
        }
    //开始暴搜
    dfs(0, 0, 0);

    //输出结果
    printf("%.2f", res);
    return 0;
}

再想出剪枝的办法后,依然在测试点10处挂掉`TLE`了。需要想办法优化

二、记忆化+二进制状态表示法

时间复杂度最大(15^{15}),太大了,然后可以想到每次搜完纪录下来结果,接下来搜索过程中大于这个数直接退出当前层(剪枝)。但这种方法不能保证最坏情况的加速,有一部分情况和之前无异。

1、记忆化+二进制状态表示法

正确方法是记忆化搜索,数组纪录到达某个状态。下次在遇到相同状态可以调用数组即可。

怎么存状态,每个点要么走了要么没走,显然是(2)进制数就可以,且发现(n)最大(15),空间绝对没问题,但是......到这里还没完,仅仅这样存不了,因为它有路径这么一说,从不同点走过来,哪些点走了哪些点没走就算是一样的,只有最后一个点(当前点)不同,也不是一个状态,所以数组要(2)维,第二位是当前走到那个点,好了到这里就顺利储存状态了。

double f[1 << N][N];

说明:
一维:
最大范围是(2^N)次方,比如(N=5),就需要(32)来描述,最大的比如((11111)_2=31),(1<<N)就是(2^N)的意思。含义是已经走过的节点有哪些,是一个二进制转为十进制的数字,最终表现为十进制. 每一位代表一个奶酪,比如一共有(3)个奶酪,(f[(101)_{(2)}]=f[5_{(10)}]) 表示(1)号和(3)号奶酪吃完了,(2)号没有吃。

二维:
最大范围是(N),表示当前所在的点。

:
(f[i][j]) 如果以前计算过在以第(j)个位置出发,在前面已经完成(i)这种二进制表示法的节点完成情况下,最短的距离是多少。
比如(f[5][2])就是表示从(2)号状态出发,前面已经走完了(5_{(10)}=(101)_{(2)}),就是(1)(3)走完了,(2)号还没有走的意思。

2、怎么使用二进制状态表示法

(1)、深度优先搜索,都是思考下一步的操作怎么办
比如,当前在(pos)这个位置,下一个要去的位置是(i),那我们要思考,去还是不去呢?

(a)、如果这种状态没有出现过,那么肯定要去。
(b)、如果这种状态出现过,但没有我尝试去一下的效果好,那么我要去。
(c)、如果这种状态出现过,但比我尝试去一下的效果好,那我就不去。(人家现有更优秀~)

前提是我假设我已经走到了(i)这个位置,那么哪些东东变化了呢?
(a)、状态会变化
状态变为原来的状态加上走了(i)的状态,就是

  int nst = st + (1 << (i - 1));

(b)、距离会变化

len + dis[pos][i]

3、C++代码

#include<bits/stdc++.h>

using namespace std;

const int INF = 0x3f3f3f3f;//无穷大
const int N = 16;          //对于全部的测试点,保证 1<=n<=15,
int n;                     //一共多少个奶酪
double res = INF;          //记录最短路径长度,也就是最终的答案
double dis[N][N];          //dis[i][j]记录第i个点到第j的点的距离.这个是预处理的二维数组,防止重复计算,预处理是搜索优化的重要手段
bool vis[N];               //记录i奶酪在当前的路径探索中是否已经尝试过(可以重复使用噢~)

//坐标
struct Point {
    double x, y;
} a[N];

//状态压缩
//一维:代表已经走过的节点有哪些,是一个二进制转为十进制的数字,最终表现为十进制.
// 每一位代表一个奶酪,比如一共有3个奶酪,f[(101)二进制] =f[5] 表示1号和3号奶酪吃完了,2号没有吃
// 二维:代表当前的出发点
// 值:f[i][j] 如果以前计算过在以第j个位置出发,在前面已经完成i这种二进制表示法的节点完成情况下,最短的距离是多少
double f[1 << N][N];

/**
 *
 * @param pos     当前的位置
 * @param status  当前状态压缩表示吃到的奶酪的状态[之前都吃了谁?]eg:status=10010,代表吃了第二个和第五个奶酪(从右往左)
 * @param num     已经吃掉了几个奶酪
 * @param len     当前距离
 */
void dfs(int pos, int num, int st, double len) {
    //剪一下枝
    if (len >= res) return;

    //表示n个都吃完了,可以进行路线长短对比了
    if (num == n) {
        res = min(len, res);
        return;
    }
    //从pos出发,向1~n个奶酪进军,去吃~
    for (int i = 1; i <= n; i++) {
        //回溯算法的套路,如果在本次寻找过程中,没有走过这个节点

        //注意:深度优先一直思考的是下一个动作,下一个动作,下一个动作,重要的话说三遍!
        //(1)、i就是下一个点,如果选择了i,那么就要状态就会变成加入i的样子:int nst = st + (1 << (i - 1));
        //(2)、如果以前记录过这个状态,那么现在有没有机会去更新它?更新的条件是比它小。
        if (!vis[i]) {
            int nst = st + (1 << (i - 1));//目标状态,也就是要走完第i个结点之后的状态,比如i=1,就是2^0=1,描述状态用 1<<(i-1)噢~
            //f[x][y]一路保存最小值,能用上就干掉一大批!
            if (f[nst][i] && f[nst][i] <= len + dis[pos][i]) continue;
            //标识状态
            vis[i] = true;
            //更新当前状态的值
            f[nst][i] = len + dis[pos][i];
            //继续深搜
            dfs(i, num + 1, nst, f[nst][i]);
            //取消标识
            vis[i] = false;
        }
    }
}

int main() {
    //老鼠的原始位置
    a[0].x = 0, a[0].y = 0;

    //读入奶酪的坐标
    cin >> n;
    for (int i = 1; i <= n; i++) cin >> a[i].x >> a[i].y;

    //预处理(dfs能进行预处理的尽量预处理)
    for (int i = 0; i <= n; i++)
        for (int j = i + 1; j <= n; j++) {
            double x1 = a[i].x, y1 = a[i].y, x2 = a[j].x, y2 = a[j].y;
            dis[j][i] = dis[i][j] = sqrt(abs((x1 - x2) * (x1 - x2)) + abs((y1 - y2) * (y1 - y2)));
        }

    //深度优先搜索
    dfs(0, 0, 0, 0);

    //输出
    printf("%.2f", res);
    return 0;
}

三、全排列最笨的解法,可以过50%-60%的测试点(吸氧)

#include<bits/stdc++.h>

using namespace std;
const int INF = 0x3f3f3f3f;

const int N = 16;   //对于全部的测试点,保证 1<=n<=15,
int n;              //一共多少个奶酪
double res = INF;   //记录最短路径长度,也就是最终的答案
double dis[N][N];    //dis[i][j]记录第i个点到第j的点的距离.这个是预处理的二维数组,防止重复计算,预处理是搜索优化的重要手段

//坐标
struct Point {
    double x, y;
} a[N];

int per[] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11};

int main() {
    //老鼠的原始位置
    a[0].x = 0, a[0].y = 0;

    //读入奶酪的坐标
    cin >> n;
    for (int i = 1; i <= n; i++) cin >> a[i].x >> a[i].y;

    //预处理
    for (int i = 0; i < n; i++)
        for (int j = i + 1; j <= n; j++) {
            double x1 = a[i].x, y1 = a[i].y, x2 = a[j].x, y2 = a[j].y;
            dis[j][i] = dis[i][j] = sqrt(abs((x1 - x2) * (x1 - x2)) + abs((y1 - y2) * (y1 - y2)));
        }

    //1~n的全排列,计算每一组组合的距离和,找出最小值,也就能过70%的测试点,11个数据是极限
    double MIN = INF;
    do {
        //计算当前路线下的行走距离
        double s = dis[0][per[0]];
        for (int i = 0; i < n - 1; i++) s += dis[per[i]][per[i + 1]];
        MIN = min(MIN, s);
    } while (next_permutation(per, per + n));
    //输出大吉
    printf("%.2f", MIN);
    return 0;
}
原文地址:https://www.cnblogs.com/littlehb/p/15015537.html