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

}

性能

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

}

性能

2131.连接两字母单词得到的最长回文串

目标

给你一个字符串数组 words 。words 中每个元素都是一个包含 两个 小写英文字母的单词。

请你从 words 中选择一些元素并按 任意顺序 连接它们,并得到一个 尽可能长的回文串 。每个元素 至多 只能使用一次。

请你返回你能得到的最长回文串的 长度 。如果没办法得到任何一个回文串,请你返回 0 。

回文串 指的是从前往后和从后往前读一样的字符串。

示例 1:

输入:words = ["lc","cl","gg"]
输出:6
解释:一个最长的回文串为 "lc" + "gg" + "cl" = "lcggcl" ,长度为 6 。
"clgglc" 是另一个可以得到的最长回文串。

示例 2:

输入:words = ["ab","ty","yt","lc","cl","ab"]
输出:8
解释:最长回文串是 "ty" + "lc" + "cl" + "yt" = "tylcclyt" ,长度为 8 。
"lcyttycl" 是另一个可以得到的最长回文串。

示例 3:

输入:words = ["cc","ll","xx"]
输出:2
解释:最长回文串是 "cc" ,长度为 2 。
"ll" 是另一个可以得到的最长回文串。"xx" 也是。

说明:

  • 1 <= words.length <= 10^5
  • words[i].length == 2
  • words[i] 仅包含小写英文字母。

思路

有一个字符串数组 words,其元素字符个数为 2,求从中选择任意元素组成回文串的最大长度。

统计每个元素的出现次数,如果数组元素的两个字符不同,要组成回文只能左右对称,计算对称的元素对数 cnt,长度为 cnt * 4。如果元素的两个字符相同,它可以全部放到中间,为了使回文最长,当出现更长的相同字符元素时,可以将原来放中间的个数 centerCnt,放到对称的两边,centerCnt / 2 * 4

代码


/**
 * @date 2025-05-25 1:03
 */
public class LongestPalindrome2131 {

    public int longestPalindrome(String[] words) {
        Map<String, Integer> map = new HashMap<>();
        for (String word : words) {
            char a = word.charAt(0);
            char b = word.charAt(1);
            map.merge(a + String.valueOf(b), 1, Integer::sum);
        }
        int res = 0;
        int centerCnt = 0;
        for (Map.Entry<String, Integer> entry : map.entrySet()) {
            String key = entry.getKey();
            char a = key.charAt(0);
            char b = key.charAt(1);
            if (a == b) {
                Integer cnt = entry.getValue();
                if (cnt % 2 == 1) {
                    res += centerCnt / 2 * 4;
                    centerCnt = cnt;
                } else {
                    res += cnt / 2 * 4;
                }
            } else {
                int cnt = Math.min(entry.getValue(), map.getOrDefault(b + String.valueOf(a), 0));
                res += cnt * 4;
            }
            entry.setValue(0);
        }
        return res + centerCnt * 2;
    }

}

性能

3362.零数组变换III

目标

给你一个长度为 n 的整数数组 nums 和一个二维数组 queries ,其中 queries[i] = [li, ri] 。

每一个 queries[i] 表示对于 nums 的以下操作:

  • 将 nums 中下标在范围 [li, ri] 之间的每一个元素 最多 减少 1 。
  • 坐标范围内每一个元素减少的值相互 独立 。

零数组 指的是一个数组里所有元素都等于 0 。

请你返回 最多 可以从 queries 中删除多少个元素,使得 queries 中剩下的元素仍然能将 nums 变为一个 零数组 。如果无法将 nums 变为一个 零数组 ,返回 -1 。

示例 1:

输入:nums = [2,0,2], queries = [[0,2],[0,2],[1,1]]
输出:1
解释:
删除 queries[2] 后,nums 仍然可以变为零数组。
对于 queries[0] ,将 nums[0] 和 nums[2] 减少 1 ,将 nums[1] 减少 0 。
对于 queries[1] ,将 nums[0] 和 nums[2] 减少 1 ,将 nums[1] 减少 0 。

示例 2:

输入:nums = [1,1,1,1], queries = [[1,3],[0,2],[1,3],[1,2]]
输出:2
解释:
可以删除 queries[2] 和 queries[3] 。

示例 3:

输入:nums = [1,2,3,4], queries = [[0,3]]
输出:-1
解释:
nums 无法通过 queries 变成零数组。

说明:

  • 1 <= nums.length <= 10^5
  • 0 <= nums[i] <= 10^5
  • 1 <= queries.length <= 10^5
  • queries[i].length == 2
  • 0 <= li <= ri < nums.length

思路

有一个长度为 n 的整数数组,每一次操作可以将给定范围内的任意元素减 1,返回最多可以删掉多少个操作,使得剩下的操作能够将数组中的所有元素变为 0

对于元素 nums[0] 要将其变为 0 必须要操作 nums[0] 次,且操作范围必须包括 0,因此可以按照操作范围的左端点排序,每次选择右端点最远即覆盖最广的操作区间。可以维护一个由大到小的优先队列,从操作中选出左端点小于等于当前下标的操作,将其右端点放入队列。从队列中取出右端点大于等于当前下标的操作,使用差分数组进行区间修改。

代码


/**
 * @date 2025-05-22 23:02
 */
public class MaxRemoval3362 {

    public int maxRemoval(int[] nums, int[][] queries) {
        PriorityQueue<Integer> q = new PriorityQueue<>((a, b) -> b - a);
        Arrays.sort(queries, (a, b) -> a[0] - b[0]);
        int n = nums.length;
        int[] diff = new int[n + 1];
        diff[0] = nums[0];
        for (int i = 1; i < n; i++) {
            diff[i] = nums[i] - nums[i - 1];
        }
        int num = 0;
        int k = 0;
        for (int i = 0; i < n; i++) {
            num += diff[i];
            while (k < queries.length && queries[k][0] <= i) {
                q.offer(queries[k++][1]);
            }
            while (!q.isEmpty() && q.peek() >= i && num > 0) {
                diff[q.poll() + 1]++;
                num--;
            }
            if (num > 0) {
                return -1;
            }
        }
        return q.size();
    }

}

性能

3356.零数组变换II

目标

给你一个长度为 n 的整数数组 nums 和一个二维数组 queries,其中 queries[i] = [li, ri, vali]。

每个 queries[i] 表示在 nums 上执行以下操作:

  • 将 nums 中 [li, ri] 范围内的每个下标对应元素的值 最多 减少 vali。
  • 每个下标的减少的数值可以独立选择。

零数组 是指所有元素都等于 0 的数组。

返回 k 可以取到的 最小非负 值,使得在 顺序 处理前 k 个查询后,nums 变成 零数组。如果不存在这样的 k,则返回 -1。

示例 1:

输入: nums = [2,0,2], queries = [[0,2,1],[0,2,1],[1,1,3]]
输出: 2
解释:
对于 i = 0(l = 0, r = 2, val = 1):
在下标 [0, 1, 2] 处分别减少 [1, 0, 1]。
数组将变为 [1, 0, 1]。
对于 i = 1(l = 0, r = 2, val = 1):
在下标 [0, 1, 2] 处分别减少 [1, 0, 1]。
数组将变为 [0, 0, 0],这是一个零数组。因此,k 的最小值为 2。

示例 2:

输入: nums = [4,3,2,1], queries = [[1,3,2],[0,2,1]]
输出: -1
解释:
对于 i = 0(l = 1, r = 3, val = 2):
在下标 [1, 2, 3] 处分别减少 [2, 2, 1]。
数组将变为 [4, 1, 0, 0]。
对于 i = 1(l = 0, r = 2, val = 1):
在下标 [0, 1, 2] 处分别减少 [1, 1, 0]。
数组将变为 [3, 0, 0, 0],这不是一个零数组。

提示:

  • 1 <= nums.length <= 10^5
  • 0 <= nums[i] <= 5 * 10^5
  • 1 <= queries.length <= 10^5
  • queries[i].length == 3
  • 0 <= li <= ri < nums.length
  • 1 <= vali <= 5

思路

有一个长度为 n 的整数数组,每一次操作可以将给定范围内的任意元素 最多 减去 vali = queries[2],计算将数组中的所有元素变为 0 最少需要按顺序操作几次。

3355.零数组变换I 相比,求的是最小操作次数,每一次操作都需要判断数组元素是否全为 0,涉及到区间修改与区间查询,可以使用线段树维护区间最大值,每次操作后判断最大值是否大于 0

官网给出了另一种思路,二分查找操作次数 k,针对每一个 k 问题变成 3355.零数组变换I

代码


/**
 * @date 2025-05-21 9:35
 */
public class MinZeroArray3356 {

    public int minZeroArray(int[] nums, int[][] queries) {
        int right = queries.length - 1;
        int left = -1;
        int mid = left + (right - left) / 2;
        while (left <= right) {
            if (check(nums, mid, queries)) {
                right = mid - 1;
            } else {
                left = mid + 1;
            }
            mid = left + (right - left) / 2;
        }
        return left == queries.length ? -1 : left + 1;
    }

    public boolean check(int[] nums, int k, int[][] queries) {
        int n = nums.length;
        int[] diff = new int[n + 1];
        diff[0] = nums[0];
        for (int i = 1; i < n; i++) {
            diff[i] = nums[i] - nums[i - 1];
        }
        for (int i = 0; i <= k; i++) {
            int[] query = queries[i];
            int val = query[2];
            diff[query[0]] -= val;
            diff[query[1] + 1] += val;
        }
        int num = 0;
        for (int i = 0; i < n; i++) {
            num += diff[i];
            if (num > 0) {
                return false;
            }
        }
        return true;
    }

}

性能

3355.零数组变换I

目标

给定一个长度为 n 的整数数组 nums 和一个二维数组 queries,其中 queries[i] = [li, ri]。

对于每个查询 queries[i]:

  • 在 nums 的下标范围 [li, ri] 内选择一个下标 子集。
  • 将选中的每个下标对应的元素值减 1。

零数组 是指所有元素都等于 0 的数组。

如果在按顺序处理所有查询后,可以将 nums 转换为 零数组 ,则返回 true,否则返回 false。

示例 1:

输入: nums = [1,0,1], queries = [[0,2]]
输出: true
解释:
对于 i = 0:
选择下标子集 [0, 2] 并将这些下标处的值减 1。
数组将变为 [0, 0, 0],这是一个零数组。

示例 2:

输入: nums = [4,3,2,1], queries = [[1,3],[0,2]]
输出: false
解释:
对于 i = 0: 
选择下标子集 [1, 2, 3] 并将这些下标处的值减 1。
数组将变为 [4, 2, 1, 0]。
对于 i = 1:
选择下标子集 [0, 1, 2] 并将这些下标处的值减 1。
数组将变为 [3, 1, 0, 0],这不是一个零数组。

说明:

  • 1 <= nums.length <= 10^5
  • 0 <= nums[i] <= 10^5
  • 1 <= queries.length <= 10^5
  • queries[i].length == 2
  • 0 <= li <= ri < nums.length

思路

有一个长度为 n 的整数数组,每一次操作可以将给定范围内的任意元素减 1,判断操作完成后能否将数组中的所有元素变为 0

使用差分数组批量修改元素,最后判断是否存在元素大于 0 即可。

代码


/**
 * @date 2025-05-20 9:37
 */
public class IsZeroArray3355 {

    public boolean isZeroArray(int[] nums, int[][] queries) {
        int n = nums.length;
        int[] diff = new int[n + 1];
        diff[0] = nums[0];
        for (int i = 1; i < n; i++) {
            diff[i] = nums[i] - nums[i - 1];
        }
        for (int[] query : queries) {
            diff[query[0]] -= 1;
            diff[query[1] + 1] += 1;
        }
        int num = 0;
        for (int i = 0; i < n; i++) {
            num += diff[i];
            if (num > 0) {
                return false;
            }
        }
        return true;
    }

}

性能

75.颜色分类

目标

给定一个包含红色、白色和蓝色、共 n 个元素的数组 nums ,原地 对它们进行排序,使得相同颜色的元素相邻,并按照红色、白色、蓝色顺序排列。

我们使用整数 0、 1 和 2 分别表示红色、白色和蓝色。

必须在不使用库内置的 sort 函数的情况下解决这个问题。

示例 1:

输入:nums = [2,0,2,1,1,0]
输出:[0,0,1,1,2,2]

示例 2:

输入:nums = [2,0,1]
输出:[0,1,2]

说明:

  • n == nums.length
  • 1 <= n <= 300
  • nums[i] 为 0、1 或 2

进阶:

  • 你能想出一个仅使用常数空间的一趟扫描算法吗?

思路

给定一个仅包含 012 的数组,对其进行原地排序,使得相同数值的元素集中在一起,并且顺序是 0 1 2

荷兰国旗问题。

代码


/**
 * @date 2025-05-17 15:35
 */
public class SortColors75 {

    public void sortColors(int[] nums) {
        int n = nums.length;
        int pivot = 1;
        int a = 0, i = 0, b = n - 1;
        while (i <= b) {
            if (nums[i] < pivot) {
                swap(nums, a, i);
                a++;
                i++;
            } else if (nums[i] > pivot) {
                swap(nums, i, b);
                b--;
            } else {
                i++;
            }
        }
    }

    public void swap(int[] nums, int i, int j) {
        int tmp = nums[i];
        nums[i] = nums[j];
        nums[j] = tmp;
    }

}

性能

2901.最长相邻不相等子序列II

目标

给你一个整数 n 和一个下标从 0 开始的字符串数组 words ,和一个下标从 0 开始的数组 groups ,两个数组长度都是 n 。

两个长度相等字符串的 汉明距离 定义为对应位置字符 不同 的数目。

你需要从下标 [0, 1, ..., n - 1] 中选出一个 最长子序列 ,将这个子序列记作长度为 k 的 [i0, i1, ..., ik - 1] ,它需要满足以下条件:

  • 相邻 下标对应的 groups 值 不同。即,对于所有满足 0 < j + 1 < k 的 j 都有 groups[ij] != groups[ij + 1] 。
  • 对于所有 0 < j + 1 < k 的下标 j ,都满足 words[ij] 和 words[ij + 1] 的长度 相等 ,且两个字符串之间的 汉明距离 为 1 。

请你返回一个字符串数组,它是下标子序列 依次 对应 words 数组中的字符串连接形成的字符串数组。如果有多个答案,返回任意一个。

子序列 指的是从原数组中删掉一些(也可能一个也不删掉)元素,剩余元素不改变相对位置得到的新的数组。

注意:words 中的字符串长度可能 不相等 。

示例 1:

输入:n = 3, words = ["bab","dab","cab"], groups = [1,2,2]
输出:["bab","cab"]
解释:一个可行的子序列是 [0,2] 。
- groups[0] != groups[2]
- words[0].length == words[2].length 且它们之间的汉明距离为 1 。
所以一个可行的答案是 [words[0],words[2]] = ["bab","cab"] 。
另一个可行的子序列是 [0,1] 。
- groups[0] != groups[1]
- words[0].length = words[1].length 且它们之间的汉明距离为 1 。
所以另一个可行的答案是 [words[0],words[1]] = ["bab","dab"] 。
符合题意的最长子序列的长度为 2 。

示例 2:

输入:n = 4, words = ["a","b","c","d"], groups = [1,2,3,4]
输出:["a","b","c","d"]
解释:我们选择子序列 [0,1,2,3] 。
它同时满足两个条件。
所以答案为 [words[0],words[1],words[2],words[3]] = ["a","b","c","d"] 。
它是所有下标子序列里最长且满足所有条件的。
所以它是唯一的答案。

说明:

  • 1 <= n == words.length == groups.length <= 1000
  • 1 <= words[i].length <= 10
  • 1 <= groups[i] <= n
  • words 中的字符串 互不相同 。
  • words[i] 只包含小写英文字母。

思路

有一个长度为 n 的字符串数组 words,其中的字符串互不相同,同时它对应一个相同长度的二进制数组 groups,从二进制数组中找出一个相邻元素不同且汉明距离为 1 的最长子序列,并返回其在 words 中对应的子序列。

2900.最长相邻不相等子序列I 相比增加了一个限制条件,判断汉明距离是否为 1

代码


/**
 * @date 2025-05-15 11:15
 */
public class GetWordsInLongestSubsequence2901 {

    public List<String> getWordsInLongestSubsequence(String[] words, int[] groups) {
        List<String> res = new ArrayList<>();
        int n = words.length;
        List<String>[] dp = new ArrayList[n];
        Arrays.setAll(dp, x -> new ArrayList<>());
        // 出错点:注意必须要初始化
        for (int i = 0; i < dp.length; i++) {
            dp[i].add(words[i]);
        }
        for (int i = 0; i < n; i++) {
            for (int j = 0; j < i; j++) {
                if (groups[i] != groups[j] && check(words[i], words[j])) {
                    if (dp[j].size() >= dp[i].size()) {
                        dp[i] = new ArrayList<>(dp[j]);
                        dp[i].add(words[i]);
                    }
                }
            }
        }
        for (List<String> list : dp) {
            if (list.size() > res.size()) {
                res = list;
            }
        }
        return res;
    }

    public boolean check(String word1, String word2) {
        if (word1.length() != word2.length()) {
            return false;
        }
        int cnt = 0;
        int n = word1.length();
        for (int i = 0; i < n; i++) {
            if (cnt > 1) {
                return false;
            }
            if (word1.charAt(i) != word2.charAt(i)) {
                cnt++;
            }
        }
        return cnt == 1;
    }

}

性能

3335.字符串转换后的长度I

目标

给你一个字符串 s 和一个整数 t,表示要执行的 转换 次数。每次 转换 需要根据以下规则替换字符串 s 中的每个字符:

  • 如果字符是 'z',则将其替换为字符串 "ab"。
  • 否则,将其替换为字母表中的下一个字符。例如,'a' 替换为 'b','b' 替换为 'c',依此类推。

返回 恰好 执行 t 次转换后得到的字符串的 长度。

由于答案可能非常大,返回其对 10^9 + 7 取余的结果。

示例 1:

输入: s = "abcyy", t = 2
输出: 7
解释:
第一次转换 (t = 1)
'a' 变为 'b'
'b' 变为 'c'
'c' 变为 'd'
'y' 变为 'z'
'y' 变为 'z'
第一次转换后的字符串为:"bcdzz"
第二次转换 (t = 2)
'b' 变为 'c'
'c' 变为 'd'
'd' 变为 'e'
'z' 变为 "ab"
'z' 变为 "ab"
第二次转换后的字符串为:"cdeabab"
最终字符串长度:字符串为 "cdeabab",长度为 7 个字符。

示例 2:

输入: s = "azbk", t = 1
输出: 5
解释:
第一次转换 (t = 1)
'a' 变为 'b'
'z' 变为 "ab"
'b' 变为 'c'
'k' 变为 'l'
第一次转换后的字符串为:"babcl"
最终字符串长度:字符串为 "babcl",长度为 5 个字符。

说明:

  • 1 <= s.length <= 10^5
  • s 仅由小写英文字母组成。
  • 1 <= t <= 10^5

思路

对字符串执行 t 次转换,求字符串的最终长度。每次转换需要将字符转换为字母表中的下一个字符,如果字符是 z 则转换为 ab

直接对字符串中的字符计数,模拟每次转换操作即可。

代码


/**
 * @date 2025-05-13 0:07
 */
public class LengthAfterTransformations3335 {

    public int lengthAfterTransformations(String s, int t) {
        int[] cnt = new int[26];
        int mod = 1000000007;
        for (char c : s.toCharArray()) {
            cnt[c - 'a']++;
        }
        for (int i = 0; i < t; i++) {
            int prev = cnt[0];
            for (int j = 1; j < 26; j++) {
                int tmp = cnt[j];
                cnt[j] = prev;
                prev = tmp;
            }
            cnt[0] = prev;
            cnt[1] = (cnt[1] + prev) % mod;
        }
        int res = 0;
        for (int num : cnt) {
            res = (res + num) % mod;
        }
        return res;
    }

}

性能