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;
    }

}

性能

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();
    }

}

性能

1298.你能从盒子里获得的最大糖果数

目标

给你 n 个盒子,每个盒子的格式为 [status, candies, keys, containedBoxes] ,其中:

  • 状态字 status[i]:整数,如果 box[i] 是开的,那么是 1 ,否则是 0 。
  • 糖果数 candies[i]: 整数,表示 box[i] 中糖果的数目。
  • 钥匙 keys[i]:数组,表示你打开 box[i] 后,可以得到一些盒子的钥匙,每个元素分别为该钥匙对应盒子的下标。
  • 内含的盒子 containedBoxes[i]:整数,表示放在 box[i] 里的盒子所对应的下标。

给你一个 initialBoxes 数组,表示你现在得到的盒子,你可以获得里面的糖果,也可以用盒子里的钥匙打开新的盒子,还可以继续探索从这个盒子里找到的其他盒子。

请你按照上述规则,返回可以获得糖果的 最大数目 。

示例 1:

输入:status = [1,0,1,0], candies = [7,5,4,100], keys = [[],[],[1],[]], containedBoxes = [[1,2],[3],[],[]], initialBoxes = [0]
输出:16
解释:
一开始你有盒子 0 。你将获得它里面的 7 个糖果和盒子 1 和 2。
盒子 1 目前状态是关闭的,而且你还没有对应它的钥匙。所以你将会打开盒子 2 ,并得到里面的 4 个糖果和盒子 1 的钥匙。
在盒子 1 中,你会获得 5 个糖果和盒子 3 ,但是你没法获得盒子 3 的钥匙所以盒子 3 会保持关闭状态。
你总共可以获得的糖果数目 = 7 + 4 + 5 = 16 个。

示例 2:

输入:status = [1,0,0,0,0,0], candies = [1,1,1,1,1,1], keys = [[1,2,3,4,5],[],[],[],[],[]], containedBoxes = [[1,2,3,4,5],[],[],[],[],[]], initialBoxes = [0]
输出:6
解释:
你一开始拥有盒子 0 。打开它你可以找到盒子 1,2,3,4,5 和它们对应的钥匙。
打开这些盒子,你将获得所有盒子的糖果,所以总糖果数为 6 个。

示例 3:

输入:status = [1,1,1], candies = [100,1,100], keys = [[],[0,2],[]], containedBoxes = [[],[],[]], initialBoxes = [1]
输出:1

示例 4:

输入:status = [1], candies = [100], keys = [[]], containedBoxes = [[]], initialBoxes = []
输出:0

示例 5:

输入:status = [1,1,1], candies = [2,3,2], keys = [[],[],[]], containedBoxes = [[],[],[]], initialBoxes = [2,1,0]
输出:7

说明:

  • 1 <= status.length <= 1000
  • status.length == candies.length == keys.length == containedBoxes.length == n
  • status[i] 要么是 0 要么是 1 。
  • 1 <= candies[i] <= 1000
  • 0 <= keys[i].length <= status.length
  • 0 <= keys[i][j] < status.length
  • keys[i] 中的值都是互不相同的。
  • 0 <= containedBoxes[i].length <= status.length
  • 0 <= containedBoxes[i][j] < status.length
  • containedBoxes[i] 中的值都是互不相同的。
  • 每个盒子最多被一个盒子包含。
  • 0 <= initialBoxes.length <= status.length
  • 0 <= initialBoxes[i] < status.length

思路

开始时有一些盒子 initialBoxes 元素值表示盒子下标,盒子的状态用 status 数组表示,0 表示盒子被锁住,1 表示盒子是打开的。盒子中装有糖果、也可能装有其它盒子,或者盒子的钥匙。求能够获得的最大糖果数。

bfs 使用优先队列对盒子状态排序,优先取开着的盒子。将开着的盒子中的盒子放入队列,并且用盒子中的钥匙修改队列中的盒子状态。记录已经开过的盒子,避免重复计数。

代码


/**
 * @date 2025-06-03 20:30
 */
public class MaxCandies1298 {

    public int maxCandies(int[] status, int[] candies, int[][] keys, int[][] containedBoxes, int[] initialBoxes) {
        int n = status.length;
        int[][] boxes = new int[n][2];
        for (int i = 0; i < n; i++) {
            boxes[i] = new int[]{i, status[i]};
        }
        PriorityQueue<int[]> q = new PriorityQueue<>((a, b) -> b[1] - a[1]);
        for (int initialBox : initialBoxes) {
            q.offer(boxes[initialBox]);
        }
        int res = 0;
        Set<Integer> visited = new HashSet<>();
        while (!q.isEmpty() && q.peek()[1] == 1) {
            while (!q.isEmpty() && q.peek()[1] == 1) {
                int i = q.poll()[0];
                visited.add(i);
                res += candies[i];
                for (int j : keys[i]) {
                    boxes[j][1] = 1;
                }
                for (int j : containedBoxes[i]) {
                    if (visited.contains(j)) {
                        continue;
                    }
                    q.offer(boxes[j]);
                }
            }
        }
        return res;
    }
}

性能

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;
    }

}

性能

3373.连接两棵树后最大目标节点数目II

目标

有两棵 无向 树,分别有 n 和 m 个树节点。两棵树中的节点编号分别为[0, n - 1] 和 [0, m - 1] 中的整数。

给你两个二维整数 edges1 和 edges2 ,长度分别为 n - 1 和 m - 1 ,其中 edges1[i] = [ai, bi] 表示第一棵树中节点 ai 和 bi 之间有一条边,edges2[i] = [ui, vi] 表示第二棵树中节点 ui 和 vi 之间有一条边。

如果节点 u 和节点 v 之间路径的边数是偶数,那么我们称节点 u 是节点 v 的 目标节点 。注意 ,一个节点一定是它自己的 目标节点 。

请你返回一个长度为 n 的整数数组 answer ,answer[i] 表示将第一棵树中的一个节点与第二棵树中的一个节点连接一条边后,第一棵树中节点 i 的 目标节点 数目的 最大值 。

注意 ,每个查询相互独立。意味着进行下一次查询之前,你需要先把刚添加的边给删掉。

示例 1:

输入:edges1 = [[0,1],[0,2],[2,3],[2,4]], edges2 = [[0,1],[0,2],[0,3],[2,7],[1,4],[4,5],[4,6]]
输出:[8,7,7,8,8]
解释:
对于 i = 0 ,连接第一棵树中的节点 0 和第二棵树中的节点 0 。
对于 i = 1 ,连接第一棵树中的节点 1 和第二棵树中的节点 4 。
对于 i = 2 ,连接第一棵树中的节点 2 和第二棵树中的节点 7 。
对于 i = 3 ,连接第一棵树中的节点 3 和第二棵树中的节点 0 。
对于 i = 4 ,连接第一棵树中的节点 4 和第二棵树中的节点 4 。

示例 2:

输入:edges1 = [[0,1],[0,2],[0,3],[0,4]], edges2 = [[0,1],[1,2],[2,3]]
输出:[3,6,6,6,6]
解释:
对于每个 i ,连接第一棵树中的节点 i 和第二棵树中的任意一个节点。

提示:

  • 2 <= n, m <= 10^5
  • edges1.length == n - 1
  • edges2.length == m - 1
  • edges1[i].length == edges2[i].length == 2
  • edges1[i] = [ai, bi]
  • 0 <= ai, bi < n
  • edges2[i] = [ui, vi]
  • 0 <= ui, vi < m
  • 输入保证 edges1 和 edges2 都表示合法的树。

思路

有两棵树,当节点 ab 之间的边数为偶数时,称 ab 的目标节点,在两棵树任意节点之间连一条边,针对第一棵树的所有节点,计算其目标节点的最大个数。

3372.连接两棵树后最大目标节点数目I 相比,目标节点的条件从边数小于等于 k 变为了 偶数。

同一颗树中,某一个节点经过 偶数 或者 奇数条边到达的节点个数是固定的。不妨固定一个节点为根节点,经过 偶数 条边到达的节点集合为 A,经过 奇数 条边到达的节点集合为 B。集合 A 中节点的目标节点还是 A 中的节点,同理 B 中节点的目标节点还是 B 中的节点。

因此问题转化为计算两棵树的集合 A B,判断第一颗树的节点所属集合,如果属于 A,则取 A1 + Math.max(A2, B2),否则取 B1 + Math.max(A2, B2)。

核心点是固定一个参考节点,根据到达该节点的边数将节点分类,可以快速计算出目标节点个数,如果针对每一节点暴力搜索目标节点个数肯定会超时。

代码


/**
 * @date 2025-05-29 23:50
 */
public class MaxTargetNodes3373 {

    public int[] maxTargetNodes(int[][] edges1, int[][] edges2) {
        int n = edges1.length + 1;
        int m = edges2.length + 1;
        List<Integer>[] g1 = new List[n];
        List<Integer>[] g2 = new List[m];
        Arrays.setAll(g1, x -> new ArrayList<>());
        Arrays.setAll(g2, x -> new ArrayList<>());
        for (int[] edge : edges1) {
            int a = edge[0];
            int b = edge[1];
            g1[a].add(b);
            g1[b].add(a);
        }
        for (int[] edge : edges2) {
            int a = edge[0];
            int b = edge[1];
            g2[a].add(b);
            g2[b].add(a);
        }
        Set<Integer>[] set1 = new HashSet[2];
        Arrays.setAll(set1, x -> new HashSet<>());
        int[] set2 = new int[]{0, 0};
        dfs(0, -1, g1, 0, set1);
        dfs(0, -1, g2, 0, set2);
        int max2 = Math.max(set2[0], set2[1]);
        int[] res = new int[n];
        for (int i = 0; i < n; i++) {
            if (set1[0].contains(i)) {
                res[i] = set1[0].size() + max2;
            } else {
                res[i] = set1[1].size() + max2;
            }
        }
        return res;
    }

    public void dfs(int cur, int parent, List<Integer>[] g, int flag, Set<Integer>[] set) {
        set[flag].add(cur);
        for (Integer next : g[cur]) {
            if (next == parent) {
                continue;
            }
            dfs(next, cur, g, flag ^ 1, set);
        }
    }

    public void dfs(int cur, int parent, List<Integer>[] g, int flag, int[] res) {
        res[flag]++;
        for (Integer next : g[cur]) {
            if (next == parent) {
                continue;
            }
            dfs(next, cur, g, flag ^ 1, res);
        }
    }
}

性能

3372.连接两棵树后最大目标节点数目I

目标

有两棵 无向 树,分别有 n 和 m 个树节点。两棵树中的节点编号分别为[0, n - 1] 和 [0, m - 1] 中的整数。

给你两个二维整数 edges1 和 edges2 ,长度分别为 n - 1 和 m - 1 ,其中 edges1[i] = [ai, bi] 表示第一棵树中节点 ai 和 bi 之间有一条边,edges2[i] = [ui, vi] 表示第二棵树中节点 ui 和 vi 之间有一条边。同时给你一个整数 k 。

如果节点 u 和节点 v 之间路径的边数小于等于 k ,那么我们称节点 u 是节点 v 的 目标节点 。注意 ,一个节点一定是它自己的 目标节点 。

请你返回一个长度为 n 的整数数组 answer ,answer[i] 表示将第一棵树中的一个节点与第二棵树中的一个节点连接一条边后,第一棵树中节点 i 的 目标节点 数目的 最大值 。

注意 ,每个查询相互独立。意味着进行下一次查询之前,你需要先把刚添加的边给删掉。

示例 1:

输入:edges1 = [[0,1],[0,2],[2,3],[2,4]], edges2 = [[0,1],[0,2],[0,3],[2,7],[1,4],[4,5],[4,6]], k = 2
输出:[9,7,9,8,8]
解释:
对于 i = 0 ,连接第一棵树中的节点 0 和第二棵树中的节点 0 。
对于 i = 1 ,连接第一棵树中的节点 1 和第二棵树中的节点 0 。
对于 i = 2 ,连接第一棵树中的节点 2 和第二棵树中的节点 4 。
对于 i = 3 ,连接第一棵树中的节点 3 和第二棵树中的节点 4 。
对于 i = 4 ,连接第一棵树中的节点 4 和第二棵树中的节点 4 。

示例 2:

输入:edges1 = [[0,1],[0,2],[0,3],[0,4]], edges2 = [[0,1],[1,2],[2,3]], k = 1
输出:[6,3,3,3,3]
解释:
对于每个 i ,连接第一棵树中的节点 i 和第二棵树中的任意一个节点。

提示:

  • 2 <= n, m <= 1000
  • edges1.length == n - 1
  • edges2.length == m - 1
  • edges1[i].length == edges2[i].length == 2
  • edges1[i] = [ai, bi]
  • 0 <= ai, bi < n
  • edges2[i] = [ui, vi]
  • 0 <= ui, vi < m
  • 输入保证 edges1 和 edges2 都表示合法的树。
  • 0 <= k <= 1000

思路

有两棵树,当节点 ab 之间的边数小于等于 k 时,称 ab 的目标节点,在两棵树任意节点之间连一条边,针对第一棵树的所有节点,计算其目标节点的最大个数。

计算第一棵树的每个节点至多经过 k 条边相连的节点个数,当 k > 0 时,加上第二棵树每个节点至多经过 k - 1 条边相连的节点个数的最大值。

代码


/**
 * @date 2025-05-28 23:36
 */
public class MaxTargetNodes3372 {

    public int[] maxTargetNodes(int[][] edges1, int[][] edges2, int k) {
        int n = edges1.length + 1;
        int m = edges2.length + 1;
        List<Integer>[] g1 = new List[n];
        List<Integer>[] g2 = new List[m];
        Arrays.setAll(g1, x -> new ArrayList<>());
        Arrays.setAll(g2, x -> new ArrayList<>());
        for (int[] edge1 : edges1) {
            int a = edge1[0];
            int b = edge1[1];
            g1[a].add(b);
            g1[b].add(a);
        }
        for (int[] edge2 : edges2) {
            int a = edge2[0];
            int b = edge2[1];
            g2[a].add(b);
            g2[b].add(a);
        }
        int max = 0;
        for (int i = 0; i < m; i++) {
            max = Math.max(max, dfs(i, -1, g2, 0, k - 1));
        }
        int[] res = new int[n];
        for (int i = 0; i < n; i++) {
            res[i] = dfs(i, -1, g1, 0, k) + (k == 0 ? 0 : max);
        }
        return res;
    }

    public int dfs(int cur, int parent, List<Integer>[] g, int cnt, int k) {
        if (cnt >= k) {
            return 1;
        }
        int res = 0;
        for (Integer next : g[cur]) {
            if (next == parent) {
                continue;
            }
            res += dfs(next, cur, g, cnt + 1, k);
        }
        return res + 1;
    }

}

性能

1863.找出所有子集的异或总和再求和

目标

一个数组的 异或总和 定义为数组中所有元素按位 XOR 的结果;如果数组为 空 ,则异或总和为 0 。

  • 例如,数组 [2,5,6] 的 异或总和 为 2 XOR 5 XOR 6 = 1 。

给你一个数组 nums ,请你求出 nums 中每个 子集 的 异或总和 ,计算并返回这些值相加之 和 。

注意:在本题中,元素 相同 的不同子集应 多次 计数。

数组 a 是数组 b 的一个 子集 的前提条件是:从 b 删除几个(也可能不删除)元素能够得到 a 。

示例 1:

输入:nums = [1,3]
输出:6
解释:[1,3] 共有 4 个子集:
- 空子集的异或总和是 0 。
- [1] 的异或总和为 1 。
- [3] 的异或总和为 3 。
- [1,3] 的异或总和为 1 XOR 3 = 2 。
0 + 1 + 3 + 2 = 6

示例 2:

输入:nums = [5,1,6]
输出:28
解释:[5,1,6] 共有 8 个子集:
- 空子集的异或总和是 0 。
- [5] 的异或总和为 5 。
- [1] 的异或总和为 1 。
- [6] 的异或总和为 6 。
- [5,1] 的异或总和为 5 XOR 1 = 4 。
- [5,6] 的异或总和为 5 XOR 6 = 3 。
- [1,6] 的异或总和为 1 XOR 6 = 7 。
- [5,1,6] 的异或总和为 5 XOR 1 XOR 6 = 2 。
0 + 5 + 1 + 6 + 4 + 3 + 7 + 2 = 28

示例 3:

输入:nums = [3,4,5,6,7,8]
输出:480
解释:每个子集的全部异或总和值之和为 480 。

说明:

  • 1 <= nums.length <= 12
  • 1 <= nums[i] <= 20

思路

计算数组所有子序列的异或和之和。

dfs 枚举所有子序列。

代码


/**
 * @date 2025-04-05 19:47
 */
public class SubsetXORSum1863 {

    int res;

    public int subsetXORSum(int[] nums) {
        dfs(0, 0, nums);
        return res;
    }

    public void dfs(int index, int xor, int[] nums) {
        int n = nums.length;
        if (index == n) {
            res += xor;
            return;
        }
        dfs(index + 1, xor, nums);
        dfs(index + 1, xor ^ nums[index], nums);
    }

}

性能

1123.最深叶节点的最近公共祖先

目标

给你一个有根节点 root 的二叉树,返回它 最深的叶节点的最近公共祖先 。

回想一下:

  • 叶节点 是二叉树中没有子节点的节点
  • 树的根节点的 深度 为 0,如果某一节点的深度为 d,那它的子节点的深度就是 d+1
  • 如果我们假定 A 是一组节点 S 的 最近公共祖先,S 中的每个节点都在以 A 为根节点的子树中,且 A 的深度达到此条件下可能的最大值。

示例 1:

输入:root = [3,5,1,6,2,0,8,null,null,7,4]
输出:[2,7,4]
解释:我们返回值为 2 的节点,在图中用黄色标记。
在图中用蓝色标记的是树的最深的节点。
注意,节点 6、0 和 8 也是叶节点,但是它们的深度是 2 ,而节点 7 和 4 的深度是 3 。

示例 2:

输入:root = [1]
输出:[1]
解释:根节点是树中最深的节点,它是它本身的最近公共祖先。

示例 3:

输入:root = [0,1,3,null,2]
输出:[2]
解释:树中最深的叶节点是 2 ,最近公共祖先是它自己。

说明:

  • 树中的节点数将在 [1, 1000] 的范围内。
  • 0 <= Node.val <= 1000
  • 每个节点的值都是 独一无二 的。

思路

找到二叉树最深的叶节点并返回它们的最近公共祖先。

dfs 记录最大深度,返回子树深度,如果左右子树深度相同且等于最大深度则更新返回节点。

代码


/**
 * @date 2025-04-04 19:09
 */
public class LcaDeepestLeaves1123 {

    TreeNode res;
    int maxDepth;

    public TreeNode lcaDeepestLeaves(TreeNode root) {
        dfs(root, -1);
        return res;
    }

    public int dfs(TreeNode node, int depth) {
        if (node == null) {
            return depth;
        }
        int leftDepth = dfs(node.left, depth + 1);
        int rightDepth = dfs(node.right, depth + 1);
        int d = Math.max(leftDepth, rightDepth);
        maxDepth = Math.max(maxDepth, d);
        if (rightDepth == maxDepth && leftDepth == maxDepth) {
            res = node;
        }
        return d;
    }
}

性能

1367.二叉树中的链表

目标

给你一棵以 root 为根的二叉树和一个 head 为第一个节点的链表。

如果在二叉树中,存在一条一直向下的路径,且每个点的数值恰好一一对应以 head 为首的链表中每个节点的值,那么请你返回 True ,否则返回 False 。

一直向下的路径的意思是:从树中某个节点开始,一直连续向下的路径。

示例 1:

输入:head = [4,2,8], root = [1,4,4,null,2,2,null,1,null,6,8,null,null,null,null,1,3]
输出:true
解释:树中蓝色的节点构成了与链表对应的子路径。

示例 2:

输入:head = [1,4,2,6], root = [1,4,4,null,2,2,null,1,null,6,8,null,null,null,null,1,3]
输出:true

示例 3:

输入:head = [1,4,2,6,8], root = [1,4,4,null,2,2,null,1,null,6,8,null,null,null,null,1,3]
输出:false
解释:二叉树中不存在一一对应链表的路径。

说明:

  • 二叉树和链表中的每个节点的值都满足 1 <= node.val <= 100 。
  • 链表包含的节点数目在 1 到 100 之间。
  • 二叉树包含的节点数目在 1 到 2500 之间。

思路

判断二叉树中是否存在给定的路径 head,路径以链表的形式给出。

dfs 判断是否存在相同的路径。这道题的难点在于没有要求起点为 root,因此需要枚举所有节点为起点的情况。针对每一个起点我们最多检查 100 次,总共大概 2.5 * 10^5 次。

代码


/**
 * @date 2024-12-30 8:56
 */
public class IsSubPath1367 {

    public ListNode head;

    public boolean isSubPath(ListNode head, TreeNode root) {
        this.head = head;
        return dfs(root, head);
    }

    public boolean dfs(TreeNode cur, ListNode node) {
        if (node == null) {
            return true;
        }
        if (cur == null) {
            return false;
        }
        return cur.val == node.val && (dfs(cur.left, node.next) || dfs(cur.right, node.next))
                || node == head && (dfs(cur.left, head) || dfs(cur.right, head));
    }

}

性能

2056.棋盘上有效移动组合的数目

目标

有一个 8 x 8 的棋盘,它包含 n 个棋子(棋子包括车,后和象三种)。给你一个长度为 n 的字符串数组 pieces ,其中 pieces[i] 表示第 i 个棋子的类型(车,后或象)。除此以外,还给你一个长度为 n 的二维整数数组 positions ,其中 positions[i] = [ri, ci] 表示第 i 个棋子现在在棋盘上的位置为 (ri, ci) ,棋盘下标从 1 开始。

棋盘上每个棋子都可以移动 至多一次 。每个棋子的移动中,首先选择移动的 方向 ,然后选择 移动的步数 ,同时你要确保移动过程中棋子不能移到棋盘以外的地方。棋子需按照以下规则移动:

  • 车可以 水平或者竖直 从 (r, c) 沿着方向 (r+1, c),(r-1, c),(r, c+1) 或者 (r, c-1) 移动。
  • 后可以 水平竖直或者斜对角 从 (r, c) 沿着方向 (r+1, c),(r-1, c),(r, c+1),(r, c-1),(r+1, c+1),(r+1, c-1),(r-1, c+1),(r-1, c-1) 移动。
  • 象可以 斜对角 从 (r, c) 沿着方向 (r+1, c+1),(r+1, c-1),(r-1, c+1),(r-1, c-1) 移动。

移动组合 包含所有棋子的 移动 。每一秒,每个棋子都沿着它们选择的方向往前移动 一步 ,直到它们到达目标位置。所有棋子从时刻 0 开始移动。如果在某个时刻,两个或者更多棋子占据了同一个格子,那么这个移动组合 不有效 。

请你返回 有效 移动组合的数目。

注意:

  • 初始时,不会有两个棋子 在 同一个位置。
  • 有可能在一个移动组合中,有棋子不移动。
  • 如果两个棋子 直接相邻 且两个棋子下一秒要互相占据对方的位置,可以将它们在同一秒内 交换位置。

示例 1:

输入:pieces = ["rook"], positions = [[1,1]]
输出:15
解释:上图展示了棋子所有可能的移动。

示例 2:

输入:pieces = ["queen"], positions = [[1,1]]
输出:22
解释:上图展示了棋子所有可能的移动。

示例 3:

输入:pieces = ["bishop"], positions = [[4,3]]
输出:12
解释:上图展示了棋子所有可能的移动。

示例 4:

输入:pieces = ["rook","rook"], positions = [[1,1],[8,8]]
输出:223
解释:每个车有 15 种移动,所以总共有 15 * 15 = 225 种移动组合。
但是,有两个是不有效的移动组合:
- 将两个车都移动到 (8, 1) ,会导致它们在同一个格子相遇。
- 将两个车都移动到 (1, 8) ,会导致它们在同一个格子相遇。
所以,总共有 225 - 2 = 223 种有效移动组合。
注意,有两种有效的移动组合,分别是一个车在 (1, 8) ,另一个车在 (8, 1) 。
即使棋盘状态是相同的,这两个移动组合被视为不同的,因为每个棋子移动操作是不相同的。

示例 5:

输入:pieces = ["queen","bishop"], positions = [[5,7],[3,4]]
输出:281
解释:总共有 12 * 24 = 288 种移动组合。
但是,有一些不有效的移动组合:
- 如果后停在 (6, 7) ,它会阻挡象到达 (6, 7) 或者 (7, 8) 。
- 如果后停在 (5, 6) ,它会阻挡象到达 (5, 6) ,(6, 7) 或者 (7, 8) 。
- 如果象停在 (5, 2) ,它会阻挡后到达 (5, 2) 或者 (5, 1) 。
在 288 个移动组合当中,281 个是有效的。

说明:

  • n == pieces.length
  • n == positions.length
  • 1 <= n <= 4
  • pieces 只包含字符串 "rook" ,"queen" 和 "bishop" 。
  • 棋盘上最多只有一个后。
  • 1 <= ri, ci <= 8
  • 每一个 positions[i] 互不相同。

思路

有一个 8 x 8 的棋盘,行列编号从 1 ~ 8。棋盘上有 n 个棋子,pieces[i] 表示棋子 i 的类型,rook 表示车,queen 表示皇后,bishop 表示象,positions[i] = [ri, ci] 表示棋子 i 所在行编号为 ri,所在列编号为 ci。棋盘上的每个棋子都可以移动 至多 一步,不同的棋子移动方式不同,参考国际象棋。车走竖线或横线,象走斜线,皇后走竖线、横线或斜线,不能跳过其它棋子。

特别注意,我们求的是 有效移动 组合,重点在 移动 而不是最终状态,比如示例4。

棋盘大小固定,考虑车移动或者不移动,它最多有 15 种移动的可能,包括不移动或者移动到相应方向上的其它格子共 1 + 2 * 7 种。由于棋盘是偶数没有中间格子,所以象有 8 + 6 种。同理皇后可能的移动方式最多有 28 种,包括不移动或者移动相应方向上的其它格子 1 + 2 * 7 + 7 + 6 种,如果它位于一条 8 格的对角线上,除去自身还剩 7 格,它所在的另一对角线剩余 6 个格子。

题目描述里面非常让人迷惑的一点是既允许相邻棋子同时交换位置,而示例5又说棋子会阻挡其它棋子通行。这个题的难点在于如果按照棋盘状态去枚举的话,当两个棋子相遇的时候不知道是否应该穿越,如果穿越的话就会移动另一个棋子,如何去重?我们必须将移动的步数考虑进去,如果两个棋子相遇时都没有到达目标位置那么它们可以穿过。否则,某个棋子提前停下,另一个棋子行进到相同的位置发生碰撞,不符合题目条件。

参考题解思路写出来了。

代码


/**
 * @date 2024-12-04 14:24
 */
public class CountCombinations2056 {

    public static class Move {
        public int x;
        public int y;
        public int dx;
        public int dy;
        public int step;

        public Move(int x, int y, int dx, int dy, int step) {
            this.x = x;
            this.y = y;
            this.dx = dx;
            this.dy = dy;
            this.step = step;
        }
    }

    public int[][] rookDir = new int[][]{{-1, 0}, {1, 0}, {0, -1}, {0, 1}};
    public int[][] bishopDir = new int[][]{{-1, -1}, {1, 1}, {1, -1}, {-1, 1}};
    public int[][] queenDir = new int[][]{{-1, 0}, {1, 0}, {0, -1}, {0, 1}, {-1, -1}, {1, 1}, {1, -1}, {-1, 1}};

    /**
     * 预处理每个棋子可行的移动方案、起点、方向、步数
     */
    public int countCombinations(String[] pieces, int[][] positions) {
        int n = pieces.length;
        Move[][] moves = new Move[n][];
        for (int i = 0; i < n; i++) {
            moves[i] = generateAllValidMove(pieces[i], positions[i]);
        }
        Move[] curMove = new Move[n];
        return dfs(0, n, curMove, moves);
    }

    /**
     * 回溯,先从棋子0中选一个可行的移动方案,然后进入下一层,
     * 从棋子1中选一个可行的移动方案,判断当前方案与前面所选的移动方案是否存在某一时刻使得两个棋子占据同一个格子
     * 如果不存在就进入下一层,否则枚举另一个方案。
     * i 表示枚举第 i 个棋子的合法移动方案
     * curMove 表示前面棋子选择的移动方案,用于下一层比较移动方案是否合法
     * moves 表示所有棋子的移动方案
     */
    public int dfs(int i, int n, Move[] curMove, Move[][] moves) {
        if (i == n) {
            return 1;
        }
        int cnt = 0;
        here:
        for (Move move : moves[i]) {
            for (int j = 0; j < i; j++) {
                if (!valid(move, curMove[j])) {
                    continue here;
                }
            }
            curMove[i] = move;
            cnt += dfs(i + 1, n, curMove, moves);
        }
        return cnt;
    }

    public boolean valid(Move move, Move curMove) {
        int x1 = move.x;
        int y1 = move.y;
        int x2 = curMove.x;
        int y2 = curMove.y;
        // 步数小的会提前停下来,如果它们坐标相同说明存在在某一时刻使得两个棋子移动到同一个位置上,不合法
        for (int i = 0; i < Math.max(move.step, curMove.step); i++) {
            if (i < move.step) {
                x1 += move.dx;
                y1 += move.dy;
            }
            if (i < curMove.step) {
                x2 += curMove.dx;
                y2 += curMove.dy;
            }
            if (x1 == x2 && y1 == y2) {
                return false;
            }
        }
        return true;
    }

    /**
     * 生成该棋子所有可能的移动方案
     * 起点,方向,步数
     */
    public Move[] generateAllValidMove(String pieces, int[] positions) {
        int[][] directions = null;
        switch (pieces) {
            case "rook":
                directions = rookDir;
                break;
            case "bishop":
                directions = bishopDir;
                break;
            case "queen":
                directions = queenDir;
                break;
            default:
        }
        if (directions == null) {
            return null;
        }
        List<Move> moves = new ArrayList<>();
        int x = positions[0];
        int y = positions[1];
        // 将不移动的情况加入集合
        moves.add(new Move(x, y, 0, 0, 0));
        // 遍历该棋子允许的行进方向,
        for (int[] dir : directions) {
            int dx = dir[0];
            int dy = dir[1];
            int step = 0;
            x = positions[0] + dx;
            y = positions[1] + dy;
            while (x > 0 && x <= 8 && y > 0 && y <= 8) {
                moves.add(new Move(positions[0], positions[1], dx, dy, ++step));
                x += dx;
                y += dy;
            }
        }
        return moves.toArray(new Move[0]);
    }

}

性能