698.划分为k个相等的子集

目标

给定一个整数数组 nums 和一个正整数 k,找出是否有可能把这个数组分成 k 个非空子集,其总和都相等。

示例 1:

输入: nums = [4, 3, 2, 3, 5, 2, 1], k = 4
输出: True
说明: 有可能将其分成 4 个子集(5),(1,4),(2,3),(2,3)等于总和。

示例 2:

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

说明:

  • 1 <= k <= len(nums) <= 16
  • 0 < nums[i] < 10000
  • 每个元素的频率在 [1,4] 范围内

思路

给定一个数组 nums,判断能否将其划分为 k 个非空子集,要求每个子集的和都相等。

与数组中元素顺序无关可以排序。数组元素的和是可以枚举的,它一定大于等于数组中的最大元素。和确定了之后就可以从后向前判断能否组成这个和,如果不能则立即返回。

// todo

代码

性能

3133.数组最后一个元素的最小值

目标

给你两个整数 n 和 x 。你需要构造一个长度为 n 的 正整数 数组 nums ,对于所有 0 <= i < n - 1 ,满足 nums[i + 1] 大于 nums[i] ,并且数组 nums 中所有元素的按位 AND 运算结果为 x 。

返回 nums[n - 1] 可能的 最小 值。

示例 1

输入:n = 3, x = 4
输出:6
解释:
数组 nums 可以是 [4,5,6] ,最后一个元素为 6 。

示例 2

输入:n = 2, x = 7
输出:15
解释:
数组 nums 可以是 [7,15] ,最后一个元素为 15 。

说明

  • 1 <= n, x <= 10^8

思路

构造一个长度为 n 的单调递增数组 nums,要求数组所有元素按位与的结果为正整数 x,返回 nums[n - 1] 的最小值。

要使元素按位与的结果为 x,那么 x 中bit位为1的位置在元素中也必须为1。我们可以找到 x 中bit位为0的位置,从最低位开始置1,如果还不凑不够数组个数就将最高位置1,重新从最低位开始置1。

刚开始是按照上面的思路模拟着写的,非常容易出错。调试了半天发现,其实就是将 n - 1 的二进制表示填入 x 的二进制表示中的bit位为0的位置,如果不够就借用高位的0。之所以是 n -1 是因为 x 本身也算一个元素,0 ~ n - 1n 个元素。

官网题解也正是这个思路,不过实现简洁多了。主要是刚开始没有想清楚,其实根本不用讨论低位/高位,直接填就行了。

代码


/**
 * @date 2024-08-22 10:12
 */
public class MinEnd3133 {

    public long minEnd(int n, int x) {
        List<Integer> zeroIndex = new ArrayList<>(63);
        int i = 0;
        int tmp = x;
        // 记录0的位置
        while (tmp > 0) {
            if ((tmp & 1) == 0) {
                zeroIndex.add(i);
            }
            tmp >>= 1;
            i++;
        }
        long res = x;
        int zeroCnt = zeroIndex.size();
        long highBitsCnt;
        // 如果不含0,那么只能填高位
        if (zeroCnt == 0) {
            // 这个不应该全填1,应填n-1的二进制表示
            highBitsCnt = n - 1;
            while (highBitsCnt > 0) {
                if ((highBitsCnt & 1) == 1) {
                    res |= 1L << i;
                }
                i++;
                highBitsCnt >>= 1;
            }
            return res;
        }
        // 如果含0则计算其能表示的数字个数,下面是快速幂
        long oneLoopElementCnt = 1;
        tmp = zeroCnt;
        int base = 2;
        while (tmp > 0) {
            if ((tmp & 1) == 1) {
                oneLoopElementCnt = oneLoopElementCnt * base;
            }
            base *= base;
            tmp >>= 1;
        }
        // 低位需要填的个数
        long lowBitsCnt = n % oneLoopElementCnt - 1;
        if (lowBitsCnt == -1) {
            // 如果为-1,表示能够整除,将低位的0全置为1
            for (Integer index : zeroIndex) {
                res |= 1L << index;
            }
            // 如果低位能够填满数组则无需填高位
            if (n <= oneLoopElementCnt) {
                highBitsCnt = 0;
            } else {
                // 高位的计数需减1,是因为将低位全填1,这时已经完成了一个loop,例如n=4,x=2,oneLoopElementCnt 为2,n / oneLoopElementCnt = 2, (10 11) 110 111,我们只需要填充一个高位
                highBitsCnt = n / oneLoopElementCnt - 1;
            }
        } else {
            // 否则将需要填在低位的个数填入低位的0
            int j = 0;
            while (lowBitsCnt > 0) {
                if ((lowBitsCnt & 1) == 1) {
                    res |= 1L << zeroIndex.get(j);
                }
                j++;
                lowBitsCnt >>= 1;
            }
            // 如果低位能够填满数组则无需填高位
            if (n <= oneLoopElementCnt) {
                highBitsCnt = 0;
            } else {
                // 这里不需要减1,是因为n不能整除oneLoopElementCnt,多余位已经被舍去了,例如n=3,x=2,n / oneLoopElementCnt = 1
                highBitsCnt = n / oneLoopElementCnt;
            }
        }

        // 填充高位个数
        while (highBitsCnt > 0) {
            if ((highBitsCnt & 1) == 1) {
                // 出错点:如果写成1,会溢出,必须加上L
                res |= 1L << i;
            }
            i++;
            highBitsCnt >>= 1;
        }

        return res;
    }

}

性能

3007.价值和小于等于K的最大数字

目标

给你一个整数 k 和一个整数 x 。整数 num 的价值是它的二进制表示中在 x2x3x …… 等位置处 set bit(指在某数的二进制表示中值为 1 的二进制位) 的数目(从最低有效位开始)。下面的表格包含了如何计算价值的例子。

x num Binary Representation Price
1 13 000001101 3
2 13 000001101 1
2 233 011101001 3
3 13 000001101 1
3 362 101101010 2

num累加价值 是从 1num 的数字的 价值。如果 num 的累加价值小于或等于 k 则被认为是 廉价 的。

请你返回 最大 的廉价数字。

示例 1:

输入:k = 9, x = 1
输出:6
解释:由下表所示,6 是最大的廉价数字。
x num Binary Representation Price Accumulated Price
1 1 001 1 1
1 2 010 1 2
1 3 011 2 4
1 4 100 1 5
1 5 101 2 7
1 6 110 2 9
1 7 111 3 12

示例 2:

输入:k = 7, x = 2
输出:9
解释:由下表所示,9 是最大的廉价数字。
x num Binary Representation Price Accumulated Price
2 1 0001 0 0
2 2 0010 1 1
2 3 0011 1 2
2 4 0100 0 2
2 5 0101 0 2
2 6 0110 1 3
2 7 0111 1 4
2 8 1000 1 5
2 9 1001 1 6
2 10 1010 2 8

说明:

  • 1 <= k <= 10^15
  • 1 <= x <= 8

提示:

  • Binary search the answer.
  • In each step of the binary search you should calculate the number of the set bits in the ith position. Then calculate the sum of them.

思路

给定步长x,数字的价值定义为从最低有效位开始,以步长x遍历数字的二进制表示,并记录bit为1的个数。数字n的累加价值定义为 1 ~ n 的价值之和。给定数字k,求累加价值不超过k的最大数字。

先尝试模拟暴力求解。注意到返回值是 long 类型,说明n的规模超过了10^9,即便是 O(n) 的解法也会超时,并且每个数字都要循环bit位计数,肯定超时。我们需要要找一种 O(logn) 的解法。

提示说使用二分查找,问题的关键是如何直接计算出给定数字的累加价值。

// todo

代码

性能

3137.K周期字符串需要的最少操作次数

目标

给你一个长度为 n 的字符串 word 和一个整数 k ,其中 k 是 n 的因数。

在一次操作中,你可以选择任意两个下标 i 和 j,其中 0 <= i, j < n ,且这两个下标都可以被 k 整除,然后用从 j 开始的长度为 k 的子串替换从 i 开始的长度为 k 的子串。也就是说,将子串 word[i..i + k - 1] 替换为子串 word[j..j + k - 1] 。

返回使 word 成为 K 周期字符串 所需的 最少 操作次数。

如果存在某个长度为 k 的字符串 s,使得 word 可以表示为任意次数连接 s ,则称字符串 word 是 K 周期字符串 。例如,如果 word == "ababab",那么 word 就是 s = "ab" 时的 2 周期字符串 。

示例 1:

输入:word = "leetcodeleet", k = 4
输出:1
解释:可以选择 i = 4 和 j = 0 获得一个 4 周期字符串。这次操作后,word 变为 "leetleetleet" 。

示例 2:

输入:word = "leetcoleet", k = 2
输出:3
解释:可以执行以下操作获得一个 2 周期字符串。

i j word
0 2 etetcoleet
4 0 etetetleet
6 0 etetetetet

说明:

  • 1 <= n == word.length <= 10^5
  • 1 <= k <= word.length
  • k 能整除 word.length 。
  • word 仅由小写英文字母组成。

思路

给定字符串 word 以及周期 k(k 为 word 长度的因数,即可以被 word 长度整除),将字符串分为 word.length()/k 份,每次操作可以将其中一份替换为其它份字符串。问最少需要多少次操作,可以使每份字符串都相等。

用份数减去出现次数最多的子串即可。

代码


/**
 * @date 2024-08-17 20:44
 */
public class MinimumOperationsToMakeKPeriodic3137 {

    /**
     * 使用substring效率更高
     * 循环步长为k
     */
    public int minimumOperationsToMakeKPeriodic(String word, int k) {
        int n = word.length();
        int max = 0;
        int parties = n / k;
        // 优化点:指定初始容量
        Map<String, Integer> map = new HashMap<>(parties);
        // 出错点,这里循环条件使 <= n,而非小于n,因为我们使用的使substring,不包括结束index
        for (int i = k; i <= n; i += k) {
            String period = word.substring(i - k, i);
            // 优化点:这里可以直接拿到cnt,不用后面再从map里get
            int cnt = map.merge(period, 1, Integer::sum);
            max = Math.max(cnt, max);
        }
        return parties - max;
    }

}

性能

50.快速幂

目标

实现 pow(x, n) ,即计算 x 的整数 n 次幂函数(即,x^n)。

示例 1:

输入:x = 2.00000, n = 10
输出:1024.00000

示例 2:

输入:x = 2.10000, n = 3
输出:9.26100

示例 3:

输入:x = 2.00000, n = -2
输出:0.25000
解释:2^-2 = 1/2^2 = 1/4 = 0.25

说明:

  • -100.0 < x < 100.0
  • -2^31 <= n <= 2^31-1
  • n 是一个整数
  • 要么 x 不为零,要么 n > 0 。
  • -10^4 <= x^n <= 10^4

思路

通常求 x^n 最直接的算法是迭代相乘,时间复杂度为 O(n)。但是当 n 超过 10^6 就需要考虑 O(logn) 的算法了。

快速幂的核心思想是根据幂次 power 的二进制表示来确定 对应的指数 是否需要计入结果。

比如 2^13, 幂次 13 的二进制表示为 1101,而 2^13 = 2^1 * 2^4 * 2^8,其中 1、4、8 刚好对应幂次的二进制表示中bit为 1 所代表的数字。我们可以循环将幂右移 1 位,直到幂为 0,并在循环中累计 base = base * base,当相应 bit 位为 1 时将 base 计入结果即可。

代码

/**
 * @date 2024-08-16 10:19
 */
public class MyPow50 {

    public double myPow(double x, int n) {
        long cnt = n >= 0 ? n : -((long) n);
        double res = 1.0;
        while (cnt > 0) {
            if ((cnt & 1) == 1) {
                res = res * x;
            }
            x = x * x;
            cnt = cnt >> 1;
        }
        return n == 0 ? 1 : n > 0 ? res : 1 / res;
    }

}

性能

3148.矩阵中的最大得分

目标

给你一个由 正整数 组成、大小为 m x n 的矩阵 grid。你可以从矩阵中的任一单元格移动到另一个位于正下方或正右侧的任意单元格(不必相邻)。从值为 c1 的单元格移动到值为 c2 的单元格的得分为 c2 - c1 。

你可以从 任一 单元格开始,并且必须至少移动一次。

返回你能得到的 最大 总得分。

示例 1:

输入:grid = [[9,5,7,3],[8,9,6,1],[6,7,14,3],[2,5,3,1]]
输出:9
解释:从单元格 (0, 1) 开始,并执行以下移动:
- 从单元格 (0, 1) 移动到 (2, 1),得分为 7 - 5 = 2 。
- 从单元格 (2, 1) 移动到 (2, 2),得分为 14 - 7 = 7 。
总得分为 2 + 7 = 9 。

示例 2:

输入:grid = [[4,3,2],[3,2,1]]
输出:-1
解释:从单元格 (0, 0) 开始,执行一次移动:从 (0, 0) 到 (0, 1) 。得分为 3 - 4 = -1 。

说明:

  • m == grid.length
  • n == grid[i].length
  • 2 <= m, n <= 1000
  • 4 <= m * n <= 10^5
  • 1 <= grid[i][j] <= 10^5

思路

有一个二维矩阵 grid,可以从任意格子出发向下或右移动(不必相邻),移动的得分为元素值之差 to - from,求最大的得分数。

首先想到动态规划,关键点是想清楚最大得分其实就是以当前元素为左上顶点(不包括其自身),以 m - 1, n - 1 为右下顶点的矩形中的最大值减去当前元素值。如果把状态定义错误还是会超时的,比如当前元素出发所能取得的最大值,需要依次向下/向右比较,时间复杂度 O(m * n * (m +n))m = 1000 n = 95,循环次数 10^5 * (10^3 + 95) 超时,耗时878 ms,下午又提交了几次,耗时 1200 ~ 1300 ms。参考特殊数组II 中的时间复杂度分析。对于O(n)的算法,10^8 差不多就是极限了,单个用例在1s左右,多个肯定超时。

代码

/**
 * @date 2024-08-15 0:22
 */
public class MaxScore3148 {

    /**
     * 修改了dp的定义,表示以i,j为起点到右下的矩形的最大值
     * 执行通过
     */
    public int maxScore_v2(List<List<Integer>> grid) {
        int m = grid.size();
        int n = grid.get(0).size();
        int[][] dp = new int[m][n];
        int res = Integer.MIN_VALUE;
        dp[m - 1][n - 1] = grid.get(m - 1).get(n - 1);
        for (int i = n - 2; i >= 0; i--) {
            int cur = grid.get(m - 1).get(i);
            // 注意这里使用的是上一个位置为顶点的矩形中的最大值
            res = Math.max(dp[m - 1][i + 1] - cur, res);
            // 如果先进行这个计算,然后再进行上面的计算,就有会出现不移动的情况,但积分只有移动才能取得,可以是负值
            dp[m - 1][i] = Math.max(cur, dp[m - 1][i + 1]);
        }
        for (int i = m - 2; i >= 0; i--) {
            int cur = grid.get(i).get(n - 1);
            res = Math.max(dp[i + 1][n - 1] - cur, res);
            dp[i][n - 1] = Math.max(cur, dp[i + 1][n - 1]);
        }

        for (int i = m - 2; i >= 0; i--) {
            for (int j = n - 2; j >= 0; j--) {
                int cur = grid.get(i).get(j);
                res = Math.max(dp[i + 1][j] - cur, res);
                res = Math.max(dp[i][j + 1] - cur, res);
                dp[i][j] = Math.max(cur, dp[i + 1][j]);
                dp[i][j] = Math.max(dp[i][j], dp[i][j + 1]);
            }
        }
        return res;
    }

    /**
     * 560 / 564 超时  m = 1000  n = 95
     * O(m * n * (m +n)) 循环次数 10^5 * (10^3 + 95)
     * 878 ms
     */
    public int maxScore_v1(List<List<Integer>> grid) {
        int m = grid.size();
        int n = grid.get(0).size();
        int[][] dp = new int[m][n];
        for (int[] row : dp) {
            Arrays.fill(row, Integer.MIN_VALUE);
        }
        dp[m - 1][n - 1] = 0;
        int res = Integer.MIN_VALUE;
        for (int i = n - 2; i >= 0; i--) {
            for (int j = i + 1; j < n; j++) {
                int diff = grid.get(m - 1).get(j) - grid.get(m - 1).get(i);
                if (dp[m - 1][j] > 0) {
                    dp[m - 1][i] = Math.max(diff + dp[m - 1][j], dp[m - 1][i]);
                }
                dp[m - 1][i] = Math.max(diff, dp[m - 1][i]);
            }
            res = Math.max(dp[m - 1][i], res);
        }
        for (int i = m - 2; i >= 0; i--) {
            for (int j = i + 1; j < m; j++) {
                int diff = grid.get(j).get(n - 1) - grid.get(i).get(n - 1);
                if (dp[j][n - 1] > 0) {
                    dp[i][n - 1] = Math.max(diff + dp[j][n - 1], dp[i][n - 1]);
                }
                dp[i][n - 1] = Math.max(diff, dp[i][n - 1]);
            }
            res = Math.max(dp[i][n - 1], res);
        }

        for (int i = m - 2; i >= 0; i--) {
            for (int j = n - 2; j >= 0; j--) {
                for (int h = i + 1; h < m; h++) {
                    int rowDiff = grid.get(h).get(j) - grid.get(i).get(j);
                    if (dp[h][j] > 0) {
                        dp[i][j] = Math.max(rowDiff + dp[h][j], dp[i][j]);
                    }
                    dp[i][j] = Math.max(rowDiff, dp[i][j]);
                }
                for (int k = j + 1; k < n; k++) {
                    int colDiff = grid.get(i).get(k) - grid.get(i).get(j);
                    if (dp[i][k] > 0) {
                        dp[i][j] = Math.max(colDiff + dp[i][k], dp[i][j]);
                    }
                    dp[i][j] = Math.max(colDiff, dp[i][j]);
                }
                res = Math.max(dp[i][j], res);
            }
        }
        return res;
    }

}

性能

3152.特殊数组II

目标

如果数组的每一对相邻元素都是两个奇偶性不同的数字,则该数组被认为是一个 特殊数组 。

有一个整数数组 nums 和一个二维整数矩阵 queries,对于 queries[i] = [fromi, toi],请你帮助检查子数组 nums[fromi..toi] 是不是一个 特殊数组 。

返回布尔数组 answer,如果 nums[fromi..toi] 是特殊数组,则 answer[i] 为 true ,否则,answer[i] 为 false 。

示例 1:

输入:nums = [3,4,1,2,6], queries = [[0,4]]
输出:[false]
解释:
子数组是 [3,4,1,2,6]。2 和 6 都是偶数。

示例 2:

输入:nums = [4,3,1,6], queries = [[0,2],[2,3]]
输出:[false,true]
解释:
子数组是 [4,3,1]。3 和 1 都是奇数。因此这个查询的答案是 false。
子数组是 [1,6]。只有一对:(1,6),且包含了奇偶性不同的数字。因此这个查询的答案是 true。

说明:

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

思路

类似于特殊数组I,只不过是判断子数组是否是特殊数组。我们可以记录奇偶性相同的下标,如果 queries[i] = [fromi, toi] 包含其中的下标对返回false。但是如果所有元素的奇偶性都相同,下标对有n个,查询也是n次,O(n^2) 对于 10^5 的数据规模会超时。

我们可以将问题转化一下,由于仅考虑相邻元素的奇偶性,将数组中的偶数元素都替换为0,奇数元素都替换为1,这样0与1交替出现的是特殊数组。使用前缀和判断不了是否交替出现,只能初步排除一些区间。

之所以超时是因为进行了许多重复判断,我们想直接判断查询区间是否包含奇偶相同的下标对。可以使用二分查找,如果查出的from,to下标相同,说明不相交。

这里比较坑的一点是,题目中没有说明如果查询区间只包含一个值视为奇偶性不同。

log2(10^5) ≈ 16.6 时间复杂度为O(nlogn),耗时30ms,说明10^6的数据规模,O(n)的解法不会超时,以后多留意一下。

看了题解,其实可以使用前缀和,只不过不是将原数组转为0或1计算前缀和,而是定义数组diff,原数组nums相邻元素奇偶性相同为0,不同为1。对应给定的区间,我们就可以根据diff的前缀和判断是否含有奇偶性相同的元素。时间复杂度为O(n),耗时3ms。

也有使用动态规划求解的,记录最近一次奇偶性相同的位置。时间复杂度为O(n),耗时3ms。

代码

/**
 * @date 2024-08-14 9:58
 */
public class IsArraySpecial3152 {

    public boolean[] isArraySpecial_v1(int[] nums, int[][] queries) {
        int n = nums.length;
        List<Integer> from = new ArrayList<>();
        List<Integer> to = new ArrayList<>();
        for (int i = 1; i < n; i++) {
            int j = i - 1;
            if (nums[i] % 2 == nums[j] % 2) {
                from.add(j);
                to.add(i);
            }
        }
        int l = from.size();
        int[] fromArray = new int[l];
        int[] toArray = new int[l];
        for (int i = 0; i < l; i++) {
            fromArray[i] = from.get(i);
            toArray[i] = to.get(i);
        }
        int length = queries.length;
        boolean[] res = new boolean[length];
        Arrays.fill(res, true);
        for (int i = 0; i < length; i++) {
            int[] query = queries[i];
            if (query[0] == query[1]) {
                // 如果查询区间相同认为是特殊区间
                res[i] = true;
                continue;
            }
            int fromIndex = Arrays.binarySearch(fromArray, query[0]);
            if (fromIndex >= 0) {
                // 如果需要查询的数组包含了query[0],由于前面判断了相等的情况,
                // 执行到这里query[1] > query[0],而对应的toIndex 为 fromIndex + 1,
                // 推出 query[1] >= toIndex,说明包含奇偶性相同的下标对,
                res[i] = false;
                continue;
            }
            int toIndex = Arrays.binarySearch(toArray, query[1]);
            if (fromIndex != toIndex) {
                // 执行到这里,fromIndex < 0,如果 toIndex >= 0,由于 query[0] < query[1],推出 query[0] <= fromIndex
                // 如果 toIndex < 0,说明查询区间开始与结束下标都没有找到,
                // 如果插入位置不同,说明也包含了奇偶性相同的元素
                res[i] = false;
            }
        }
        return res;
    }

}

性能

676.实现一个魔法字典

目标

设计一个使用单词列表进行初始化的数据结构,单词列表中的单词 互不相同 。 如果给出一个单词,请判定能否只将这个单词中一个字母换成另一个字母,使得所形成的新单词存在于你构建的字典中。

实现 MagicDictionary 类:

  • MagicDictionary() 初始化对象
  • void buildDict(String[] dictionary) 使用字符串数组 dictionary 设定该数据结构,dictionary 中的字符串互不相同
  • bool search(String searchWord) 给定一个字符串 searchWord ,判定能否只将字符串中 一个 字母换成另一个字母,使得所形成的新字符串能够与字典中的任一字符串匹配。如果可以,返回 true ;否则,返回 false 。

示例:

输入
["MagicDictionary", "buildDict", "search", "search", "search", "search"]
[[], [["hello", "leetcode"]], ["hello"], ["hhllo"], ["hell"], ["leetcoded"]]
输出
[null, null, false, true, false, false]

解释
MagicDictionary magicDictionary = new MagicDictionary();
magicDictionary.buildDict(["hello", "leetcode"]);
magicDictionary.search("hello"); // 返回 False
magicDictionary.search("hhllo"); // 将第二个 'h' 替换为 'e' 可以匹配 "hello" ,所以返回 True
magicDictionary.search("hell"); // 返回 False
magicDictionary.search("leetcoded"); // 返回 False

说明:

  • 1 <= dictionary.length <= 100
  • 1 <= dictionary[i].length <= 100
  • dictionary[i] 仅由小写英文字母组成
  • dictionary 中的所有字符串 互不相同
  • 1 <= searchWord.length <= 100
  • searchWord 仅由小写英文字母组成
  • buildDict 仅在 search 之前调用一次
  • 最多调用 100 次 search

思路

实现这样一个数据结构,初始化一个单词列表,查询 给定单词修改一个字母(修改的字母与原字母不能相同)后是否位于字典中。言外之意,要在字典中查找长度相同,但字母有一个不同的单词。

如何判断两个长度 相等 的字符串之间是否只有一个字母不同?直接的想法是逐个字母比较,并记录不同字母的个数,最坏情况下查询的时间复杂度是 O(mn),m为被查单词的平均长度,n为单词列表长度。

一个更好的想法是构建一颗字典树,这样只需沿着给定单词的路径去查找而无需与字典中单词一一比较,最坏的情况是第一个字母就不同,这样得遍历字典中同级的其它字母下的子树共|Σ|个,时间复杂度降为O(|Σ|m)。最好的情况就是单词的最后一个字母不同,时间复杂度为O(m)。

针对本题的数据范围以及调用次数,暴力解法就足够了,字典树反而体现不出优势。

代码

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

    private String[] dictionary;

    public MagicDictionary676() {

    }

    public void buildDict(String[] dictionary) {
        this.dictionary = dictionary;
    }

    public boolean search(String searchWord) {
        for (String word : dictionary) {
            int length = searchWord.length();
            if (word.length() != length) {
                continue;
            }
            int cnt = 0;
            for (int i = 0; i < length; i++) {
                if (word.charAt(i) != searchWord.charAt(i)) {
                    cnt++;
                }
                if (cnt > 1) {
                    // 提前结束比较,效率有明显提升
                    break;
                }
            }
            if (cnt == 1) {
                return true;
            }
        }
        return false;
    }

}

性能

1035.不相交的线

目标

在两条独立的水平线上按给定的顺序写下 nums1 和 nums2 中的整数。

现在,可以绘制一些连接两个数字 nums1[i] 和 nums2[j] 的直线,这些直线需要同时满足:

  • nums1[i] == nums2[j]
  • 且绘制的直线不与任何其他连线(非水平线)相交。

请注意,连线即使在端点也不能相交:每个数字只能属于一条连线。

以这种方法绘制线条,并返回可以绘制的最大连线数。

示例 1:

输入:nums1 = [1,4,2], nums2 = [1,2,4]
输出:2
解释:可以画出两条不交叉的线,如上图所示。 
但无法画出第三条不相交的直线,因为从 nums1[1]=4 到 nums2[2]=4 的直线将与从 nums1[2]=2 到 nums2[1]=2 的直线相交。

示例 2:

输入:nums1 = [2,5,1,2,5], nums2 = [10,5,2,1,5,2]
输出:3

示例 3:

输入:nums1 = [1,3,7,1,7,5], nums2 = [1,9,2,5,1]
输出:2

说明:

  • 1 <= nums1.length, nums2.length <= 500
  • 1 <= nums1[i], nums2[j] <= 2000

思路

在两条平行线上有两个数组,可以将两个数组中的相同元素连线,问最多有多少条不相交的连线。

典型的动态规划题,根据选或不选来进行状态转移,连线之后只能从后面的位置选两点连线。

// todo

代码

性能

3132.找出与数组相加的整数 II

目标

给你两个整数数组 nums1 和 nums2。

从 nums1 中移除两个元素,并且所有其他元素都与变量 x 所表示的整数相加。如果 x 为负数,则表现为元素值的减少。

执行上述操作后,nums1 和 nums2 相等 。当两个数组中包含相同的整数,并且这些整数出现的频次相同时,两个数组 相等 。

返回能够实现数组相等的 最小 整数 x 。

示例 1:

输入:nums1 = [4,20,16,12,8], nums2 = [14,18,10]
输出:-2
解释:移除 nums1 中下标为 [0,4] 的两个元素,并且每个元素与 -2 相加后,nums1 变为 [18,14,10] ,与 nums2 相等。

示例 2:

输入:nums1 = [3,5,5,3], nums2 = [7,7]
输出:2
解释: 移除 nums1 中下标为 [0,3] 的两个元素,并且每个元素与 2 相加后,nums1 变为 [7,7] ,与 nums2 相等。

说明:

  • 3 <= nums1.length <= 200
  • nums2.length == nums1.length - 2
  • 0 <= nums1[i], nums2[i] <= 1000
  • 测试用例以这样的方式生成:存在一个整数 x,nums1 中的每个元素都与 x 相加后,再移除两个元素,nums1 可以与 nums2 相等。

思路

这与找出与数组相加的整数 I类似,只不过需要删除nums1中的两个元素,这样x就可能存在多个值,取其中最小的。

最简单的想法是将这两个数组排序,枚举 nums1 长度为 2 的子序列组合,将其从数组中删去,判断对应位置上的 nums2[i] - nums1[i] 的值是否都等于 x。如果相等则保存,枚举完所有子序列组合后取保存的最小值即可。排序的时间复杂度为 O(nlogn) 代入 n = 200 大约 460.2,子序列可能的组合有 C(200, 2) = 19900,针对每种组合都要遍历数组,循环 200 次,大概需要执行 200 * 19900 ≈ 4 * 10^6 次,应该不会超时。

可以将 nums2nums1 中的元素两两相减然后统计差值的出现次数,如果出现次数小于 nums2.length 那么该差值一定不是 x。因为 nums1.length - 2 == nums2.length,如果 x 存在,那么其出现次数最少为nums2.length。例如,[1,2,3,4][3,4] 的差值 2 1 0 -1 3 2 1 0 2出现了2次,1出现了2次,0出现了2次,取最小的0。

得到了可能的 x 之后,我们需要判断是否合法,例如 [9,9,1,1,1][5,5,5], 差值 -4 出现了 6 次,4 出现了 9 次。但是 -4 是不合法的,因为使用 nums2 - (-4) 还原回去的个数对不上。

看官网题解 // todo

代码

/**
 * @date 2024-08-09 0:30
 */
public class MinimumAddedInteger3132 {

    public int minimumAddedInteger(int[] nums1, int[] nums2) {
        int n = nums2.length;
        Map<Integer, Integer> map = new TreeMap<>();
        Map<Integer, Integer> set = new HashMap<>(nums1.length);
        for (int i : nums1) {
            set.merge(i, 1, Integer::sum);
        }
        for (int v2 : nums2) {
            for (int v1 : nums1) {
                map.merge(v2 - v1, 1, Integer::sum);
            }
        }
        int res = Integer.MAX_VALUE;
        here:
        for (Map.Entry<Integer, Integer> entry : map.entrySet()) {
            Map<Integer, Integer> tmp = new HashMap<>(set);
            if (entry.getValue() >= n) {
                for (int i : nums2) {
                    if (tmp.get(i - entry.getKey()) == null || tmp.get(i - entry.getKey()) <= 0) {
                        continue here;
                    }
                    tmp.merge(i - entry.getKey(), -1, Integer::sum);
                }
                res = Math.min(res, entry.getKey());
            }
        }
        return res;
    }

}

性能