[leetcode每日一题2020/7/29]LCP 13. 寻宝

题目来源于leetcode,解法和思路仅代表个人观点。传送门
难度:困难
用时:08:00:00以上,构思大半天了

题目

我们得到了一副藏宝图,藏宝图显示,在一个迷宫中存在着未被世人发现的宝藏。

迷宫是一个二维矩阵,用一个字符串数组表示。它标识了唯一的入口(用 ‘S’ 表示),和唯一的宝藏地点(用 ‘T’ 表示)。但是,宝藏被一些隐蔽的机关保护了起来。在地图上有若干个机关点(用 ‘M’ 表示),只有所有机关均被触发,才可以拿到宝藏。

要保持机关的触发,需要把一个重石放在上面。迷宫中有若干个石堆(用 ‘O’ 表示),每个石堆都有无限个足够触发机关的重石。但是由于石头太重,我们一次只能搬一个石头到指定地点。

迷宫中同样有一些墙壁(用 ‘#’ 表示),我们不能走入墙壁。剩余的都是可随意通行的点(用 ‘.’ 表示)。石堆、机关、起点和终点(无论是否能拿到宝藏)也是可以通行的。

我们每步可以选择向上/向下/向左/向右移动一格,并且不能移出迷宫。搬起石头和放下石头不算步数。那么,从起点开始,我们最少需要多少步才能最后拿到宝藏呢?如果无法拿到宝藏,返回 -1 。

示例 1:

输入: ["S#O", "M..", "M.T"]

输出:16

解释:最优路线为: S->O, cost = 4, 去搬石头 O->第二行的M, cost = 3, M机关触发 第二行的M->O,
cost = 3, 我们需要继续回去 O 搬石头。 O->第三行的M, cost = 4, 此时所有机关均触发 第三行的M->T, cost
= 2,去T点拿宝藏。 总步数为16。

示例 2:

输入: ["S#O", "M.#", "M.T"]

输出:-1

解释:我们无法搬到石头触发机关

示例 3:

输入: ["S#O", "M.T", "M.."]

输出:17

解释:注意终点也是可以通行的。
限制:

1 <= maze.length <= 100
1 <= maze[i].length <= 100
maze[i].length == maze[j].length
S 和 T 有且只有一个
0 <= M的数量 <= 16
0 <= O的数量 <= 40,题目保证当迷宫中存在 M 时,一定存在至少一个 O 。

思路

首先可以思考一下,最短路径是怎么样的?
仔细拆解每一个不步骤就可以发现,主要是以下5步

  1. 起点到石堆S->O
  2. 石堆到其中一个机关O->M
  3. 机关到其中一个石堆M->O
  4. 重复(2)(3)中的步骤,直到剩下最后一个M
  5. 从最后一个M走到终点T。M->T

更进一步的分析

  1. 对于给定的Mi,计算S-Oi-Mi的最短路径
  2. 对于给定的Mj,计算Mi-Oi-Mj的最短路径
  3. 对于给定的T,计算Mi-T的最短路径
  4. 最后是求旅行商问题。从S出发,经过所有的M,最后到T的旅行商问题

*特殊情况:不存在M,则S-T的最短距离为所求。

所以此题需要解决的问题就是,
两点距离的搜索+预处理数据+旅行商问题

两点间的距离

两点间的距离需要用BFS,而不是DFS。这题DFS会超时。(我一开始只想到了dfs,是我太菜了)

对于结点的全遍历,二者性能相当。
对于求最短路径,BFS快,BFS从起点开始一圈圈往外扩,但凡见到终点即结束遍历,大概率不会把结点全遍历完。但是对于DFS,必须每条路径都走一遍,经过所有路径长度比较后,才敢确定最短的那条,所以,DFS一定要遍历完所有结点,时间当然长了。

预处理数据

  1. 转换矩阵中的数据到一个list中。机关Ms,巨石Os,起点startPoint,终点targetPoint
  2. 先计算好每个Mi每个Oj的距离m2o(如果每次都去计算Mi到Oj的距离会超时)
  3. 计算S->某个O->Mi的距离s2m
  4. 计算Mi->某个O->Mj的距离m2m
  5. 计算Mi->T的距离m2t

旅行商问题

可以用回溯(排列树)或者dp的方法解决。这里我用了dp的方法。

  1. 首先想到,需要枚举2^m种可能性(m为机关M的个数)。
  2. dp[i][j]表示可能性i的情况下,以Mj为最后到达点的,最短路径长度。

注:例如:i 为 0000 、 0001 、 0010 …二进制枚举一共2^4种可能。

  1. i为0001,0010,0100,1000时,即i为2^k时,其值为dp[2^k][j] = s2m[j]。
  2. 剩下的考虑一下状态转移的方法。dp[i][j](Mj应该是在i中)去掉其中一个k(Mk在i中)后,再从Mk到Mj。此时,就枚举了该状态下,以Mj结尾的,Mk到Mj的最短路径。dp[i][j] = dp[i xor 2^k][k] + m2m[k][j]。

d p [ i ] [ j ] = { s 2 m [ j ] , i = 2 k ( 0 < = k < m ) m i n 0 < = k < m   & &   k ! = j { d p [ i   x o r   2 j ] [ k ] + m 2 m [ k ] [ j ] } , 0 < i < 2 m dp[i][j]=\begin{cases} s2m[j] &, & {i=2^k (0<=k<m)}\\ min_{0<=k<m\ \&\&\ k!=j}\left\{dp[i\ xor \ 2^j][k]+m2m[k][j] \right\} & , & {0<i<2^m} \end{cases} dp[i][j]={s2m[j]min0<=k<m && k!=j{dp[i xor 2j][k]+m2m[k][j]},,i=2k(0<=k<m)0<i<2m

代码

	class Solution {
        //最短路径是怎么样的?
        /*
        1. 起点到石堆S->O
        2. 石堆到其中一个机关O->M
        3. 机关到其中一个石堆M->O
        3. 重复(2)(3)中的步骤,直到剩下最后一个M
        5. 从最后一个M走到终点T。M->T
        */
        //更进一步
        /*
        1. 对于给定的Mi,计算S-Oi-Mi的最短路径
        2. 对于给定的Mj,计算Mi-Oi-Mj的最短路径
        3. 对于给定的T,计算Mi-T的最短路径
        *特殊情况,不存在M,则S-T的最短距离为所求。
        4. 求旅行商问题。从S出发,经过所有的M,最后到T的旅行商问题
        */
        char[][] maze;
        class Point{
    		int x;
    		int y;
            Point(){}
    		Point(int x,int y){
    			this.x = x;
    			this.y = y;
    		}
    	}
    	//主函数
        public int minimalSteps(String[] maze) {
            this.maze = new char[maze.length][maze[0].length()];
            //一维数组转换成二维数组
            for(int i=0;i<maze.length;i++){
                for(int j=0;j<maze[0].length();j++){
                    this.maze[i][j] = maze[i].charAt(j);
                }
            }
            //开始点
            Point startPoint = new Point();
            //结束点
            Point targetPoint = new Point(); 
            //机关点
            List<Point> Ms = new ArrayList();
            //石头点
            List<Point> Os = new ArrayList();
            
            //遍历所有点,记录需要的点
            for(int i=0;i<this.maze.length;i++){
                for(int j=0;j<this.maze[0].length;j++){
                    if(this.maze[i][j] == 'S'){
                        startPoint.x = i;
                        startPoint.y = j;
                    }
                    if(this.maze[i][j] == 'T'){
                        targetPoint.x = i;
                        targetPoint.y = j;
                    }
                    if(this.maze[i][j] == 'M'){
                        Ms.add(new Point(i,j));
                    }
                    if(this.maze[i][j] == 'O'){
                        Os.add(new Point(i,j));
                    }
                }
            }
            //特殊情况处理
            //如果没有机关,直接起点到终点
            if(Ms.size() == 0){
                int ansSP = bfs(startPoint,targetPoint);
                if(ansSP>=10000){
                    ansSP = -1;
                }
                return ansSP;
            }

            //--------下面开始计算
            //预处理一下,先计算好每个Mi到O的距离
            int[][] m2o = new int[Ms.size()][Os.size()];
            for(int i=0;i<m2o.length;i++) {
            	for(int j=0;j<m2o[0].length;j++) {
            		m2o[i][j] = bfs(Ms.get(i),Os.get(j));
            	}
            }
            
            //第一部分,对于给定的Mi,计算S-Oi-Mi的最短路径
            int[] s2m = new int[Ms.size()];
            //初始化s2m
            for(int i=0;i<s2m.length;i++){
                s2m[i] = 10000;
            }

            //对于每个M
            for(int i=0;i<Ms.size();i++){
                //遍历每个O
                for(int j=0;j<Os.size();j++){
                    //取S->某个O->Mi的最短距离
                	s2m[i] = Math.min(s2m[i], bfs(startPoint,Os.get(j))+m2o[i][j]);
                }
            }
            //第二部分,对于给定的Mj,计算Mi-Oi-Mj的最短路径
            int[][] m2m = new int[Ms.size()][Ms.size()];
            //初始化m2m
            for(int i=0;i<m2m.length;i++){
                for(int j=0;j<m2m[0].length;j++){
                    m2m[i][j] = 10000;
                }
            }

            //对于每个Mi
            for(int i=0;i<Ms.size();i++){
                //到另外的Mj
                for(int j=0;j<Ms.size();j++){
                    //遍历每个Ok
                    for(int k=0;k<Os.size();k++){
                        //取Mi->某个O->Mj的最短距离
                    	m2m[i][j] = Math.min(m2m[i][j],m2o[i][k]+m2o[j][k]);
                    }
                }
            }
            //第三部分,对于给定的T,计算Mi-T的最短路径
            int[] m2t = new int[Ms.size()];
            for(int i=0;i<Ms.size();i++){
                //每个Mi到T的最短距离
            	m2t[i] = bfs(Ms.get(i),targetPoint);
            }
            //第四部分,从S出发,遍历每一个M,最后到T的旅行商问题
            //dp或回溯(排列树)
            /*
            枚举2^n种可能性
            在i的机关触发形式中,以最后一步到Mj的最短步数
            m为机关M的个数
            dp[i][j] = min(0<=k<m,k在i中,k!=j){dp[i^jmask][k]+m2m[k][j]}
             */
            int[][] dp = new int[(int)Math.pow(2,Ms.size())][Ms.size()];
            for(int i=0;i<dp.length;i++){
                for(int j=0;j<dp[0].length;j++){
                    dp[i][j] = 10000;
                }
            }
            
            for(int i=0;i<dp.length;i++) {
            	if(i==0) {
            		continue;
            	}
            	for(int j=0;j<dp[0].length;j++) {
            		if(i==j) {
            			continue;
            		}
            		int jmask = (int) Math.pow(2, j);
            		if(jmask == i) {
        				dp[i][j] = s2m[j];
        				continue;
        			}
            		//j需要在i中(机关已经被触发)
            		if((jmask&i) == 0) {
            			continue;
            		}
            		//枚举每个机关
            		for(int k=0;k<Ms.size();k++) {
            			//需要去掉的机关和枚举的机关不得重复
            			if(j==k) {
            				continue;
            			}
            			//制作mask
            			int kmask = (int) Math.pow(2, k);
            			//在机关k在i中
            			if((kmask&i) !=0 ) {
            				//在i中去掉其中一个j,从k到j的最短步数
            				dp[i][j] = Math.min(dp[i][j],dp[i^jmask][k]+m2m[k][j]);
            			}
            		}
            	}
            }
            
            
            int ans = 10000;
            for(int i=0;i<dp[0].length;i++) {
                ans = Math.min(ans,dp[dp.length-1][i]+m2t[i]);
            }
            return ans>=10000?-1:ans;
        }

        //dfs(不用这个)
        //对于给定的两个点,中间存在障碍物,求P1到P2的最短距离
        //i,j为当前的点.x,y目标点
        public int dfs(int i,int j,int[][] flag,int x,int y){
            //如果越界,或者该点为障碍物,或者该点已经走过,说明该方向无法到达目标点。置一个【大】值。
            if(i<0 || j<0 || i>=maze.length || j >= maze[0].length || maze[i][j] == '#' || flag[i][j] == 1){
                return 10000;
            }
            
            flag[i][j] = 1;
            
            //如果该点就是终点
            if(i == x && j == y){
            	flag[i][j] = 0;
                return 0;
            }

            int up = 0;
            int down = 0;
            int left = 0;
            int right = 0;
            //进行bfs+记忆的方式
            //四个方向-上下左右
            //上
            up = dfs(i-1,j,flag,x,y);
            //下
            down = dfs(i+1,j,flag,x,y);
            //左
            left = dfs(i,j-1,flag,x,y);
            //右
            right = dfs(i,j+1,flag,x,y);
            
            flag[i][j] = 0;
            
            //取四个方向中最短的长度
            int minLength = Math.min(Math.min(up,down),Math.min(left,right));
            
            return minLength+1;
        }
        
        //bfs
        private int bfs(Point from, Point to) {
        	int[][] dist = new int[150][150];;
        	Queue<Point> queue = new LinkedList<>();;
        	int[] dir = {-1, 0, 1, 0, -1};  // 压缩方向数组,二维变一维, {-1,0},{0,1},{1,0},{0,-1}
            // 特判: 如果是墙壁, 返回 -1
            if (maze[from.x][from.y] == '#') {
                return 10000;
            }
            // 初始化 dist 数组
            for (int[] a : dist) {
                Arrays.fill(a, -1);
            }
            queue.offer(from);
            dist[from.x][from.y] = 0;

            while (!queue.isEmpty()) {
                Point cur = queue.poll();
                int x = cur.x;
                int y = cur.y;
                for (int i = 0; i < 4; i++) {
                    int nx = x + dir[i];
                    int ny = y + dir[i + 1];
                    if (nx < 0 || nx >= maze.length || ny < 0 || ny >= maze[0].length || maze[nx][ny] == '#') continue;
                    if (dist[nx][ny] == -1) {
                        dist[nx][ny] = dist[x][y] + 1;
                        queue.offer(new Point(nx, ny));
                    }
                }
            }
            return dist[to.x][to.y]==-1?10000:dist[to.x][to.y];
        }
    }

算法复杂度分析

(来自leetcode官方)

假设迷宫的面积为 s,M 的数量为 m,O 的数量为 o。

时间复杂度:O(ms + m2o + 2mm2)。单次 BFS 的时间代价为 O(s),m 次 BFS 的时间代价为 O(ms);预处理任意两个 M 经过 O 的最短距离的时间代价是 O(m2o);动态规划的时间代价是 O(2mm2)
空间复杂度:O(s + bs + 2mm)。BFS 队列的空间代价是 O(s);预处理 Mi到各个点的最短距离的空间代价是 O(bs);动态规划数组的空间代价是 O(2mm)。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
### 回答1: 好的,我来用中文回复这个链接:https://leetcode-cn.com/tag/dynamic-programming/ 这个链接是 LeetCode 上关于动态规划的题目集合。动态规划是一种常用的算法思想,可以用来解决很多实际问题,比如最长公共子序列、背包问题、最短路径等等。在 LeetCode 上,动态规划也是一个非常重要的题型,很多题目都需要用到动态规划的思想来解决。 这个链接里包含了很多关于动态规划的题目,按照难度从简单到困难排列。每个题目都有详细的题目描述、输入输出样例、题目解析和代码实现等内容,非常适合想要学习动态规划算法的人来练习和提高自己的能力。 总之,这个链接是一个非常好的学习动态规划算法的资源,建议大家多多利用。 ### 回答2: 动态规划是一种算法思想,通常用于优化具有重叠子问题和最优子结构性质的问题。由于其成熟的数学理论和强大的实用效果,动态规划在计算机科学、数学、经济学、管理学等领域均有重要应用。 在计算机科学领域,动态规划常用于解决最优化问题,如背包问题、图像处理、语音识别、自然语言处理等。同时,在计算机网络和分布式系统中,动态规划也广泛应用于各种优化算法中,如链路优化、路由算法、网络流量控制等。 对于算法领域的程序员而言,动态规划是一种必要的技能和知识点。在LeetCode这样的程序员平台上,题目分类和标签设置十分细致和方便,方便程序员查找并深入学习不同类型的算法LeetCode动态规划标签下的题目涵盖了各种难度级别和场景的问题。从简单的斐波那契数列、迷宫问题到可以用于实际应用的背包问题、最长公共子序列等,难度不断递进且话题丰富,有助于开发人员掌握动态规划的实际应用技能和抽象思维模式。 因此,深入LeetCode动态规划分类下的题目学习和练习,对于程序员的职业发展和技能提升有着重要的意义。 ### 回答3: 动态规划是一种常见的算法思想,它通过将问题拆分成子问题的方式进行求解。在LeetCode中,动态规划标签涵盖了众多经典和优美的算法问题,例如斐波那契数列、矩阵链乘法、背包问题等。 动态规划的核心思想是“记忆化搜索”,即将中间状态保存下来,避免重复计算。通常情况下,我们会使用一张二维表来记录状态转移过程中的中间值,例如动态规划求解斐波那契数列问题时,就可以定义一个二维数组f[i][j],代表第i项斐波那契数列中,第j个元素的值。 在LeetCode中,动态规划标签下有众多难度不同的问题。例如,经典的“爬楼梯”问题,要求我们计算到n级楼梯的方案数。这个问题的解法非常简单,只需要维护一个长度为n的数组,记录到达每一级楼梯的方案数即可。类似的问题还有“零钱兑换”、“乘积最大子数组”、“通配符匹配”等,它们都采用了类似的动态规划思想,通过拆分问题、保存中间状态来求解问题。 需要注意的是,动态规划算法并不是万能的,它虽然可以处理众多经典问题,但在某些场景下并不适用。例如,某些问题的状态转移过程比较复杂,或者状态转移方程中存在多个参数,这些情况下使用动态规划算法可能会变得比较麻烦。此外,动态规划算法也存在一些常见误区,例如错用贪心思想、未考虑边界情况等。 总之,掌握动态规划算法对于LeetCode的学习和解题都非常重要。除了刷题以外,我们还可以通过阅读经典的动态规划书籍,例如《算法竞赛进阶指南》、《算法与数据结构基础》等,来深入理解这种算法思想。
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值