3218.切蛋糕的最小总开销 I

目标

有一个 m x n 大小的矩形蛋糕,需要切成 1 x 1 的小块。

给你整数 m ,n 和两个数组:

  • horizontalCut 的大小为 m - 1 ,其中 horizontalCut[i] 表示沿着水平线 i 切蛋糕的开销。
  • verticalCut 的大小为 n - 1 ,其中 verticalCut[j] 表示沿着垂直线 j 切蛋糕的开销。

一次操作中,你可以选择任意不是 1 x 1 大小的矩形蛋糕并执行以下操作之一:

  1. 沿着水平线 i 切开蛋糕,开销为 horizontalCut[i] 。
  2. 沿着垂直线 j 切开蛋糕,开销为 verticalCut[j] 。

每次操作后,这块蛋糕都被切成两个独立的小蛋糕。

每次操作的开销都为最开始对应切割线的开销,并且不会改变。

请你返回将蛋糕全部切成 1 x 1 的蛋糕块的 最小 总开销。

示例 1:

输入:m = 3, n = 2, horizontalCut = [1,3], verticalCut = [5]
输出:13
解释:
沿着垂直线 0 切开蛋糕,开销为 5 。
沿着水平线 0 切开 3 x 1 的蛋糕块,开销为 1 。
沿着水平线 0 切开 3 x 1 的蛋糕块,开销为 1 。
沿着水平线 1 切开 2 x 1 的蛋糕块,开销为 3 。
沿着水平线 1 切开 2 x 1 的蛋糕块,开销为 3 。
总开销为 5 + 1 + 1 + 3 + 3 = 13 。

示例 2:

输入:m = 2, n = 2, horizontalCut = [7], verticalCut = [4]
输出:15
解释:
沿着水平线 0 切开蛋糕,开销为 7 。
沿着垂直线 0 切开 1 x 2 的蛋糕块,开销为 4 。
沿着垂直线 0 切开 1 x 2 的蛋糕块,开销为 4 。
总开销为 7 + 4 + 4 = 15 。

说明:

  • 1 <= m, n <= 20
  • horizontalCut.length == m - 1
  • verticalCut.length == n - 1
  • 1 <= horizontalCut[i], verticalCut[i] <= 10^3

思路

有一块 m x n 的蛋糕,horizontalCut[i] 表示水平切第 i 行的开销,verticalCut[i] 表示垂直切第 i 列的开销。求将蛋糕切成 1 x 1 小块的最小代价。

需要注意每次切的蛋糕必须是整块的,并不能将几块蛋糕排到一起切。

我们应该先沿着代价最大的位置切吗?比如大小为 2 x 100 的蛋糕,水平切的代价为 99,垂直切每一列的代价为 100。先切每一列代价为 99 * 100,然后对切开的 100 块水平切 100 次,代价为 100 * 99,总代价为 2 * 99 * 100。如果先水平切一次,代价为 99。然后需要垂直切 2 * 99 次,代价为 2 * 99 * 100,总代价为 99 + 2 * 99 * 100。这种贪心策略应该是可行的,因为每切一次块数会增加,现在不切代价大的,后面再切的时候代价会增加。

选择沿某一水平或垂直线切割时,需要记录水平 和 垂直方向蛋糕块的数量,用来计算代价。每切一次,横向与纵向的蛋糕块就会增加一个。

刚开始不确定能否使用贪心策略,考虑如何表示哪些切过了,哪些没切,卡了很久。关键点是如何将问题抽象建模,使用分治思想,不要面向过程。每切一次之后,问题转化为切剩余蛋糕的子问题。我们可以使用记忆化搜索解空间,求出最小值。

代码


/**
 * @date 2024-12-25 10:41
 */
public class MinimumCost3218 {

    public int minimumCost_v1(int m, int n, int[] horizontalCut, int[] verticalCut) {
        int horizontalCutPart = 1;
        int verticalCutPart = 1;
        Arrays.sort(horizontalCut);
        Arrays.sort(verticalCut);
        int h = horizontalCut.length - 1;
        int v = verticalCut.length - 1;
        int res = 0;
        while (h >= 0 || v >= 0) {
            if (h < 0){
                res += horizontalCutPart * verticalCut[v];
                v--;
                continue;
            }
            if (v < 0){
                res += verticalCutPart * horizontalCut[h];
                h--;
                continue;
            }
            if (horizontalCut[h] > verticalCut[v]) {
                res += verticalCutPart * horizontalCut[h];
                horizontalCutPart++;
                h--;
            } else if (horizontalCut[h] <= verticalCut[v]) {
                res += horizontalCutPart * verticalCut[v];
                verticalCutPart++;
                v--;
            }
        }

        return res;
    }

    int[] rowCost;
    int[] colCost;
    int[][][][] mem;

    public int minimumCost(int m, int n, int[] horizontalCut, int[] verticalCut) {
        this.rowCost = horizontalCut;
        this.colCost = verticalCut;
        mem = new int[m + 1][m + 1][n + 1][n + 1];
        // rowStart rowEnd colStart colEnd 表示蛋糕的边界
        //   0   1   2   3   4   5  ... n
        // 0  ———————————————————
        //   |   |   |   |   |   |
        // 1  ———————————————————
        //   |   |   |   |   |   |
        // 2  ———————————————————
        //   |   |   |   |   |   |
        // 3 ———————————————————
        //   |   |   |   |   |   |
        // 4 ———————————————————
        //   |   |   |   |   |   |
        // 5  ———————————————————
        // ...
        // m
        return dfs(0, m, 0, n);
    }

    public int dfs(int rowStart, int rowEnd, int colStart, int colEnd) {
        if (rowEnd - rowStart == 1 && colEnd - colStart == 1) {
            return 0;
        }
        int res = Integer.MAX_VALUE;
        for (int i = rowStart + 1; i < rowEnd; i++) {
            if (mem[rowStart][i][colStart][colEnd] == 0) {
                mem[rowStart][i][colStart][colEnd] = dfs(rowStart, i, colStart, colEnd);
            }
            if (mem[i][rowEnd][colStart][colEnd] == 0) {
                mem[i][rowEnd][colStart][colEnd] = dfs(i, rowEnd, colStart, colEnd);
            }
            res = Math.min(res, rowCost[i - 1] + mem[rowStart][i][colStart][colEnd] + mem[i][rowEnd][colStart][colEnd]);
        }
        for (int i = colStart + 1; i < colEnd; i++) {
            if (mem[rowStart][rowEnd][colStart][i] == 0) {
                mem[rowStart][rowEnd][colStart][i] = dfs(rowStart, rowEnd, colStart, i);
            }
            if (mem[rowStart][rowEnd][i][colEnd] == 0) {
                mem[rowStart][rowEnd][i][colEnd] = dfs(rowStart, rowEnd, i, colEnd);
            }
            res = Math.min(res, colCost[i - 1] + mem[rowStart][rowEnd][colStart][i] + mem[rowStart][rowEnd][i][colEnd]);
        }
        return res;
    }

}

性能

1705.吃苹果的最大数目

目标

有一棵特殊的苹果树,一连 n 天,每天都可以长出若干个苹果。在第 i 天,树上会长出 apples[i] 个苹果,这些苹果将会在 days[i] 天后(也就是说,第 i + days[i] 天时)腐烂,变得无法食用。也可能有那么几天,树上不会长出新的苹果,此时用 apples[i] == 0 且 days[i] == 0 表示。

你打算每天 最多 吃一个苹果来保证营养均衡。注意,你可以在这 n 天之后继续吃苹果。

给你两个长度为 n 的整数数组 days 和 apples ,返回你可以吃掉的苹果的最大数目。

示例 1:

输入:apples = [1,2,3,5,2], days = [3,2,1,4,2]
输出:7
解释:你可以吃掉 7 个苹果:
- 第一天,你吃掉第一天长出来的苹果。
- 第二天,你吃掉一个第二天长出来的苹果。
- 第三天,你吃掉一个第二天长出来的苹果。过了这一天,第三天长出来的苹果就已经腐烂了。
- 第四天到第七天,你吃的都是第四天长出来的苹果。

示例 2:

输入:apples = [3,0,0,0,0,2], days = [3,0,0,0,0,2]
输出:5
解释:你可以吃掉 5 个苹果:
- 第一天到第三天,你吃的都是第一天长出来的苹果。
- 第四天和第五天不吃苹果。
- 第六天和第七天,你吃的都是第六天长出来的苹果。

说明:

  • apples.length == n
  • days.length == n
  • 1 <= n <= 2 * 10^4
  • 0 <= apples[i], days[i] <= 2 * 10^4
  • 只有在 apples[i] = 0 时,days[i] = 0 才成立

思路

有一颗特殊的苹果树,第 i 天会结出 apples[i] 个果子,这些果子将在第 i + days[i] 天腐烂。求每天吃一个未腐烂的苹果,最多可以吃几个。

我们的贪心策略是优先吃快腐烂的苹果,首先将当天结果的苹果个数以及腐烂时间放入最小堆,获取堆顶最近要腐烂的苹果,判断是否已经腐烂,如果没有将苹果个数减一,如果数量减为 0 或者已经腐烂,将其从堆中移出。

代码


/**
 * @date 2024-12-24 10:35
 */
public class EatenApples1705 {

    public int eatenApples_v2(int[] apples, int[] days) {
        int n = apples.length;
        PriorityQueue<int[]> q = new PriorityQueue<>((a, b) -> a[1] - b[1]);
        int res = 0;
        for (int day = 0; day < n || !q.isEmpty(); day++) {
            if (day < n && apples[day] > 0){
                q.offer(new int[]{apples[day], day + days[day]});
            }
            while (!q.isEmpty()) {
                int[] applesInfo = q.peek();
                if (applesInfo[0] == 0 || applesInfo[1] == day) {
                    q.poll();
                    continue;
                }
                if (applesInfo[1] > day && applesInfo[0] > 0) {
                    applesInfo[0]--;
                    res++;
                    break;
                }
            }
        }
        return res;
    }

}

性能

855.考场就座

目标

在考场里,一排有 N 个座位,分别编号为 0, 1, 2, ..., N-1 。

当学生进入考场后,他必须坐在能够使他与离他最近的人之间的距离达到最大化的座位上。如果有多个这样的座位,他会坐在编号最小的座位上。(另外,如果考场里没有人,那么学生就坐在 0 号座位上。)

返回 ExamRoom(int N) 类,它有两个公开的函数:其中,函数 ExamRoom.seat() 会返回一个 int (整型数据),代表学生坐的位置;函数 ExamRoom.leave(int p) 代表坐在座位 p 上的学生现在离开了考场。每次调用 ExamRoom.leave(p) 时都保证有学生坐在座位 p 上。

示例:

输入:["ExamRoom","seat","seat","seat","seat","leave","seat"], [[10],[],[],[],[],[4],[]]
输出:[null,0,9,4,2,null,5]
解释:
ExamRoom(10) -> null
seat() -> 0,没有人在考场里,那么学生坐在 0 号座位上。
seat() -> 9,学生最后坐在 9 号座位上。
seat() -> 4,学生最后坐在 4 号座位上。
seat() -> 2,学生最后坐在 2 号座位上。
leave(4) -> null
seat() -> 5,学生最后坐在 5 号座位上。

说明:

  • 1 <= N <= 10^9
  • 在所有的测试样例中 ExamRoom.seat() 和 ExamRoom.leave() 最多被调用 10^4 次。
  • 保证在调用 ExamRoom.leave(p) 时有学生正坐在座位 p 上。

思路

有一排座位编号为 0 ~ N - 1,当学生进入考场后可以选择距离其它同学最远的位置坐下。实现 ExamRoom 类,提供 seat() 方法返回合法的座位,以及 leave() 方法允许学生离开考场。

显然下一个进入考场的同学应该坐在当前相邻座位最大距离的中间位置,考虑到允许离开座位,那么端点的座位离开的情况也要考虑。我们需要提供一个类,快速地查询相邻学生座位的最大值,并且支持座位的删减。

使用最大堆,堆中元素为 [距离左边相邻同学的距离,座位编号],根据 距离/2 排序,除以 2 是为了消除左右距离不等造成的影响。例如 0 4 9 这种情况,区间长度一个是 4,一个是 5,但是 2 ~ 44 ~ 6 的距离相同,应该取编号最小的。

计算下一个座位时,需要考虑三种情况:

  • 如果是第一个同学进入考场,直接坐在 0 号位置
  • 如果只有一个同学在考场,下一个同学需要坐在距离该位置距离最大的端点
  • 如果有大于两个同学在考场,我们可以选择两个同学中间的位置,或者两端的位置

如何处理离开考场的情况?如果离开的同学左右位置有人,需要进行区间合并,将合并后的区间放入堆中,并且删除堆中左右两侧区间。但是在堆中查找元素的时间复杂度是 O(n),由于最多总共调用 10^4 次,leave 最多调用 k = 5 * 10^3 次,堆中元素个数随着调用减少,总复杂度为 O(k^2) 可能勉强通过。

为了快速访问左右的相邻座位以便区间合并以及左右端点的距离判断,使用 TreeSet 记录同学的座位编号。我们可以将删除操作延迟到获取座位的时候处理。

代码


/**
 * @date 2024-12-23 14:28
 */
public class ExamRoom {

    private TreeSet<Integer> occupied;
    // [distance, no]
    private PriorityQueue<int[]> q;
    private int n;

    public ExamRoom(int n) {
        this.n = n;
        occupied = new TreeSet<>();
        q = new PriorityQueue<>((a, b) -> {
            int compare = b[0] / 2 - a[0] / 2;
            if (compare != 0) {
                return compare;
            }
            return a[1] - b[1];
        });
    }

    public int seat() {
        if (occupied.size() == 0) {
            occupied.add(0);
            return 0;
        } else if (occupied.size() == 1) {
            Integer no = occupied.first();
            int distance = n - 1 - no;
            if (distance > no) {
                q.offer(new int[]{distance, n - 1});
                occupied.add(n - 1);
                return n - 1;
            } else {
                q.offer(new int[]{no, no});
                occupied.add(0);
                return 0;
            }
        } else {
            while (true) {
                int first = occupied.first();
                int last = occupied.last();
                int rd = n - 1 - last;
                int[] dn = q.peek();
                int r = dn[1];
                int l = r - dn[0];
                // 注意第三个条件,可以防止离开座位后又有同学坐下,由于延迟删除,距离并未更新导致的计算错误
                if (!occupied.contains(l) || !occupied.contains(r) || occupied.higher(l) != r) {
                    q.poll();
                    continue;
                }
                int distance = dn[0] / 2;
                if (distance <= first || distance < rd) {
                    if (first < rd) {
                        q.offer(new int[]{rd, n - 1});
                        occupied.add(n - 1);
                        return n - 1;
                    } else {
                        q.offer(new int[]{l, l});
                        occupied.add(0);
                        return 0;
                    }
                } else {
                    q.poll();
                    int m = l + (r - l) / 2;
                    occupied.add(m);
                    q.offer(new int[]{m - l, m});
                    q.offer(new int[]{r - m, r});
                    return m;
                }
            }
        }
    }

    public void leave(int p) {
        if (p != occupied.first() && p != occupied.last()) {
            int l = occupied.lower(p);
            int r = occupied.higher(p);
            q.offer(new int[]{r - l, r});
        }
        occupied.remove(p);
    }
}

性能

1387.将整数按权重排序

目标

我们将整数 x 的 权重 定义为按照下述规则将 x 变成 1 所需要的步数:

  • 如果 x 是偶数,那么 x = x / 2
  • 如果 x 是奇数,那么 x = 3 * x + 1

比方说,x=3 的权重为 7 。因为 3 需要 7 步变成 1 (3 --> 10 --> 5 --> 16 --> 8 --> 4 --> 2 --> 1)。

给你三个整数 lo, hi 和 k 。你的任务是将区间 [lo, hi] 之间的整数按照它们的权重 升序排序 ,如果大于等于 2 个整数有 相同 的权重,那么按照数字自身的数值 升序排序 。

请你返回区间 [lo, hi] 之间的整数按权重排序后的第 k 个数。

注意,题目保证对于任意整数 x (lo <= x <= hi) ,它变成 1 所需要的步数是一个 32 位有符号整数。

示例 1:

输入:lo = 12, hi = 15, k = 2
输出:13
解释:12 的权重为 9(12 --> 6 --> 3 --> 10 --> 5 --> 16 --> 8 --> 4 --> 2 --> 1)
13 的权重为 9
14 的权重为 17
15 的权重为 17
区间内的数按权重排序以后的结果为 [12,13,14,15] 。对于 k = 2 ,答案是第二个整数也就是 13 。
注意,12 和 13 有相同的权重,所以我们按照它们本身升序排序。14 和 15 同理。

示例 2:

输入:lo = 7, hi = 11, k = 4
输出:7
解释:区间内整数 [7, 8, 9, 10, 11] 对应的权重为 [16, 3, 19, 6, 14] 。
按权重排序后得到的结果为 [8, 10, 11, 7, 9] 。
排序后数组中第 4 个数字为 7 。

说明:

1 <= lo <= hi <= 1000
1 <= k <= hi - lo + 1

思路

定义整数 x 的权重为 将其变为 1 的操作次数,根据整数的奇偶性,可以执行不同的操作:

  • x 为偶数,x -> x / 2
  • x 为奇数,x -> 3 * x + 1

返回区间 [lo, hi] 之间的整数按权重排序后的第 k 个数。

根据题意模拟计算出每个数字的权重,将它和数字一起保存起来,然后按照权重、数值排序即可。

可以预处理 1 ~ 1000 内的所有权重,保存中间结果减少重复计算。

看到题目时我们都会有这样的疑问,如何证明 x 最终都会回到 1?有网友提到题目中的操作与考拉兹猜想(Collatz conjecture)的操作一样,由于操作过程与冰雹的形成和下落过程相似,因此也叫冰雹猜想。

代码


/**
 * @date 2024-12-22 16:20
 */
public class GetKth1387 {

    public int getKth(int lo, int hi, int k) {
        int n = hi - lo + 1;
        int[][] w = new int[n][2];
        int c = 0;
        for (int i = lo; i <= hi; i++) {
            w[c++] = new int[]{getWeight(i), i};
        }
        Arrays.sort(w, (a, b) -> {
            int compare = a[0] - b[0];
            if (compare != 0) {
                return compare;
            }
            return a[1] - b[1];
        });
        return w[k - 1][1];
    }

    public int getWeight(int x) {
        int cnt = 0;
        while (x > 1) {
            if (x % 2 == 0) {
                x >>= 1;
            } else {
                x = 3 * x + 1;
            }
            cnt++;
        }
        return cnt;
    }
}

性能

2545.根据第K场考试的分数排序

目标

班里有 m 位学生,共计划组织 n 场考试。给你一个下标从 0 开始、大小为 m x n 的整数矩阵 score ,其中每一行对应一位学生,而 score[i][j] 表示第 i 位学生在第 j 场考试取得的分数。矩阵 score 包含的整数 互不相同 。

另给你一个整数 k 。请你按第 k 场考试分数从高到低完成对这些学生(矩阵中的行)的排序。

返回排序后的矩阵。

示例 1:

输入:score = [[10,6,9,1],[7,5,11,2],[4,8,3,15]], k = 2
输出:[[7,5,11,2],[10,6,9,1],[4,8,3,15]]
解释:在上图中,S 表示学生,E 表示考试。
- 下标为 1 的学生在第 2 场考试取得的分数为 11 ,这是考试的最高分,所以 TA 需要排在第一。
- 下标为 0 的学生在第 2 场考试取得的分数为 9 ,这是考试的第二高分,所以 TA 需要排在第二。
- 下标为 2 的学生在第 2 场考试取得的分数为 3 ,这是考试的最低分,所以 TA 需要排在第三。

示例 2:

输入:score = [[3,4],[5,6]], k = 0
输出:[[5,6],[3,4]]
解释:在上图中,S 表示学生,E 表示考试。
- 下标为 1 的学生在第 0 场考试取得的分数为 5 ,这是考试的最高分,所以 TA 需要排在第一。
- 下标为 0 的学生在第 0 场考试取得的分数为 3 ,这是考试的最低分,所以 TA 需要排在第二。

说明:

  • m == score.length
  • n == score[i].length
  • 1 <= m, n <= 250
  • 1 <= score[i][j] <= 10^5
  • score 由 不同 的整数组成
  • 0 <= k < n

思路

有一个二维矩阵 score[i][j],根据第 k 列的值进行排序,返回排序后的数组。

直接调用 API 就是一个简单题,本题应该是考察手写排序吧。

// todo

代码


/**
 * @date 2024-12-21 17:31
 */
public class SortTheStudents2545 {

    public int[][] sortTheStudents(int[][] score, int k) {
        Arrays.sort(score, (a, b) -> b[k] - a[k]);
        return score;
    }
}

性能

3138.同位字符串连接的最小长度

目标

给你一个字符串 s ,它由某个字符串 t 和若干 t 的 同位字符串 连接而成。

请你返回字符串 t 的 最小 可能长度。

同位字符串 指的是重新排列一个单词得到的另外一个字符串,原来字符串中的每个字符在新字符串中都恰好只使用一次。

示例 1:

输入:s = "abba"
输出:2
解释:
一个可能的字符串 t 为 "ba" 。

示例 2:

输入:s = "cdef"
输出:4
解释:
一个可能的字符串 t 为 "cdef" ,注意 t 可能等于 s 。

说明:

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

思路

字符串 s 由某个字符串 t 以及若干(可以为0) t 的同位字符串 连接 而成,返回字符串 t 最小的可能长度。同位字符串指构成字符串的字符分布完全相同,换句话说就是不同字符的种类与数量完全相同。

特别注意该题与字串的顺序有关,比如 aabb 并不能由 ab 拼接而来,它的同位字符串是 abba,只能构成 abba abab baab baba

注意到子串的长度 length 一定能够被 s.length 整除。将字符串截成 k 个长度为 length 的子字符串,通过计算这些子字符串的字母个数,判断是否是同位字符串,从小到大遍历因数 length,取最小的即可。

代码


/**
 * @date 2024-12-20 9:08
 */
public class MinAnagramLength3138 {

    public int minAnagramLength_v1(String s) {
        int n = s.length();
        int[] cnt = new int[26];
        Map<Integer, int[]> possibleLength = new LinkedHashMap<>();
        for (int i = 0; i < n; i++) {
            int c = s.charAt(i) - 'a';
            cnt[c]++;
            int length = i + 1;
            if (n % length == 0) {
                int[] composition = new int[26];
                System.arraycopy(cnt, 0, composition, 0, 26);
                possibleLength.put(length, composition);
            }
        }
        char[] chars = s.toCharArray();
        for (Map.Entry<Integer, int[]> entry : possibleLength.entrySet()) {
            int length = entry.getKey();
            if (length == n) {
                return n;
            }
            int[] composition = entry.getValue();
            int loop = n / length;
            boolean find = true;
            here:
            for (int i = 0; i < loop; i++) {
                int[] tmp = new int[26];
                System.arraycopy(composition, 0, tmp, 0, 26);
                for (int j = 0; j < length; j++) {
                    int c = chars[i * length + j] - 'a';
                    tmp[c]--;
                    if (tmp[c] < 0) {
                        find = false;
                        break here;
                    }
                }
            }
            if (find) {
                return length;
            }
        }
        return n;
    }

}

性能

3291.形成目标字符串需要的最少字符串数I

目标

给你一个字符串数组 words 和一个字符串 target。

如果字符串 x 是 words 中 任意 字符串的 前缀,则认为 x 是一个 有效 字符串。

现计划通过 连接 有效字符串形成 target ,请你计算并返回需要连接的 最少 字符串数量。如果无法通过这种方式形成 target,则返回 -1。

示例 1:

输入: words = ["abc","aaaaa","bcdef"], target = "aabcdabc"
输出: 3
解释:
target 字符串可以通过连接以下有效字符串形成:
words[1] 的长度为 2 的前缀,即 "aa"。
words[2] 的长度为 3 的前缀,即 "bcd"。
words[0] 的长度为 3 的前缀,即 "abc"。

示例 2:

输入: words = ["abababab","ab"], target = "ababaababa"
输出: 2
解释:
target 字符串可以通过连接以下有效字符串形成:
words[0] 的长度为 5 的前缀,即 "ababa"。
words[0] 的长度为 5 的前缀,即 "ababa"。

示例 3:

输入: words = ["abcdef"], target = "xyz"
输出: -1

说明:

  • 1 <= words.length <= 100
  • 1 <= words[i].length <= 5 * 10^3
  • 输入确保 sum(words[i].length) <= 10^5。
  • words[i] 只包含小写英文字母。
  • 1 <= target.length <= 5 * 10^3
  • target 只包含小写英文字母。

思路

有一个字符串数组 words 和目标字符串 target,请你使用最少的字符串前缀组成 target,返回需要的字符串数量,如果无法组成 target 返回 -1

注意前缀是允许重复使用的,状态个数为 target.length ^ 2,深度为 100,直接使用记忆化搜索会超时。

使用字典树加动态规划可以勉强通过,但是明天的通过不了。

//todo KMP 算法 Z 函数 / 字符串哈希+二分 / AC 自动机

代码


/**
 * @date 2024-12-17 8:59
 */
public class MinValidStrings3291 {

    static public class Trie {
        public boolean isLeaf;
        public Trie[] children;

        public Trie() {
            this.children = new Trie[26];
        }

        public Trie build(String[] dict) {
            Trie root = this;
            for (String word : dict) {
                root = this;
                char[] chars = word.toCharArray();
                for (int i = 0; i < chars.length; i++) {
                    int c = chars[i] - 'a';
                    if (root.children[c] == null) {
                        root.children[c] = new Trie();
                    }
                    root = root.children[c];
                }
                root.isLeaf = true;
            }
            return root;
        }

        public int exists(char[] target, int start) {
            int n = target.length;
            int length = 0;
            Trie root = this;
            int c = target[start] - 'a';
            while (root.children[c] != null) {
                root = root.children[c];
                length++;
                start++;
                if (start == n) {
                    break;
                }
                c = target[start] - 'a';
            }
            return length;
        }

    }

    public int minValidStrings_v1(String[] words, String target) {
        Trie root = new Trie();
        root.build(words);
        char[] chars = target.toCharArray();
        int n = chars.length;
        int[] dp = new int[n];
        Arrays.fill(dp, Integer.MAX_VALUE);
        int length = root.exists(chars, 0);
        for (int i = 0; i < length; i++) {
            dp[i] = 1;
        }

        for (int i = 1; i < n; i++) {
            if (dp[i - 1] == Integer.MAX_VALUE) {
                return -1;
            }
            length = root.exists(chars, i);
            for (int j = i; j < i + length; j++) {
                dp[j] = Math.min(dp[j], dp[i - 1] + 1);
            }
        }

        return dp[n - 1] == Integer.MAX_VALUE ? -1 : dp[n - 1];
    }

}

性能

1338.数组大小减半

目标

给你一个整数数组 arr。你可以从中选出一个整数集合,并删除这些整数在数组中的每次出现。

返回 至少 能删除数组中的一半整数的整数集合的最小大小。

示例 1:

输入:arr = [3,3,3,3,5,5,5,2,2,7]
输出:2
解释:选择 {3,7} 使得结果数组为 [5,5,5,2,2]、长度为 5(原数组长度的一半)。
大小为 2 的可行集合有 {3,5},{3,2},{5,2}。
选择 {2,7} 是不可行的,它的结果数组为 [3,3,3,3,5,5,5],新数组长度大于原数组的二分之一。

示例 2:

输入:arr = [7,7,7,7,7,7]
输出:1
解释:我们只能选择集合 {7},结果数组为空。

说明:

  • 1 <= arr.length <= 10^5
  • arr.length 为偶数
  • 1 <= arr[i] <= 10^5

思路

从整数数组中选出一个元素集合,使该集合中元素在原数组中的出现次数超过原数组长度的一半,求集合大小的最小值。

统计每个元素的出现次数,将出现次数从大到小排序,然后开始选元素直到满足题目条件。

代码


/**
 * @date 2024-12-15 0:17
 */
public class MinSetSize1338 {

    public int minSetSize(int[] arr) {
        int n = arr.length;
        int[] cnt = new int[100001];
        for (int i : arr) {
            cnt[i]++;
        }
        PriorityQueue<Integer> q = new PriorityQueue<>((a, b) -> b - a);
        for (int i : cnt) {
            if (i > 0) {
                q.offer(i);
            }
        }
        int res = 0;
        int l = 0;
        while (!q.isEmpty()) {
            l += q.poll();
            res++;
            if (l >= n / 2) {
                break;
            }
        }
        return res;
    }
}

性能

935.骑士拨号器

目标

象棋骑士有一个独特的移动方式,它可以垂直移动两个方格,水平移动一个方格,或者水平移动两个方格,垂直移动一个方格(两者都形成一个 L 的形状)。

象棋骑士可能的移动方式如下图所示:

我们有一个象棋骑士和一个电话垫,如下所示,骑士只能站在一个数字单元格上(即蓝色单元格)。

给定一个整数 n,返回我们可以拨多少个长度为 n 的不同电话号码。

你可以将骑士放置在任何数字单元格上,然后你应该执行 n - 1 次移动来获得长度为 n 的号码。所有的跳跃应该是有效的骑士跳跃。

因为答案可能很大,所以输出答案模 10^9 + 7.

示例 1:

输入:n = 1
输出:10
解释:我们需要拨一个长度为1的数字,所以把骑士放在10个单元格中的任何一个数字单元格上都能满足条件。

示例 2:

输入:n = 2
输出:20
解释:我们可以拨打的所有有效号码为[04, 06, 16, 18, 27, 29, 34, 38, 40, 43, 49, 60, 61, 67, 72, 76, 81, 83, 92, 94]

示例 3:

输入:n = 3131
输出:136006598
解释:注意取模

说明:

  • 1 <= n <= 5000

思路

有一个电话键盘,布局如下:

1  2  3
4  5  6
7  8  9
*  0  #

开始时,将一个骑士棋子放在数字键上,然后按照国际象棋骑士的走法(类似于中国象棋里的马)走 n - 1 步,问能够组成多少种长度为 n 的不同号码(不能走到 *#)。

这道题与 688.骑士在棋盘上的概率 类似,只不过棋盘更小,状态转移是确定的。

定义 dp[k][i] 表示以 i 结尾长度为 k 的号码组合数。初始时,dp[1][i] 均为 1。状态转移方程,针对不同的数字有所不同,例如 dp[k][1] = dp[k - 1][8] + dp[k - 1][6]dp[k][4] = dp[k - 1][3] + dp[k - 1][9] + dp[k - 1][0]等等。

1 - 6 - 7
|   |   |
8   0   2
|   |   |
3 - 4 - 9

仔细分析可以得到上面的状态转化图,1 3 7 9 的结果是完全相同的,同理 2 8 也可以认为是一个状态,4 6 是一个状态,0 是一个状态。定义以上 4 个状态为 a b c d,那么最终结果可以表示为 4 * dp[n][a] + 2 * dp[n][b] + 2 * dp[n][c] + dp[n][d],状态转移方程为:

  • dp[k][a] = dp[k - 1][b] + dp[k - 1][c]
  • dp[k][b] = 2 * dp[k - 1][a]
  • dp[k][c] = 2 * dp[k - 1][a] + dp[k - 1][d]
  • dp[k][d] = 2 * dp[k - 1][c]

注意 k 的初值为 2k == 1 是特殊情况,骑士可以在数字 5,但是无法继续移动。dp[2][a] = 2, dp[2][b] = 2, dp[2][c] = 3, dp[2][d] = 2

如果写成矩阵的形式 列向量 dp[k] 等于 A · dp[k - 1],其中矩阵 A 为:

0 1 1 0
2 0 0 0
2 0 0 1
0 0 2 0

因此 dp[n] = A^(n - 1) · dp[1],其中 dp[1] = [1, 1, 1, 1]。我们可以使用矩阵的快速幂求解。

代码


/**
 * @date 2024-12-10 9:39
 */
public class KnightDialer935 {

    public static int mod = 1000000007;

    public int knightDialer_v3(int n) {
        if (n == 1) {
            return 10;
        }
        long[][] A = new long[][]{
                {0, 1, 1, 0},
                {2, 0, 0, 0},
                {2, 0, 0, 1},
                {0, 0, 2, 0},
        };
        A = pow(A, n - 1);
        long[] res = new long[4];
        for (int i = 0; i < 4; i++) {
            for (int j = 0; j < 4; j++) {
                res[i] += A[i][j];
            }
        }
        return (int) ((4 * res[0] + 2 * res[1] + 2 * res[2] + res[3]) % mod);
    }

    public long[][] pow(long[][] A, int exponent) {
        long[][] res = new long[][]{
                {1, 0, 0, 0},
                {0, 1, 0, 0},
                {0, 0, 1, 0},
                {0, 0, 0, 1},
        };
        while (exponent > 0) {
            if ((exponent & 1) == 1) {
                res = mul(A, res);
            }
            A = mul(A, A);
            exponent >>= 1;
        }
        return res;
    }

    public long[][] mul(long[][] A1, long[][] A2) {
        long[][] res = new long[4][4];
        for (int i = 0; i < 4; i++) {
            for (int k = 0; k < 4; k++) {
                if (A1[i][k] == 0) {
                    continue;
                }
                for (int j = 0; j < 4; j++) {
                    res[i][j] = (res[i][j] + A1[i][k] * A2[k][j]) % mod;
                }
            }
        }
        return res;
    }

    public static long[][] dp;
    public static int MAX = 5001;

    static {
        dp = new long[MAX][4];
        dp[2][0] = 2;
        dp[2][1] = 2;
        dp[2][2] = 3;
        dp[2][3] = 2;
        for (int k = 3; k < MAX; k++) {
            dp[k][0] = (dp[k - 1][1] + dp[k - 1][2]) % mod;
            dp[k][1] = (2 * dp[k - 1][0]) % mod;
            dp[k][2] = (2 * dp[k - 1][0] + dp[k - 1][3]) % mod;
            dp[k][3] = (2 * dp[k - 1][2]) % mod;
        }
    }

    public int knightDialer_v2(int n) {
        if (n == 1) {
            return 10;
        }
        return (int)((4 * dp[n][0] + 2 * dp[n][1] + 2 * dp[n][2] + dp[n][3]) % mod);
    }

    public int knightDialer_v1(int n) {
        long[][] dp = new long[n + 1][10];
        dp[1] = new long[]{1, 1, 1, 1, 1, 1, 1, 1, 1, 1};
        int MOD = 1000000007;
        int res = 0;
        for (int k = 2; k <= n; k++) {
            dp[k][0] = (dp[k - 1][4] + dp[k - 1][6]) % MOD;
            dp[k][1] = (dp[k - 1][6] + dp[k - 1][8]) % MOD;
            dp[k][2] = (dp[k - 1][7] + dp[k - 1][9]) % MOD;
            dp[k][3] = (dp[k - 1][4] + dp[k - 1][8]) % MOD;
            dp[k][4] = (dp[k - 1][3] + dp[k - 1][9] + dp[k - 1][0]) % MOD;
            dp[k][6] = (dp[k - 1][1] + dp[k - 1][7] + dp[k - 1][0]) % MOD;
            dp[k][7] = (dp[k - 1][2] + dp[k - 1][6]) % MOD;
            dp[k][8] = (dp[k - 1][1] + dp[k - 1][3]) % MOD;
            dp[k][9] = (dp[k - 1][2] + dp[k - 1][4]) % MOD;
        }
        for (int i = 0; i < 10; i++) {
            res = (int) (res + dp[n][i]) % MOD;
        }
        return res;
    }

}

性能

688.骑士在棋盘上的概率

目标

在一个 n x n 的国际象棋棋盘上,一个骑士从单元格 (row, column) 开始,并尝试进行 k 次移动。行和列是 从 0 开始 的,所以左上单元格是 (0,0) ,右下单元格是 (n - 1, n - 1) 。

象棋骑士有8种可能的走法,如下图所示。每次移动在基本方向上是两个单元格,然后在正交方向上是一个单元格。

每次骑士要移动时,它都会随机从8种可能的移动中选择一种(即使棋子会离开棋盘),然后移动到那里。

骑士继续移动,直到它走了 k 步或离开了棋盘。

返回 骑士在棋盘停止移动后仍留在棋盘上的概率 。

示例 1:

输入: n = 3, k = 2, row = 0, column = 0
输出: 0.0625
解释: 有两步(到(1,2),(2,1))可以让骑士留在棋盘上。
在每一个位置上,也有两种移动可以让骑士留在棋盘上。
骑士留在棋盘上的总概率是0.0625。

示例 2:

输入: n = 1, k = 0, row = 0, column = 0
输出: 1.00000

说明:

  • 1 <= n <= 25
  • 0 <= k <= 100
  • 0 <= row, column <= n - 1

思路

n x n 棋盘上的 (row, column) 位置上有一个骑士(坐标从 0 开始),求它朝 8 个方向任意走 k 次还停留在棋盘上的概率是多少。所谓的 8 个方向类似中国象棋中的马走 ,不过没有蹩马腿的限制。

定义 dp[i][j][k] 表示当前在 (i, j)k 步后最终在棋盘上的概率。初始 dp[i][j][0] = 1dp[i][j][k] = Σdp[.][.][k - 1] / 8,最终的结果为 dp[row][column][k]

代码


/**
 * @date 2024-12-07 20:48
 */
public class KnightProbability688 {

    public double knightProbability(int n, int k, int row, int column) {
        int[][] direction = new int[][]{{-1, -2}, {-2, -1}, {-1, 2}, {-2, 1}, {1, 2}, {2, 1}, {1, -2}, {2, -1}};
        double[][][] dp = new double[n][n][k + 1];
        for (int i = 0; i < n; i++) {
            for (int j = 0; j < n; j++) {
                dp[i][j][0] = 1.0;
            }
        }
        for (int step = 1; step <= k; step++) {
            for (int i = 0; i < n; i++) {
                for (int j = 0; j < n; j++) {
                    for (int d = 0; d < 8; d++) {
                        int dx = direction[d][0];
                        int dy = direction[d][1];
                        int x = i + dx;
                        int y = j + dy;
                        if (x >= 0 && x < n && y >= 0 && y < n) {
                            dp[i][j][step] += dp[x][y][step - 1];
                        }
                    }
                    dp[i][j][step] /= 8;
                }
            }
        }
        return dp[row][column][k];
    }

}

性能