2966.划分数组并满足最大差限制

目标

给你一个长度为 n 的整数数组 nums,以及一个正整数 k 。

将这个数组划分为 n / 3 个长度为 3 的子数组,并满足以下条件:

  • 子数组中 任意 两个元素的差必须 小于或等于 k 。

返回一个 二维数组 ,包含所有的子数组。如果不可能满足条件,就返回一个空数组。如果有多个答案,返回 任意一个 即可。

示例 1:

输入:nums = [1,3,4,8,7,9,3,5,1], k = 2
输出:[[1,1,3],[3,4,5],[7,8,9]]
解释:
每个数组中任何两个元素之间的差小于或等于 2。

示例 2:

输入:nums = [2,4,2,2,5,2], k = 2
输出:[]
解释:
将 nums 划分为 2 个长度为 3 的数组的不同方式有:
[[2,2,2],[2,4,5]] (及其排列)
[[2,2,4],[2,2,5]] (及其排列)
因为有四个 2,所以无论我们如何划分,都会有一个包含元素 2 和 5 的数组。因为 5 - 2 = 3 > k,条件无法被满足,所以没有合法的划分。

示例 3:

输入:nums = [4,2,9,8,2,12,7,12,10,5,8,5,5,7,9,2,5,11], k = 14
输出:[[2,2,12],[4,8,5],[5,9,7],[7,8,5],[5,9,10],[11,12,2]]
解释:
每个数组中任何两个元素之间的差小于或等于 14。

说明:

  • n == nums.length
  • 1 <= n <= 10^5
  • n 是 3 的倍数
  • 1 <= nums[i] <= 10^5
  • 1 <= k <= 10^5

思路

将数组划分为 n / 3 个长度为 3 的数组,使得子数组内元素的差值不超过 k。如果有多个答案返回任意一个即可,如果满足条件的子数组不够 n / 3,返回空数组。

注意将原数组划分为 n / 3 个数组,先从数组中取 3 个元素,再从剩余元素中取 3 个 …… 。并不是取不重复的长度为 3 的子数组。

排序后每三个元素一组判断即可,因为相邻元素的差值最小,只要有一组不满足条件,那么个数就不够,直接返回空数组。

代码


/**
 * @date 2025-06-18 0:08
 */
public class DivideArray2966 {

    public int[][] divideArray(int[] nums, int k) {
        Arrays.sort(nums);
        int n = nums.length;
        int[][] res = new int[n / 3][];
        for (int i = 1; i < n - 1; i += 3) {
            int cur = nums[i];
            int prev = nums[i - 1];
            int next = nums[i + 1];
            if (next - prev > k) {
                return new int[][]{};
            }
            res[i / 3] = new int[]{prev, cur, next};
        }
        return res;
    }

}

性能

1432.改变一个整数能得到的最大差值

目标

给你一个整数 num 。你可以对它进行以下步骤共计 两次:

  • 选择一个数字 x (0 <= x <= 9).
  • 选择另一个数字 y (0 <= y <= 9) 。数字 y 可以等于 x 。
  • 将 num 中所有出现 x 的数位都用 y 替换。

令两次对 num 的操作得到的结果分别为 a 和 b 。

请你返回 a 和 b 的 最大差值 。

注意,新的整数(a 或 b)必须不能 含有前导 0,并且 非 0。

示例 1:

  • 输入:num = 555
  • 输出:888
  • 解释:第一次选择 x = 5 且 y = 9 ,并把得到的新数字保存在 a 中。
  • 第二次选择 x = 5 且 y = 1 ,并把得到的新数字保存在 b 中。
  • 现在,我们有 a = 999 和 b = 111 ,最大差值为 888

示例 2:

  • 输入:num = 9
  • 输出:8
  • 解释:第一次选择 x = 9 且 y = 9 ,并把得到的新数字保存在 a 中。
  • 第二次选择 x = 9 且 y = 1 ,并把得到的新数字保存在 b 中。
  • 现在,我们有 a = 9 和 b = 1 ,最大差值为 8

示例 3:

  • 输入:num = 123456
  • 输出:820000

示例 4:

  • 输入:num = 10000
  • 输出:80000

示例 5:

  • 输入:num = 9288
  • 输出:8700

说明:

  • 1 <= num <= 10^8

思路

num 中选择一个数字 d,可以将 num 中所有的 d 都替换成另一个数字(不允许包含前导零),返回能够得到的最大值与最小值的差。

2566.替换一个数字后的最大差值 的区别是不能有前导零。

最大值:找到第一个不是 9 的位置,将 num 中所有与之相同的数字替换为 9

最小值:找到第一个不是 1 的位置,如果是首位,将 num 中所有与之相同的数字替换为 1,否则替换为 0

代码


/**
 * @date 2025-06-15 18:15
 */
public class MaxDiff1432 {

    public int maxDiff(int num) {
        int a = num, b = num;
        String s = String.valueOf(num);
        int n = s.length();
        int m = (int) Math.pow(10, n - 1);
        int ar = '-', br = '-';
        boolean first = false;
        for (int i = 0; i < n; i++) {
            char c = s.charAt(i);
            if (ar == '-' && c != '9') {
                ar = c;
            }
            if (br == '-' && c > '1') {
                br = c;
                first = i == 0;
            }
            if (ar == c) {
                a += (9 - (c - '0')) * m;
            }
            if (br == c) {
                b -= ((c - '0') - (first ? 1 : 0)) * m;
            }
            m /= 10;
        }
        return a - b;
    }
}

性能

386.字典序排数

目标

给你一个整数 n ,按字典序返回范围 [1, n] 内所有整数。

你必须设计一个时间复杂度为 O(n) 且使用 O(1) 额外空间的算法。

示例 1:

输入:n = 13
输出:[1,10,11,12,13,2,3,4,5,6,7,8,9]

示例 2:

输入:n = 2
输出:[1,2]

说明:

  • 1 <= n <= 5 * 10^4

思路

1 ~ n 的所有整数按照字典序排序,要求时间复杂度为 O(n),空间复杂度为 O(1)

想象遍历一颗字典树,优先扩展(乘以 10),如果超过 n,则优先将个位数加一(遍历兄弟节点),如果需要进位,则返回父节点(除以10)并将个位数加一(父节点的兄弟节点),接着执行同样的逻辑,扩展,个位数加一,回退。

代码


/**
 * @date 2025-06-08 18:15
 */
public class LexicalOrder386 {

    public List<Integer> lexicalOrder(int n) {
        List<Integer> res = new ArrayList<>();
        for (int i = 0, j = 1; i < n; i++) {
            res.add(j);
            if (j * 10 <= n) {
                j *= 10;
            } else {
                while (j % 10 == 9 || j >= n) {
                    j /= 10;
                }
                j++;
            }
        }
        return res;
    }

}

性能

3170.删除星号以后字典序最小的字符串

目标

给你一个字符串 s 。它可能包含任意数量的 '' 字符。你的任务是删除所有的 '' 字符。

当字符串还存在至少一个 '*' 字符时,你可以执行以下操作:

  • 删除最左边的 '*' 字符,同时删除该星号字符左边一个字典序 最小 的字符。如果有多个字典序最小的字符,你可以删除它们中的任意一个。

请你返回删除所有 '*' 字符以后,剩余字符连接而成的 字典序最小 的字符串。

示例 1:

输入:s = "aaba*"
输出:"aab"
解释:
删除 '*' 号和它左边的其中一个 'a' 字符。如果我们选择删除 s[3] ,s 字典序最小。

示例 2:

输入:s = "abc"
输出:"abc"
解释:
字符串中没有 '*' 字符。

说明:

  • 1 <= s.length <= 10^5
  • s 只含有小写英文字母和 '*' 字符。
  • 输入保证操作可以删除所有的 '*' 字符。

思路

有一个包含任意数量 * 的字符串,每次操作可以删掉 * 以及它左侧的一个字典序最小的字符,如果有多个可以,删除任意一个。求删除所有 * 之后能够得到的 字典序最小的字符串。

贪心算法,优先删除左侧字典序最小且下标最大的字符即可。

代码


/**
 * @date 2025-06-07 9:37
 */
public class ClearStars3170 {

    public String clearStars(String s) {
        PriorityQueue<int[]> q = new PriorityQueue<>((a, b) -> {
            int compare = a[0] - b[0];
            if (compare != 0) {
                return compare;
            }
            return b[1] - a[1];
        });
        char[] chars = s.toCharArray();
        int n = chars.length;
        for (int i = 0; i < n; i++) {
            char c = chars[i];
            if (c != '*') {
                q.offer(new int[]{c, i});
            } else {
                q.poll();
            }
        }
        char[] res = new char[q.size()];
        PriorityQueue<int[]> tmp = new PriorityQueue<>((a, b) -> a[1] - b[1]);
        tmp.addAll(q);
        int i = 0;
        while (!tmp.isEmpty()) {
            int[] c = tmp.poll();
            res[i++] = (char) c[0];
        }
        return new String(res);
    }
}

性能

2434.使用机器人打印字典序最小的字符串

目标

给你一个字符串 s 和一个机器人,机器人当前有一个空字符串 t 。执行以下操作之一,直到 s 和 t 都变成空字符串:

  • 删除字符串 s 的 第一个 字符,并将该字符给机器人。机器人把这个字符添加到 t 的尾部。
  • 删除字符串 t 的 最后一个 字符,并将该字符给机器人。机器人将该字符写到纸上。

请你返回纸上能写出的字典序最小的字符串。

示例 1:

输入:s = "zza"
输出:"azz"
解释:用 p 表示写出来的字符串。
一开始,p="" ,s="zza" ,t="" 。
执行第一个操作三次,得到 p="" ,s="" ,t="zza" 。
执行第二个操作三次,得到 p="azz" ,s="" ,t="" 。

示例 2:

输入:s = "bac"
输出:"abc"
解释:用 p 表示写出来的字符串。
执行第一个操作两次,得到 p="" ,s="c" ,t="ba" 。
执行第二个操作两次,得到 p="ab" ,s="c" ,t="" 。
执行第一个操作,得到 p="ab" ,s="" ,t="c" 。
执行第二个操作,得到 p="abc" ,s="" ,t="" 。

示例 3:

输入:s = "bdda"
输出:"addb"
解释:用 p 表示写出来的字符串。
一开始,p="" ,s="bdda" ,t="" 。
执行第一个操作四次,得到 p="" ,s="" ,t="bdda" 。
执行第二个操作四次,得到 p="addb" ,s="" ,t="" 。

说明:

  • 1 <= s.length <= 10^5
  • s 只包含小写英文字母。

思路

  • 首先统计字符串中的字符个数,要使输出的字典序最小,那么第一个字符一定是字符串中出现过的字典序最小的字符
  • 为了将该字符打印到首位,需要先定位到这个字符,它前面的字符都会被暂存到栈中
  • 确定了第一个字典序最小的字符之后,下一个字符有两个选择,取栈顶字符,或者找到剩下的字符中的字典序最小的

需要维护剩余字符中最小字典序的字符,可以使用有序集合或者后缀。

代码


/**
 * @date 2025-06-06 8:47
 */
public class RobotWithString2434 {

    public String robotWithString(String s) {
        TreeMap<Character, Integer> map = new TreeMap<>();
        char[] chars = s.toCharArray();
        int n = chars.length;
        for (char c : chars) {
            map.merge(c, 1, Integer::sum);
        }
        ArrayDeque<Character> q = new ArrayDeque<>();
        StringBuilder sb = new StringBuilder();
        int i = 0;
        while (i < n) {
            Character min = map.firstKey();
            while (i < n && chars[i] != min) {
                char c = chars[i++];
                q.offer(c);
                map.merge(c, -1, Integer::sum);
                if (map.get(c) == 0) {
                    map.remove(c);
                }
            }
            sb.append(min);
            map.merge(min, -1, Integer::sum);
            if (map.get(min) == 0) {
                map.remove(min);
            }
            i++;
            while (!q.isEmpty() && (map.size() == 0 || q.peekLast() <= map.firstKey())) {
                sb.append(q.pollLast());
            }
        }
        return sb.toString();
    }

}

性能

1061.按字典序排列最小的等效字符串

目标

给出长度相同的两个字符串s1 和 s2 ,还有一个字符串 baseStr 。

其中 s1[i] 和 s2[i] 是一组等价字符。

  • 举个例子,如果 s1 = "abc" 且 s2 = "cde",那么就有 'a' == 'c', 'b' == 'd', 'c' == 'e'。

等价字符遵循任何等价关系的一般规则:

  • 自反性 :'a' == 'a'
  • 对称性 :'a' == 'b' 则必定有 'b' == 'a'
  • 传递性 :'a' == 'b' 且 'b' == 'c' 就表明 'a' == 'c'

例如, s1 = "abc" 和 s2 = "cde" 的等价信息和之前的例子一样,那么 baseStr = "eed" , "acd" 或 "aab",这三个字符串都是等价的,而 "aab" 是 baseStr 的按字典序最小的等价字符串

利用 s1 和 s2 的等价信息,找出并返回 baseStr 的按字典序排列最小的等价字符串。

示例 1:

输入:s1 = "parker", s2 = "morris", baseStr = "parser"
输出:"makkek"
解释:根据 A 和 B 中的等价信息,我们可以将这些字符分为 [m,p], [a,o], [k,r,s], [e,i] 共 4 组。每组中的字符都是等价的,并按字典序排列。所以答案是 "makkek"。

示例 2:

输入:s1 = "hello", s2 = "world", baseStr = "hold"
输出:"hdld"
解释:根据 A 和 B 中的等价信息,我们可以将这些字符分为 [h,w], [d,e,o], [l,r] 共 3 组。所以只有 S 中的第二个字符 'o' 变成 'd',最后答案为 "hdld"。

示例 3:

输入:s1 = "leetcode", s2 = "programs", baseStr = "sourcecode"
输出:"aauaaaaada"
解释:我们可以把 A 和 B 中的等价字符分为 [a,o,e,r,s,c], [l,p], [g,t] 和 [d,m] 共 4 组,因此 S 中除了 'u' 和 'd' 之外的所有字母都转化成了 'a',最后答案为 "aauaaaaada"。

说明:

  • 1 <= s1.length, s2.length, baseStr <= 1000
  • s1.length == s2.length
  • 字符串s1, s2, and baseStr 仅由从 'a' 到 'z' 的小写英文字母组成。

思路

定义 s1[i]s2[i] 是等价字符,返回 baseStr 字典序最小的等价字符串。

可以使用并查集,用字典序小的字符代表等价字符,然后逐个替换 baseStr 即可。

代码


/**
 * @date 2025-06-05 0:15
 */
public class SmallestEquivalentString1061 {

    public class UnionFind {
        private int[] father;

        public UnionFind() {
            this.father = new int[26];
            Arrays.setAll(father, i -> i);
        }

        public void merge(int a, int b) {
            int x = find(a);
            int y = find(b);
            if (x == y) {
                return;
            }
            if (x < y) {
                father[y] = x;
            } else {
                father[x] = y;
            }
        }

        public int find(int a) {
            if (father[a] != a) {
                father[a] = find(father[a]);
            }
            return father[a];
        }
    }

    public String smallestEquivalentString(String s1, String s2, String baseStr) {
        int n = s1.length();
        UnionFind uf = new UnionFind();
        for (int i = 0; i < n; i++) {
            uf.merge(s1.charAt(i) - 'a', s2.charAt(i) - 'a');
        }
        StringBuilder sb = new StringBuilder();
        for (char c : baseStr.toCharArray()) {
            sb.append((char) ('a' + uf.find(c - 'a')));
        }
        return sb.toString();
    }

}

性能

3403.从盒子中找出字典序最大的字符串I

目标

给你一个字符串 word 和一个整数 numFriends。

Alice 正在为她的 numFriends 位朋友组织一个游戏。游戏分为多个回合,在每一回合中:

  • word 被分割成 numFriends 个 非空 字符串,且该分割方式与之前的任意回合所采用的都 不完全相同 。
  • 所有分割出的字符串都会被放入一个盒子中。

在所有回合结束后,找出盒子中 字典序最大的 字符串。

示例 1:

输入: word = "dbca", numFriends = 2
输出: "dbc"
解释: 
所有可能的分割方式为:
"d" 和 "bca"。
"db" 和 "ca"。
"dbc" 和 "a"。

示例 2:

输入: word = "gggg", numFriends = 4
输出: "g"
解释: 
唯一可能的分割方式为:"g", "g", "g", 和 "g"。

提示:

  • 1 <= word.length <= 5 * 10^3
  • word 仅由小写英文字母组成。
  • 1 <= numFriends <= word.length

思路

word 划分为 numFriends 个非空子串,求字典序最大的子串。

找字典序最大的首字母,如果存在多个需要比较后面字符的字典序,还要保证能够划分成非空子串。

先找到字符串中字典序最大的字符集合作为起点,将其后面的字符放入优先队列 [char, index],根据字符从大到小排序,取队首连续相同的字符,将其后一个字符放到下一轮处理。

也可以不用手动逐个比较字符,枚举字典序最大的字符作为左端点,然后尽可能地扩展字符串长度 n - numFriends + 1,将结果收集后排序即可。

代码


/**
 * @date 2025-06-04 9:19
 */
public class AnswerString3403 {

    public String answerString(String word, int numFriends) {
        if (numFriends == 1) {
            return word;
        }
        int n = word.length();
        List<Integer>[] chars = new ArrayList[26];
        Arrays.setAll(chars, x -> new ArrayList<>());
        for (int i = 0; i < n; i++) {
            char c = word.charAt(i);
            chars[c - 'a'].add(i);
        }
        int l = n - numFriends + 1;
        String[] strs = null;
        for (int i = 25; i >= 0; i--) {
            if (chars[i].size() > 0) {
                strs = new String[chars[i].size()];
                for (int j = 0; j < chars[i].size(); j++) {
                    int index = chars[i].get(j);
                    strs[j] = word.substring(index, Math.min(index + l, n));
                }
                break;
            }
        }
        Arrays.sort(strs);
        return strs[strs.length - 1];
    }

}

性能

2929.给小朋友们分糖果II

目标

给你两个正整数 n 和 limit 。

请你将 n 颗糖果分给 3 位小朋友,确保没有任何小朋友得到超过 limit 颗糖果,请你返回满足此条件下的 总方案数 。

示例 1:

输入:n = 5, limit = 2
输出:3
解释:总共有 3 种方法分配 5 颗糖果,且每位小朋友的糖果数不超过 2 :(1, 2, 2) ,(2, 1, 2) 和 (2, 2, 1) 。

示例 2:

输入:n = 3, limit = 3
输出:10
解释:总共有 10 种方法分配 3 颗糖果,且每位小朋友的糖果数不超过 3 :(0, 0, 3) ,(0, 1, 2) ,(0, 2, 1) ,(0, 3, 0) ,(1, 0, 2) ,(1, 1, 1) ,(1, 2, 0) ,(2, 0, 1) ,(2, 1, 0) 和 (3, 0, 0) 。

说明:

  • 1 <= n <= 10^6
  • 1 <= limit <= 10^6

思路

n 颗糖果分给 3 位小朋友,每个小朋友分到的糖果数量不超过 limit,求分配的方案数。注意,糖果必须分完,比如示例 1,不存在分得糖果数量为 0 的情况。

  • 第一个小朋友分到的糖果数为 a ∈ [0, Math.min(n, limit)]
  • 第二个小朋友分到的糖果数为 b ∈ [Math.max(0, n - a - limit), Math.min(n - a, limit)]
  • 第三个小朋友分到的糖果数为 n - a - b

代码


/**
 * @date 2025-06-01 21:49
 */
public class DistributeCandies2929 {

    public long distributeCandies(int n, int limit) {
        long res = 0L;
        for (int i = 0; i <= Math.min(n, limit); i++) {
            if (n - i > 2 * limit) {
                continue;
            }
            res += Math.min(n - i, limit) - Math.max(0, n - i - limit) + 1;
        }
        return res;
    }
}

性能

909.蛇梯棋

目标

给你一个大小为 n x n 的整数矩阵 board ,方格按从 1 到 n2 编号,编号遵循 转行交替方式 ,从左下角开始 (即,从 board[n - 1][0] 开始)的每一行改变方向。

你一开始位于棋盘上的方格 1。每一回合,玩家需要从当前方格 curr 开始出发,按下述要求前进:

  • 选定目标方格 next ,目标方格的编号在范围 [curr + 1, min(curr + 6, n2)] 。
    • 该选择模拟了掷 六面体骰子 的情景,无论棋盘大小如何,玩家最多只能有 6 个目的地。
  • 传送玩家:如果目标方格 next 处存在蛇或梯子,那么玩家会传送到蛇或梯子的目的地。否则,玩家传送到目标方格 next 。
  • 当玩家到达编号 n2 的方格时,游戏结束。

如果 board[r][c] != -1 ,位于 r 行 c 列的棋盘格中可能存在 “蛇” 或 “梯子”。那个蛇或梯子的目的地将会是 board[r][c]。编号为 1 和 n2 的方格不是任何蛇或梯子的起点。

注意,玩家在每次掷骰的前进过程中最多只能爬过蛇或梯子一次:就算目的地是另一条蛇或梯子的起点,玩家也 不能 继续移动。

  • 举个例子,假设棋盘是 [[-1,4],[-1,3]] ,第一次移动,玩家的目标方格是 2 。那么这个玩家将会顺着梯子到达方格 3 ,但 不能 顺着方格 3 上的梯子前往方格 4 。(简单来说,类似飞行棋,玩家掷出骰子点数后移动对应格数,遇到单向的路径(即梯子或蛇)可以直接跳到路径的终点,但如果多个路径首尾相连,也不能连续跳多个路径)
-1(4) 4(3)
-1(1) 3(2)

返回达到编号为 n2 的方格所需的最少掷骰次数,如果不可能,则返回 -1。

示例 1:

输入:board = [[-1,-1,-1,-1,-1,-1],[-1,-1,-1,-1,-1,-1],[-1,-1,-1,-1,-1,-1],[-1,35,-1,-1,13,-1],[-1,-1,-1,-1,-1,-1],[-1,15,-1,-1,-1,-1]]
输出:4
解释:
首先,从方格 1 [第 5 行,第 0 列] 开始。 
先决定移动到方格 2 ,并必须爬过梯子移动到到方格 15 。
然后决定移动到方格 17 [第 3 行,第 4 列],必须爬过蛇到方格 13 。
接着决定移动到方格 14 ,且必须通过梯子移动到方格 35 。 
最后决定移动到方格 36 , 游戏结束。 
可以证明需要至少 4 次移动才能到达最后一个方格,所以答案是 4 。 

示例 2:

输入:board = [[-1,-1],[-1,3]]
输出:1

说明:

  • n == board.length == board[i].length
  • 2 <= n <= 20
  • board[i][j] 的值是 -1 或在范围 [1, n^2]
  • 编号为 1 和 n^2 的方格上没有蛇或梯子

思路

有一个 n x n 棋盘 board,左下角从 1 开始编号,换行时改变编号顺序,例如 4 x 4 棋盘格子编号如下:

16  15  14  13
 9  10  11  12
 8   7   6   5
 1   2   3   4

玩家投骰子 1 ~ 6,根据点数移动到编号为 cur + point 的格子上,如果目标格子的值不为 -1,则传送到对应的格子编号。求到达终点所需的最小步数。

首先要解决棋盘编号与格子坐标的转换关系,然后使用 bfs 尝试所有可能的走法,并记录步数。为了防止到达不了终点陷入死循环,需要记录已经访问过的编号。

  • int row = m - 1 - (no - 1) / n;
  • int col = (m - 1 - row) % 2 == 0 ? (no - 1) % n : n - 1 - (no - 1) % n;

代码


/**
 * @date 2025-04-21 8:46
 */
public class SnakesAndLadders909 {

    public int snakesAndLadders(int[][] board) {
        int m = board.length;
        int n = board[0].length;
        int end = m * n;
        int res = 0;
        boolean[] visited = new boolean[end + 1];
        Queue<Integer> q = new ArrayDeque<>();
        q.add(1);
        while (!q.isEmpty()) {
            res++;
            int size = q.size();
            for (int i = 0; i < size; i++) {
                Integer cur = q.poll();
                for (int no = cur + 1; no <= cur + 6; no++) {
                    if (no == end) {
                        return res;
                    }
                    if (visited[no]) {
                        continue;
                    }
                    visited[no] = true;
                    int row = m - 1 - (no - 1) / n;
                    int col = (m - 1 - row) % 2 == 0 ? (no - 1) % n : n - 1 - (no - 1) % n;
                    if (board[row][col] == -1) {
                        q.offer(no);
                    } else {
                        if (board[row][col] == end) {
                            return res;
                        }
                        q.offer(board[row][col]);
                    }
                }
            }
        }
        return -1;
    }

}

性能

2359.找到离给定两个节点最近的节点

目标

给你一个 n 个节点的 有向图 ,节点编号为 0 到 n - 1 ,每个节点 至多 有一条出边。

有向图用大小为 n 下标从 0 开始的数组 edges 表示,表示节点 i 有一条有向边指向 edges[i] 。如果节点 i 没有出边,那么 edges[i] == -1 。

同时给你两个节点 node1 和 node2 。

请你返回一个从 node1 和 node2 都能到达节点的编号,使节点 node1 和节点 node2 到这个节点的距离 较大值最小化。如果有多个答案,请返回 最小 的节点编号。如果答案不存在,返回 -1 。

注意 edges 可能包含环。

示例 1:

输入:edges = [2,2,3,-1], node1 = 0, node2 = 1
输出:2
解释:从节点 0 到节点 2 的距离为 1 ,从节点 1 到节点 2 的距离为 1 。
两个距离的较大值为 1 。我们无法得到一个比 1 更小的较大值,所以我们返回节点 2 。

示例 2:

输入:edges = [1,2,-1], node1 = 0, node2 = 2
输出:2
解释:节点 0 到节点 2 的距离为 2 ,节点 2 到它自己的距离为 0 。
两个距离的较大值为 2 。我们无法得到一个比 2 更小的较大值,所以我们返回节点 2 。

说明:

  • n == edges.length
  • 2 <= n <= 10^5
  • -1 <= edges[i] < n
  • edges[i] != i
  • 0 <= node1, node2 < n

思路

使用两个不同的起点公用一个dfs,出度最大为 1,使用两个集合记录访问路径,遇到环、共同访问过的节点或者出度为 0 则停止。

代码


/**
 * @date 2025-05-30 0:44
 */
public class ClosestMeetingNode2359 {

    public int closestMeetingNode(int[] edges, int node1, int node2) {
        return dfs(node1, node2, edges, new HashSet<>(), new HashSet<>());
    }

    public int dfs(int cur1, int cur2, int[] edges, Set<Integer> set1, Set<Integer> set2) {
        if (set1.contains(cur2) && set2.contains(cur1) || cur1 == cur2) {
            return Math.min(cur1, cur2);
        } else if (set1.contains(cur2)) {
            return cur2;
        } else if (set2.contains(cur1)) {
            return cur1;
        }
        if (cur1 != -1) {
            set1.add(cur1);
        }
        if (cur2 != -1) {
            set2.add(cur2);
        }
        if (cur1 != -1 && !set1.contains(edges[cur1])) {
            return dfs(edges[cur1], cur2 == -1 ? -1 : edges[cur2], edges, set1, set2);
        } else if (cur2 != -1 && !set2.contains(edges[cur2])) {
            return dfs(cur1 == -1 ? -1 : edges[cur1], edges[cur2], edges, set1, set2);
        }
        return -1;
    }

}

性能