• 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;
    }
    
  • 相关阅读:
    币值转换
    第八周作业
    第七周作业
    第五周编程总结
    第四周编程总结
    第三周编程总结
    7-1 查找整数
    7-2 求最大值及其下标
    秋季学习总结
    对我影响最大的三个老师
  • 原文地址:https://www.cnblogs.com/littlehb/p/15015537.html
Copyright © 2020-2023  润新知