LC1345跳跃游戏IV:单向BFS

前言

  • 大家好,我是新人简书博主:「 个人主页」主要分享程序员生活、编程技术、* * 以及每日的LeetCode刷题记录,欢迎大家关注我,一起学习交流,谢谢!
  • 正在坚持每日更新LeetCode每日一题,发布的题解有些会参考其他大佬的思路(参考资料的链接会放在最下面),欢迎大家关注我 ~ ~ ~
  • 同时也在进行其他专项类型题目的刷题与题解活动,相关资料也会同步到「GitHub」上面 ~
  • 今天是坚持写题解的25天(haha,从21年圣诞节开始的),大家一起加油!

  • 每日一题:LeetCode:1345.跳跃游戏IV
    • 时间:2022-01-21
    • 力扣难度:Hard
    • 个人难度:Hard
    • 数据结构:数组、图论
    • 算法:BFS、双向BFS

2022-01-21:LeetCode:1345.跳跃游戏IV

1. 题目描述

  • 题目:原题链接

    • 给你一个整数数组 arr ,你一开始在数组的第一个元素处(下标为 0)。

    • 每一步,你可以从下标 i 跳到下标:

      • i + 1 满足:i + 1 < arr.length
      • i - 1 满足:i - 1 >= 0
      • j 满足:arr[i] == arr[j] 且 i != j
    • 请你返回到达数组最后一个元素的下标处所需的 最少操作次数 。

    • 注意:任何时候你都不能跳到数组外面。

    • 1 <= arr.length <= 5 * 10^4

    • -10^8 <= arr[i] <= 10^8

  • 输入输出规范

    • 输入:数组 arr
    • 输出:最小步数
  • 输入输出示例

    • 输入:arr = [100,-23,-23,404,100,23,23,23,3,404]
    • 输出:3

3. 方法一:单向BFS

  • 思路

    • 本题是求解给定的数组中,按照一定的规则到达末尾的最小步数,即最短路径问题,可以根据元素跳跃的规则来抽象成一个无向图
    • 本题的无向图:图的顶点是数组中的元素,就表示两个元素能否直接跳跃过去,是无向边,且权值都为1,属于无权图
    • 通过将数组抽象为无向图,就将求到达末尾的最小步数转化为最短路径问题,可以使用BFS来解决,暴力BFS的话需要遍历所有顶点和边,因为本题等值的顶点间也存在边,所以时间复杂度约为 O(n^2),暴力BFS会TLE超时
    • 因此,需要对BFS的过程进行一定的优化,我们发现,对于等值顶点构成的子图,其实在遍历其中一个顶点时,就将整个子图访问一次,之后清空整个子图,从而避免重复访问该子图
  • 解题步骤

    • 首先,使用哈希表保存图的顶点和边,即key为元素的值,value 记录的是索引
    • 然后,将首个顶点入队,开始BFS搜素,当搜索到某个顶点时,将其连接的其他顶点入队
    • 搜索中,分为三种情况,向前跳、向后跳和等值跳,分类讨论,将此时有边连接的顶点和此时的步数更新入队列
    • 为了避免重复访问顶点,需要标记每个顶点是否访问过(布尔数组),如果已经访问过就不进行入队操作,因为访问过的节点能否直接到达末尾已经求解过了
    • 此外,为了避免重复访问子图,还需要在第一次添加等值子图入队后,将该元素从哈希表中删除,从而后续搜索中不会在将该子图加入队列
    • 注意:队列中存放一个长度为2的数组,分别表示当前索引和步数;也可以队列中只放索引,步数通过维护一个和元素数组一样长的数组来表示,同时还可以用该数组表示当前元素是否访问过(代替布尔数组)
  • 图解


  • 题解

    // 方法1 : 单向BFS
    public int minJumps(int[] arr) {
        if (arr == null || arr.length == 0) return -1;
        int n = arr.length;
        if (n == 1) return 0;
        Map<Integer, List<Integer>> indexMap = new HashMap<>();
        // 哈希表存 value 和 index
        for (int i = 0; i < n; i++) {
            List<Integer> list = indexMap.getOrDefault(arr[i], new ArrayList<>());
            list.add(i);
            indexMap.put(arr[i], list);
        }
        boolean[] visited = new boolean[n];
        Deque<int[]> deque = new LinkedList<>(); // int[] {index, step}
        deque.add(new int[]{0, 0});
        while (!deque.isEmpty()) {
            int[] cur = deque.poll();
            int index = cur[0];
            int step = cur[1];
            // 搜到了末尾
            if (index == n - 1) return step;
            // 前后跳
            if (index + 1 < n && !visited[index + 1]) {
                deque.add(new int[]{index + 1, step + 1});
                visited[index + 1] = true;
            }
            if (index - 1 >= 0 && !visited[index - 1]) {
                deque.add(new int[]{index - 1, step + 1});
                visited[index - 1] = true;
            }
            // 等值跳
            if (indexMap.containsKey(arr[index])) {
                List<Integer> indexList = indexMap.get(arr[index]);
                for (int idx : indexList) {
                    if(!visited[idx]) {
                        deque.add(new int[]{idx, step + 1});
                        visited[idx] = true;
                    }
                }
            }
            // 注意从哈希表中删除访问过的元素
            indexMap.remove(arr[index]);
        }
    }
    return -1;
    }
    
  • 复杂度分析:n 是数组的大小

    • 时间复杂度:O(n),通过拒绝访问已经访问过的顶点和等值子图,从平方级优化到线性
    • 空间复杂度:O(n)

2. 方法二:双向BFS

  • 思路

    • 本题如果使用单向BFS暴搜的话,复杂度约为 O(n^2),会造成超时问题,通过额外维护表示顶点是否访问过的数组,并且将访问过的元素从哈希表中进行删除,可以优化到线性复杂度
    • 本题同样可以使用双向BFS来进行搜索,双向BFS主要是用来解决单向BFS中搜索空间爆炸的问题,即从开头和结尾一起开始搜索,当搜索到相同的值时,意味着找到了一条联通起点和终点的最短路径
  • 解题步骤

    • 首先,为了从开头和结尾一起开始搜索,需要两个队列
    • 其次,为了使的两个搜索方向尽可能平均,每次从元素较少的队列中取值搜索,进行扩展,加入新的顶点到队列中(update)
    • 在搜索过程中,如果搜索到了相同的值,包括对方已经搜索过的值,就表示起点到终点的最短路径
    • 对于本题,搜索过程仍然分三种情况讨论,只是可以省略哈希表的删除操作
  • 题解

    // 方法2 : 双向BFS
    public int minJumps(int[] arr) {
        if (arr == null || arr.length == 0) return -1;
        int n = arr.length;
        if (n == 1) return 0;
        Map<Integer, List<Integer>> indexMap = new HashMap<>();
        // 哈希表存 value 和 index
        for (int i = 0; i < n; i++) {
            List<Integer> list = indexMap.getOrDefault(arr[i], new ArrayList<>());
            list.add(i);
            indexMap.put(arr[i], list);
        }
    
        int[] step1 = new int[n];
        int[] step2 = new int[n];
        Arrays.fill(step1, -1);
        Arrays.fill(step2, -1);
    
        Deque<Integer> deque1 = new LinkedList<>(); //  index
        Deque<Integer> deque2 = new LinkedList<>();
        deque1.add(0);
        step1[0] = 0;
        deque2.add(n - 1);
        step2[n - 1] = 0;
    
        while (!deque1.isEmpty() && !deque2.isEmpty()) {
            int step = -1;
            if (deque1.size() <= deque2.size()) {
                step = update(deque1, deque2, step1, step2, indexMap, arr);
            } else {
                step = update(deque2, deque1, step2, step1, indexMap, arr);
            }
            if (step != -1) return step;
        }
        return -1;
    }
    
    private int update(Deque<Integer> deque1, Deque<Integer> deque2, int[] step1, int[] step2, Map<Integer, List<Integer>> indexMap, int[] arr) {
        int size = deque1.size();
        int n = arr.length;
        while (size > 0) {
            int index = deque1.poll();
            int step = step1[index];
            // 前后跳
            if (index + 1 < n) {
                if (step2[index + 1] != -1) return step + 1 + step2[index + 1];
                if (step1[index + 1] == -1) {
                    deque1.add(index + 1);
                    step1[index + 1] = step + 1;
                }
            }
            if (index - 1 >= 0) {
                if (step2[index - 1] != -1) return step + 1 + step2[index - 1];
                if (step1[index - 1] == -1) {
                    deque1.add(index - 1);
                    step1[index - 1] = step + 1;
                }
            }
            // 等值跳
            if (indexMap.containsKey(arr[index])) {
                List<Integer> indexList = indexMap.get(arr[index]);
                for (int idx : indexList) {
                    if (step2[idx] != -1) return step + 1 + step2[idx];
                    if (step1[idx] == -1) {
                        deque1.add(idx);
                        step1[idx] = step + 1;
                    }
                }
            }
            // 注意从哈希表中删除访问过的元素
            indexMap.remove(arr[index]);
            size--;
        }
        return -1;
    }
    
  • 复杂度分析:n 是数组的大小

    • 时间复杂度:O(n)
    • 空间复杂度:O(n)

最后

如果本文有所帮助的话,欢迎大家可以给个三连「点赞」&「收藏」&「关注」 ~ ~ ~
也希望大家有空的时候光临我的其他平台,上面会更新Java面经、八股文、刷题记录等等,欢迎大家光临交流,谢谢!


©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 196,099评论 5 462
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 82,473评论 2 373
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 143,229评论 0 325
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,570评论 1 267
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,427评论 5 358
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,335评论 1 273
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,737评论 3 386
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,392评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,693评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,730评论 2 312
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,512评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,349评论 3 314
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,750评论 3 299
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,017评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,290评论 1 251
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,706评论 2 342
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,904评论 2 335

推荐阅读更多精彩内容

  • 算法口试也就是用自然语言描述算法,脑海中要有一个流程图。 【目录】考点一:循环考点二:递归考点三:排序考点四:查找...
    三金姐姐阅读 642评论 -1 2
  • 一、上节课回顾 1、常用类 A:8种基本类型的包装类: B:Object类:toString(),equals()...
    机会留给有准备的人阅读 178评论 0 0
  • to-do:看一下别人写的题解 https://github.com/981377660LMT/algorithm...
    winter_sweetie阅读 731评论 1 0
  • 一、泛型Generics JDK1.5之后出现的。 1.概念:广泛的类型——>声明要存储的类型是什么。 2.作用:...
    麦小玮阅读 412评论 0 0
  • 2.1、泛型Generics 概念:广泛的类型——>声明要存储的类型是什么。 作用:存入到容器中的元素,Objec...
    Hoffnung_8164阅读 200评论 0 0