807.保持城市天际线

目标

给你一座由 n x n 个街区组成的城市,每个街区都包含一座立方体建筑。给你一个下标从 0 开始的 n x n 整数矩阵 grid ,其中 grid[r][c] 表示坐落于 r 行 c 列的建筑物的 高度 。

城市的 天际线 是从远处观察城市时,所有建筑物形成的外部轮廓。从东、南、西、北四个主要方向观测到的 天际线 可能不同。

我们被允许为 任意数量的建筑物 的高度增加 任意增量(不同建筑物的增量可能不同) 。 高度为 0 的建筑物的高度也可以增加。然而,增加的建筑物高度 不能影响 从任何主要方向观察城市得到的 天际线 。

在 不改变 从任何主要方向观测到的城市 天际线 的前提下,返回建筑物可以增加的 最大高度增量总和 。

示例 1:

输入:grid = [[3,0,8,4],[2,4,5,7],[9,2,6,3],[0,3,1,0]]
输出:35
解释:建筑物的高度如上图中心所示。
用红色绘制从不同方向观看得到的天际线。
在不影响天际线的情况下,增加建筑物的高度:
gridNew = [ [8, 4, 8, 7],
            [7, 4, 7, 7],
            [9, 4, 8, 7],
            [3, 3, 3, 3] ]

示例 2:

输入:grid = [[0,0,0],[0,0,0],[0,0,0]]
输出:0
解释:增加任何建筑物的高度都会导致天际线的变化。

说明:

  • n == grid.length
  • n == grid[r].length
  • 2 <= n <= 50
  • 0 <= grid[r][c] <= 100

思路

有一个 n * n 的二维矩阵,要求保持从四个方向看轮廓不变的情况下,最大可增加的元素值总和。

可以分别求出各行、列的最大值,然后累加每个元素与其所在行列最大值中较小值的差值即可。

代码

/**
 * @date 2024-07-14 14:22
 */
public class MaxIncreaseKeepingSkyline807 {

    public int maxIncreaseKeepingSkyline(int[][] grid) {
        int n = grid.length;
        int[] rowMax = new int[n];
        int[] colMax = new int[n];
        for (int i = 0; i < n; i++) {
            for (int j = 0; j < n; j++) {
                rowMax[i] = Math.max(rowMax[i], grid[i][j]);
                colMax[i] = Math.max(colMax[i], grid[j][i]);
            }
        }
        int res = 0;
        for (int i = 0; i < n; i++) {
            for (int j = 0; j < n; j++) {
                res += Math.min(rowMax[i], colMax[j]) - grid[i][j];
            }
        }
        return res;
    }
}

性能

3011.判断一个数组是否可以变为有序

目标

给你一个下标从 0 开始且全是 正 整数的数组 nums 。

一次 操作 中,如果两个 相邻 元素在二进制下数位为 1 的数目 相同 ,那么你可以将这两个元素交换。你可以执行这个操作 任意次 (也可以 0 次)。

如果你可以使数组变有序,请你返回 true ,否则返回 false 。

示例 1:

输入:nums = [8,4,2,30,15]
输出:true
解释:我们先观察每个元素的二进制表示。 2 ,4 和 8 分别都只有一个数位为 1 ,分别为 "10" ,"100" 和 "1000" 。15 和 30 分别有 4 个数位为 1 :"1111" 和 "11110" 。
我们可以通过 4 个操作使数组有序:
- 交换 nums[0] 和 nums[1] 。8 和 4 分别只有 1 个数位为 1 。数组变为 [4,8,2,30,15] 。
- 交换 nums[1] 和 nums[2] 。8 和 2 分别只有 1 个数位为 1 。数组变为 [4,2,8,30,15] 。
- 交换 nums[0] 和 nums[1] 。4 和 2 分别只有 1 个数位为 1 。数组变为 [2,4,8,30,15] 。
- 交换 nums[3] 和 nums[4] 。30 和 15 分别有 4 个数位为 1 ,数组变为 [2,4,8,15,30] 。
数组变成有序的,所以我们返回 true 。
注意我们还可以通过其他的操作序列使数组变得有序。

示例 2:

输入:nums = [1,2,3,4,5]
输出:true
解释:数组已经是有序的,所以我们返回 true 。

示例 3:

输入:nums = [3,16,8,4,2]
输出:false
解释:无法通过操作使数组变为有序。

说明:

  • 1 <= nums.length <= 100
  • 1 <= nums[i] <= 2^8

思路

有一个正整数数组,如果相邻元素的二进制表示中1的个数相同,则可以将二者交换。问能否将数组变为有序。

这里没有说是正序还是倒序,也没说是否是严格有序。首先我们需要将各元素的二进制表示中1的个数给求出来,将个数相同的相邻元素分为一组,找出最大与最小值。

可以根据前两个分组确定是正序还是倒序,第一个分组的最小值大于另一分组的最大值,或者第一分组的最大值小于另一分组的最小值,否则无法变为有序。然后按照正序/倒序来判断后续分组是否满足条件。

提交之后发现题目中所谓的有序指的是非严格正序。

这样的话就没必要记录最小值了,最大值也无需使用数组保存了,依次与后续分组中所有元素比较即可。也有网友使用了分组局部排序,然后再重新两两比较,看是否非严格递增。

知识点:

  • 数字的二进制表示中有几个1可以直接使用 Integer.bitCount API

代码

/**
 * @date 2024-07-13 5:29
 */
public class CanSortArray3011 {

    public boolean canSortArray_v2(int[] nums) {
        int n = nums.length;
        int preMax = 0;
        for (int i = 0; i < n; ) {
            int max = nums[i];
            int cnt = Integer.bitCount(nums[i]);
            while (i < n && cnt == Integer.bitCount(nums[i])) {
                if (preMax > nums[i]) {
                    return false;
                }
                max = Math.max(max, nums[i++]);
            }
            preMax = max;
        }
        return true;
    }

    public boolean canSortArray_v1(int[] nums) {
        int n = nums.length;
        int[] oneNums = new int[n];
        List<int[]> group = new ArrayList<>();
        oneNums[0] = Integer.bitCount(nums[0]);
        int max = nums[0];
        int min = nums[0];
        for (int i = 1; i < n; i++) {
            oneNums[i] = Integer.bitCount(nums[i]);
            if (oneNums[i] != oneNums[i - 1]) {
                group.add(new int[]{min, max});
                min = nums[i];
                max = nums[i];
            }
            max = Math.max(max, nums[i]);
            min = Math.min(min, nums[i]);
        }
        group.add(new int[]{min, max});
        if (group.size() == 1) {
            return true;
        }
        int[] pre = new int[]{Integer.MAX_VALUE, 0};
        for (int[] value : group) {
            if (value[0] - pre[1] < 0) {
                return false;
            }
            pre = value;
        }
        return true;
    }

}

性能

1958.检查操作是否合法

目标

给你一个下标从 0 开始的 8 x 8 网格 board ,其中 board[r][c] 表示游戏棋盘上的格子 (r, c) 。棋盘上空格用 '.' 表示,白色格子用 'W' 表示,黑色格子用 'B' 表示。

游戏中每次操作步骤为:选择一个空格子,将它变成你正在执行的颜色(要么白色,要么黑色)。但是,合法 操作必须满足:涂色后这个格子是 好线段的一个端点 (好线段可以是水平的,竖直的或者是对角线)。

好线段 指的是一个包含 三个或者更多格子(包含端点格子)的线段,线段两个端点格子为 同一种颜色 ,且中间剩余格子的颜色都为 另一种颜色 (线段上不能有任何空格子)。你可以在下图找到好线段的例子:

给你两个整数 rMove 和 cMove 以及一个字符 color ,表示你正在执行操作的颜色(白或者黑),如果将格子 (rMove, cMove) 变成颜色 color 后,是一个 合法 操作,那么返回 true ,如果不是合法操作返回 false 。

示例 1:

输入:board = [[".",".",".","B",".",".",".","."],[".",".",".","W",".",".",".","."],[".",".",".","W",".",".",".","."],[".",".",".","W",".",".",".","."],["W","B","B",".","W","W","W","B"],[".",".",".","B",".",".",".","."],[".",".",".","B",".",".",".","."],[".",".",".","W",".",".",".","."]], rMove = 4, cMove = 3, color = "B"
输出:true
解释:'.','W' 和 'B' 分别用颜色蓝色,白色和黑色表示。格子 (rMove, cMove) 用 'X' 标记。
以选中格子为端点的两个好线段在上图中用红色矩形标注出来了。

示例 2:

输入:board = [[".",".",".",".",".",".",".","."],[".","B",".",".","W",".",".","."],[".",".","W",".",".",".",".","."],[".",".",".","W","B",".",".","."],[".",".",".",".",".",".",".","."],[".",".",".",".","B","W",".","."],[".",".",".",".",".",".","W","."],[".",".",".",".",".",".",".","B"]], rMove = 4, cMove = 4, color = "W"
输出:false
解释:虽然选中格子涂色后,棋盘上产生了好线段,但选中格子是作为中间格子,没有产生以选中格子为端点的好线段。

说明:

  • board.length == board[r].length == 8
  • 0 <= rMove, cMove < 8
  • board[rMove][cMove] == '.'
  • color 要么是 'B' 要么是 'W' 。

思路

有一个 8 x 8 网格,网格中 . 表示空格,W 表示白色,B 表示黑色。现在需要给空格涂色,合法的操作指涂色的格子是好线段的端点。好线段长度至少包含三个格子,且两端颜色一致,中间颜色也一致但与两端颜色不同。给定一个操作,判断其是否合法。

按照题意从涂色端点开始依次遍历八个方向上是否存在好线段。

代码

/**
 * @date 2024-07-07 12:21
 */
public class CheckMove1958 {

    public boolean checkMove(char[][] board, int rMove, int cMove, char color) {
        if (board[rMove][cMove] != '.') {
            return false;
        }
        int[][] direction = new int[][]{
                {-1, 0}, {-1, 1}, {0, 1}, {1, 1}, {1, 0}, {1, -1}, {0, -1}, {-1, -1}
        };
        char opposite = (char) (color ^ 'B' ^ 'W');
        for (int[] d : direction) {
            int cnt = 0;
            int r = d[0];
            int c = d[1];
            while (rMove + r >= 0 && rMove + r < 8
                    && cMove + c >= 0 && cMove + c < 8
                    && board[rMove + r][cMove + c] == opposite) {
                r += d[0];
                c += d[1];
                cnt++;
            }
            // 题目要求至少三个格子,所以要求cnt>0
            if (cnt > 0 && rMove + r >= 0 && rMove + r < 8
                    && cMove + c >= 0 && cMove + c < 8
                    && board[rMove + r][cMove + c] == color) {
                return true;
            }
        }
        return false;
    }
}

性能

3101.交替子数组计数

目标

给你一个 二进制数组 nums 。

如果一个 子数组 中 不存在 两个 相邻 元素的值 相同 的情况,我们称这样的子数组为 交替子数组 。

返回数组 nums 中交替子数组的数量。

示例 1:

输入: nums = [0,1,1,1]
输出: 5
解释:
以下子数组是交替子数组:[0] 、[1] 、[1] 、[1] 以及 [0,1] 。

示例 2:

输入: nums = [1,0,1,0]
输出: 10
解释:
数组的每个子数组都是交替子数组。可以统计在内的子数组共有 10 个。

说明:

  • 1 <= nums.length <= 10^5
  • nums[i] 不是 0 就是 1 。

思路

返回二进制数组中交替子数组的数量。

记录相邻元素相同的下标,计算子数组的数量。拥有n个元素的数组,其子数组数量为 n * (n + 1) / 2

官网题解是另一种思路:

  • 如果相邻的元素 nums[i-1] ≠ nums[i],那么可以将 nums[i] 加到所有以 i-1 为右端点的子数组末尾,再加上nums[i]自身,即 以 i 为右端点的交替子数组数量 = 以 i-1 为右端点的交替子数组数量 + 1
  • 如果相邻元素相等,则记录以i为右端点的交替子数组数量为1。

其实本质都是一样的,一个是使用公式求解,一个是通过循环累加。

代码

/**
 * @date 2024-07-06 20:52
 */
public class CountAlternatingSubarrays3101 {
    /**
     * 官网题解
     */
    public long countAlternatingSubarrays_v1(int[] nums) {
        long res = 0, cur = 0;
        int pre = -1;
        for (int a : nums) {
            cur = (pre != a) ? cur + 1 : 1;
            pre = a;
            res += cur;
        }
        return res;
    }

    public long countAlternatingSubarrays(int[] nums) {
        int n = nums.length;
        long res = 0;
        int s = 0;
        for (int i = 1; i < n; i++) {
            if (nums[i] == nums[i - 1]) {
                // 这里个数的计算是从 s ~ i-1的元素个数 i - 1 - s + 1
                int k = i - s;
                res += k * (k + 1) / 2;
                s = i;
            }
        }
        // 注意处理结尾的情况
        if (s != n - 1) {
            // 这里计算的是 s ~ n-1 的元素个数 n - 1 - s + 1
            int k = n - s;
            // 这里要防止溢出,使用1L
            res += k * (k + 1L) / 2;
        } else {
            res++;
        }
        return res;
    }
}

性能

3115.质数的最大距离

目标

给你一个整数数组 nums。

返回两个(不一定不同的)质数在 nums 中 下标 的 最大距离。

示例 1:

输入: nums = [4,2,9,5,3]
输出: 3
解释: nums[1]、nums[3] 和 nums[4] 是质数。因此答案是 |4 - 1| = 3。

示例 2:

输入: nums = [4,8,2,8]
输出: 0
解释: nums[2] 是质数。因为只有一个质数,所以答案是 |2 - 2| = 0。

说明:

  • 1 <= nums.length <= 3 * 10^5
  • 1 <= nums[i] <= 100
  • 输入保证 nums 中至少有一个质数。

思路

找出数组中质数的最远距离(下标之差)。

知识点:

  • 自然数:非负整数
  • 质数:只能被1和它本身整除的大于1的自然数
  • 合数:不是质数的大于1的自然数

如果 n 是一个合数,那么它可以分解为两个自然数 a、b 的乘积,即 n = a * b。设 a ≤ b ,如果 a ≥ √n ,那么 a * b ≥ n。 也就是说 a、b 要么同时等于 √n,要么一个大于一个小于 √n不可能同时大于√n。于是判断一个数是否是质数,只需判断 n 是否能够整除 1 ~ √n

除了2的偶数都不是质数,因此自增的步长可以设为2。

更进一步分析,所有质数除了2和3外,都形如 6k - 16k + 1。考虑 n % 6

  • 余数为0,首先6不是质数,能被6整除的数也不是质数
  • 余数为2、4,表明能够被2整除,不是质数
  • 余数为3,表明能被3整除,不是质数
  • 余数为5,6k - 1
  • 余数为1,6k + 1

从5开始,步长可以设为6。

代码

/**
 * @date 2024-07-02 9:13
 */
public class MaximumPrimeDifference3115 {

    public int maximumPrimeDifference_v2(int[] nums) {
        int i = 0, j = nums.length - 1;
        while (!isPrimeNumber(nums[i])) {
            i++;
        }
        while (!isPrimeNumber(nums[j])) {
            j--;
        }
        return j - i;
    }

    public boolean isPrimeNumber(int num) {
        if (num == 1) {
            return false;
        }
        if (num == 2) {
            return true;
        }
        if (num % 2 == 0) {
            return false;
        }
        for (int i = 3; i * i <= num; i += 2) {
            if (num % i == 0) {
                return false;
            }
        }
        return true;
    }

    public static boolean isPrimeNumber_v1(int num) {
        if (num <= 1) {
            return false;
        }
        if (num <= 3) {
            return true; 
        }
        if (num % 2 == 0 || num % 3 == 0) {
            return false; 
        }
        for (int i = 5; i * i <= num; i += 6) {
            // i = 6k - 1, i + 2 = 6k + 1
            if (num % i == 0 || num % (i + 2) == 0) {
                return false;
            }
        }
        return true;
    }
}

性能

494.目标和

目标

给你一个非负整数数组 nums 和一个整数 target 。

向数组中的每个整数前添加 '+' 或 '-' ,然后串联起所有整数,可以构造一个 表达式 :

  • 例如,nums = [2, 1] ,可以在 2 之前添加 '+' ,在 1 之前添加 '-' ,然后串联起来得到表达式 "+2-1" 。

返回可以通过上述方法构造的、运算结果等于 target 的不同 表达式 的数目。

示例 1:

输入:nums = [1,1,1,1,1], target = 3
输出:5
解释:一共有 5 种方法让最终目标和为 3 。
-1 + 1 + 1 + 1 + 1 = 3
+1 - 1 + 1 + 1 + 1 = 3
+1 + 1 - 1 + 1 + 1 = 3
+1 + 1 + 1 - 1 + 1 = 3
+1 + 1 + 1 + 1 - 1 = 3

示例 2:

输入:nums = [1], target = 1
输出:1

说明:

  • 1 <= nums.length <= 20
  • 0 <= nums[i] <= 1000
  • 0 <= sum(nums[i]) <= 1000
  • -1000 <= target <= 1000

思路

有一个数组,可以在数组元素前加上正负号来组成表达式,问表达式等于target的数目。

如果当前元素为正则累加,否则相减,递归直到所有元素都已列入表达式,如果累加结果等于target则返回1,否则返回0。

//todo 改为递推,或记忆化搜索

代码

/**
 * @date 2024-06-30 20:07
 */
public class FindTargetSumWays494 {
    public int findTargetSumWays(int[] nums, int target) {
        return dfs(nums, 1, nums[0], target) + dfs(nums, 1, -nums[0], target);
    }

    public int dfs(int[] nums, int i, int res, int target) {
        if (i == nums.length) {
            return res - target == 0 ? 1 : 0;
        }
        return dfs(nums, i + 1, res + nums[i], target) + dfs(nums, i + 1, res - nums[i], target);
    }

}

性能

2734.执行子串操作后的字典序最小字符串

目标

给你一个仅由小写英文字母组成的字符串 s 。在一步操作中,你可以完成以下行为:

  • 选择 s 的任一非空子字符串,可能是整个字符串,接着将字符串中的每一个字符替换为英文字母表中的前一个字符。例如,'b' 用 'a' 替换,'a' 用 'z' 替换。

返回执行上述操作 恰好一次 后可以获得的 字典序最小 的字符串。

子字符串 是字符串中的一个连续字符序列。

现有长度相同的两个字符串 x 和 字符串 y ,在满足 x[i] != y[i] 的第一个位置 i 上,如果 x[i] 在字母表中先于 y[i] 出现,则认为字符串 x 比字符串 y 字典序更小 。

示例 1:

输入:s = "cbabc"
输出:"baabc"
解释:我们选择从下标 0 开始、到下标 1 结束的子字符串执行操作。 
可以证明最终得到的字符串是字典序最小的。

示例 2:

输入:s = "acbbc"
输出:"abaab"
解释:我们选择从下标 1 开始、到下标 4 结束的子字符串执行操作。
可以证明最终得到的字符串是字典序最小的。

示例 3:

输入:s = "leetcode"
输出:"kddsbncd"
解释:我们选择整个字符串执行操作。
可以证明最终得到的字符串是字典序最小的。

说明:

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

思路

求对一个字符串的 非空子串 进行操作后字典序最小的字符串,两个字符串 第一个不同的字母 在字母表 越先出现 其字典序就越小。字符串仅由小写字母组成,可进行的操作指将子串的每一个字母替换为其在字母表中的前一个字母,a 前面的字母定义为 z

关键在于非空子串如何选,根据题意可知,如果子串不含字母 a 操作总能使字典序变小。字符串前面字符的字典序越小整个字符串的字典序就越小,因此可以从前向后遍历直到遇到字母 a 作为子串。如果字符串字母均为 a,操作总会使字典序变大,显然将最后一个 a 改为 z 可以使操作后的字符串字典序最小。

代码

/**
 * @date 2024-06-27 0:05
 */
public class SmallestString2734 {

    public String smallestString_v1(String s) {
        int i = 0;
        int n = s.length();
        char[] chars = s.toCharArray();
        while (i < n && chars[i] == 'a') {
            i++;
        }
        while (i < n && chars[i] != 'a') {
            chars[i++]--;
        }
        if (i == n && s.charAt(n - 1) == 'a') {
            chars[n - 1] = 'z';
        }
        return new String(chars);
    }

    public String smallestString_v2(String s) {
        int i = 0;
        int n = s.length();
        StringBuilder sb = new StringBuilder(s);
        while (i < n && s.charAt(i) == 'a') {
            i++;
        }
        if (i == n) {
            sb.setCharAt(n - 1, 'z');
            return sb.toString();
        }
        while (i < n && s.charAt(i) != 'a') {
            sb.setCharAt(i, (char) (s.charAt(i++) - 1));
        }
        return sb.toString();
    }
}

性能

使用 StringBuilder 显示用时更少,我试了一下与if判断的位置没关系,按道理来说直接数组访问比方法调用开销更小,new StringBuilder(s)s.toCharArray 都进行了数组拷贝。我能想到的解释就是大家都用的StringBuilder,然后这段代码被JIT编译器优化。

2741.特别的排列

目标

给你一个下标从 0 开始的整数数组 nums ,它包含 n 个 互不相同 的正整数。如果 nums 的一个排列满足以下条件,我们称它是一个特别的排列:

  • 对于 0 <= i < n - 1 的下标 i ,要么 nums[i] % nums[i+1] == 0 ,要么 nums[i+1] % nums[i] == 0 。

请你返回特别排列的总数目,由于答案可能很大,请将它对 109 + 7 取余 后返回。

示例 1:

输入:nums = [2,3,6]
输出:2
解释:[3,6,2] 和 [2,6,3] 是 nums 两个特别的排列。

示例 2:

输入:nums = [1,4,3]
输出:2
解释:[3,1,4] 和 [4,1,3] 是 nums 两个特别的排列。

说明:

  • 2 <= nums.length <= 14
  • 1 <= nums[i] <= 10^9

思路

有一个互不相同的正整数数组,问使得相邻元素可以被整除(对于相邻元素a % b == 0 || b % a == 0)的排列有多少种。

排列数的计算需要使用dfs,但如果不保存重复子问题的话会超时。

难点在于是否将保存的结果计入,例如 [2,6,3],虽然dfs 2 -> 6 -> 36 -> 2 -> 3有重复的子问题3,但是后者不符合题目条件。

// todo

代码

性能

503.下一个更大元素II – 单调栈

目标

给定一个循环数组 nums ( nums[nums.length - 1] 的下一个元素是 nums[0] ),返回 nums 中每个元素的 下一个更大元素 。

数字 x 的 下一个更大的元素 是按数组遍历顺序,这个数字之后的第一个比它更大的数,这意味着你应该循环地搜索它的下一个更大的数。如果不存在,则输出 -1 。

示例 1:

输入: nums = [1,2,1]
输出: [2,-1,2]
解释: 第一个 1 的下一个更大的数是 2;
数字 2 找不到下一个更大的数; 
第二个 1 的下一个最大的数需要循环搜索,结果也是 2。

示例 2:

输入: nums = [1,2,3,4,3]
输出: [2,3,4,-1,4]

说明:

  • 1 <= nums.length <= 10^4
  • -10^9 <= nums[i] <= 10^9

思路

暴力解法需要针对每一个元素向后搜索,时间复杂度为O(n^2)。这里面有一些重复的操作,例如,数组 [5,4,3,2,1,6] 找下一个更大的元素,从 5 向后搜索 与从 4 向后搜索有重复的部分 3 2 1。单调栈的核是这样处理的,先将第一个元素下标压入栈中,指针移到下一个元素,如果栈顶对应元素大于等于当前元素则将当前元素下标也压入栈中,否则弹栈,记录 res[stack.pop()] = 当前元素,直到栈顶元素大于等于当前元素。

针对这个题目,栈中保存的下标对应的元素值从栈底到栈顶是单调递减的,如果遇到第一个更大元素就弹栈了,因此称为单调栈。

这样做的好处是避免了重复的查找,它将重复搜索的部分使用栈保存了起来,这样就变成了从后向前搜索。如果栈顶元素遇到了第一个更大的元素,那么它也是前面同样小于该值的第一个更大元素,从而避免了重复查找。

这里对循环数组的处理是将原数组拼接到后面,下标进行取模运算。

代码

/**
 * @date 2024-06-24 0:18
 */
public class NextGreaterElements503 {

    public int[] nextGreaterElements_v4(int[] nums) {
        int n = nums.length;
        int[] res = new int[n];
        Arrays.fill(res, -1);
        int[] stack = new int[n];
        int top = -1;
        for (int i = 0; i < 2 * n - 1; i++) {
            int index = i % n;
            while (top > -1 && nums[stack[top]] < nums[index]) {
                res[stack[top--]] = nums[index];
            }
            if (i < n) {
                stack[++top] = index;
            }
        }
        return res;
    }
}

性能

139.单词拆分

目标

给你一个字符串 s 和一个字符串列表 wordDict 作为字典。如果可以利用字典中出现的一个或多个单词拼接出 s 则返回 true。

注意:不要求字典中出现的单词全部都使用,并且字典中的单词可以重复使用。

示例 1:

输入: s = "leetcode", wordDict = ["leet", "code"]
输出: true
解释: 返回 true 因为 "leetcode" 可以由 "leet" 和 "code" 拼接成。

示例 2:

输入: s = "applepenapple", wordDict = ["apple", "pen"]
输出: true
解释: 返回 true 因为 "applepenapple" 可以由 "apple" "pen" "apple" 拼接成。注意,你可以重复使用字典中的单词。

示例 3:

输入: s = "catsandog", wordDict = ["cats", "dog", "sand", "and", "cat"]
输出: false

说明:

  • 1 <= s.length <= 300
  • 1 <= wordDict.length <= 1000
  • 1 <= wordDict[i].length <= 20
  • s 和 wordDict[i] 仅由小写英文字母组成
  • wordDict 中的所有字符串 互不相同

思路

已知一个字符串列表 wordDict 和一个字符串 s,问能否用列表中的元素拼成该字符串,列表中的元素可以重复使用。

很明显需要使用动态规划来求解,假设当前列表元素 word 的长度为 l,子字符串 sub 的长度为 i,如果 sub.substring(0, i-l) 能由字典中的词拼成并且 word.equals(sub.substring(i-l, l)) 那么 sub 也能由字典中的词拼成。

代码

/**
 * @date 2024-06-23 19:58
 */
public class WordBreak139 {
    public boolean wordBreak(String s, List<String> wordDict) {
        int n = s.length();
        boolean[] dp = new boolean[n + 1];
        dp[0] = true;
        for (int i = 1; i <= n; i++) {
            for (String word : wordDict) {
                int length = word.length();
                if (length <= i && dp[i - length] && word.equals(s.substring(i - length, i))) {
                    dp[i] = true;
                }
            }
        }
        return dp[n];
    }

    public boolean wordBreak_v1(String s, List<String> wordDict) {
        int n = s.length();
        char[] mem = new char[n + 1];
        Arrays.fill(mem, '2');
        return dfs(s, 0, wordDict, mem) == '1';
    }

    public char dfs(String s, int i, List<String> wordDict, char[] mem) {
        int n = s.length();
        if (i == n) {
            return '1';
        }
        if (mem[i] != '2') {
            return mem[i];
        }
        for (String word : wordDict) {
            if (s.startsWith(word, i) && '1' == dfs(s, i + word.length(), wordDict, mem)) {
                return mem[i] = '1';
            }
        }
        return mem[i] = '0';
    }
}

性能

最快的解法是使用记忆化搜索,可以剪枝缩小搜索范围。