力扣第 415 场周赛

3289. 数字小镇中的捣蛋鬼

数字小镇 Digitville 中,存在一个数字列表 nums,其中包含从 0 到 n - 1 的整数。每个数字本应 只出现一次,然而,有 两个 顽皮的数字额外多出现了一次,使得列表变得比正常情况下更长。

为了恢复 Digitville 的和平,作为小镇中的名侦探,请你找出这两个顽皮的数字。

返回一个长度为 2 的数组,包含这两个数字(顺序任意)。

示例 1:

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

输出: [0,1]

解释:

数字 0 和 1 分别在数组中出现了两次。

示例 2:

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

输出: [2,3]

解释:

数字 2 和 3 分别在数组中出现了两次。

示例 3:

输入: nums = [7,1,5,4,3,4,6,0,9,5,8,2]

输出: [4,5]

解释:

数字 4 和 5 分别在数组中出现了两次。

提示:

  • 2 <= n <= 100
  • nums.length == n + 2
  • 0 <= nums[i] < n
  • 输入保证 nums 中 恰好 包含两个重复的元素。

思路:用Map存取每个元素出现的次数,然后再计算其中等于2的个数。

代码:

class Solution {
    public int[] getSneakyNumbers(int[] nums) {
              // 创建一个 HashMap 来记录每个数字的出现次数
        Map<Integer, Integer> countMap = new HashMap<>();
        // 遍历数组并统计每个数字的出现次数
        for (int num : nums) {
            countMap.put(num, countMap.getOrDefault(num, 0) + 1);
        }

        // 存储结果的数组
        int[] result = new int[2];
        int index = 0;

        // 查找出现两次的数字
        for (Map.Entry<Integer, Integer> entry : countMap.entrySet()) {
            if (entry.getValue() == 2) {
                result[index++] = entry.getKey();
            }
        }

        return result;
    }
}

3290. 最高乘法得分

给你一个大小为 4 的整数数组 a 和一个大小 至少为 4 的整数数组 b

你需要从数组 b 中选择四个下标 i0i1i2, 和 i3,并满足 i0 < i1 < i2 < i3。你的得分将是 a[0] * b[i0] + a[1] * b[i1] + a[2] * b[i2] + a[3] * b[i3] 的值。

返回你能够获得的 最大 得分。

示例 1:

输入: a = [3,2,5,6], b = [2,-6,4,-5,-3,2,-7]

输出: 26

解释:
选择下标 0, 1, 2 和 5。得分为 3 * 2 + 2 * (-6) + 5 * 4 + 6 * 2 = 26

示例 2:

输入: a = [-1,4,5,-2], b = [-5,-1,-3,-2,-4]

输出: -1

解释:
选择下标 0, 1, 3 和 4。得分为 (-1) * (-5) + 4 * (-1) + 5 * (-2) + (-2) * (-4) = -1

提示:

  • a.length == 4
  • 4 <= b.length <= 10^5
  • -10^5 <= a[i], b[i] <= 10^5

思路:

一.寻找子问题
看示例 1,a=[3,2,5,6], b=[2,−6,4,−5,−3,2,−7]。

考虑从右往左选 b 数字,分类讨论:

如果不选 b[6],那么需要解决的问题为:从 b[0] 到 b[5] 选 4 个数,与 a[0] 到 a[3] 计算点积,结果的最大值。
如果选 b[6],那么需要解决的问题为:从 b[0] 到 b[5] 选 3 个数,与 a[0] 到 a[2] 计算点积,结果的最大值。

二、状态定义与状态转移方程
根据上面的讨论,我们需要在递归过程中跟踪以下信息:

i:当前要考虑 b[i] 选或不选。
j:如果选 b[i],那么与 a[j] 相乘。
因此,定义状态为 dfs(i,j),表示从 b[0] 到 b[i] 选 j+1 个数,与 a[0] 到 a[j] 计算点积,结果的最大值。

接下来,思考如何从一个状态转移到另一个状态。

考虑 b[i] 数字,分类讨论:

如果不选 b[i],那么需要解决的问题为:从 b[0] 到 b[i−1] 选 j+1 个数,与 a[0] 到 a[j] 计算点积,结果的最大值,即 dfs(i−1,j)。
如果选 b[i],那么需要解决的问题为:从 b[0] 到 b[i−1] 选 j 个数,与 a[0] 到 a[j−1] 计算点积,结果的最大值,即 dfs(i−1,j−1)+a[j]⋅b[i]。
这两种情况取最大值,就得到了 dfs(i,j),即dfs(i,j)=max(dfs(i−1,j),dfs(i−1,j−1)+a[j]⋅b[i])
递归边界:dfs(i,−1)=0, dfs(−1,j≥0)=−∞。用 −∞ 表示不合法的状态,保证 max 不会取到不合法的状态。

递归入口:dfs(n−1,3),也就是答案。

代码:

class Solution {
    public long maxScore(int[] a, int[] b) {
        int n = b.length;
        long[][] memo = new long[n][4];
        for (long[] row : memo) {
            Arrays.fill(row, Long.MIN_VALUE); // 表示没有计算过
        }
        return dfs(n - 1, 3, a, b, memo);
    }
    private long dfs(int i, int j, int[] a, int[] b, long[][] memo) {
        if (j < 0) { // 选完了
            return 0;
        }
        if (i < 0) { // j >= 0,没选完
            return Long.MIN_VALUE / 2; // 防止溢出
        }
        if (memo[i][j] == Long.MIN_VALUE) { // 需要计算,并记忆化
            memo[i][j] = Math.max(dfs(i - 1, j, a, b, memo), dfs(i - 1, j - 1, a, b, memo) + (long) a[j] * b[i]);
        }
        return memo[i][j];
    }
}

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

思路:

跳跃游戏 + 字符串哈希 + 二分
用划分型 DP 思考,考虑第一段划分多长。

比如示例 1 的 target=aabcdabc。如果第一段能划分出一个长为 2 的子串,即 aa,那么也可以划分出一个更短的,长为 1 的子串,即 a。所以只需考虑最大的 r,满足 target 下标从 0 到 r 这一段是某个 words[i] 的前缀。

如果第一段划分出一个长为1的子串,那么接下来要解决的问题为:target 的长为 n−1 的后缀的最小划分个数,也就是从下标 1 开始,剩余字符串的最小划分个数。一般地,对于每个 i,都计算一个 ri,满足target 下标从 i 到 ri这一段是某个 words[i] 的前缀。算出ri​后,我们可以枚举当前这一段的长度:

长为 1,那么接下来思考从 i+1 开始,剩余字符串的最小划分个数。
长为 2,那么接下来思考从 i+2 开始,剩余字符串的最小划分个数。
……
长为 ri,那么接下来思考从 i+ri开始,剩余字符串的最小划分个数。
相当于我们可以从下标 i「跳到」下标 i+1,i+2,⋯,ri+1 中的任意位置。如果跳到 n,表示 target 划分完毕。

问题转换成:

计算 ri。计算从 0 跳到 n 的最小跳跃次数。这类似 45. 跳跃游戏 II 或者 1326. 灌溉花园的最少水龙头数目。
如何计算 ri?

可以用字典树 + 枚举,这可以通过 周赛第三题,但无法通过本题。

预处理每个 words[i] 的每个前缀的字符串哈希值,按照前缀长度分组,保存到不同的集合中。每个集合保存的是相同前缀长度的哈希值。

对于 i,我们需要计算最大的长度 sz,满足 target 从下标 i 开始的长为 sz 的子串的哈希值是否在对应的集合中。

这可以用二分算出来,原理见 二分查找 红蓝染色法【基础算法精讲 04】。

算出 sz,就可以算出从 i 向右,最远可以跳到 i+sz。

代码:

class Solution {
    private static final int MOD = 1_070_777_777;

    public int minValidStrings(String[] words, String target) {
        char[] t = target.toCharArray();
        int n = t.length;

        // 多项式字符串哈希(方便计算子串哈希值)
        // 哈希函数 hash(s) = s[0] * base^(n-1) + s[1] * base^(n-2) + ... + s[n-2] * base + s[n-1]
        final int BASE = (int) 8e8 + new Random().nextInt((int) 1e8); // 随机 base,防止 hack
        int[] powBase = new int[n + 1]; // powBase[i] = base^i
        int[] preHash = new int[n + 1]; // 前缀哈希值 preHash[i] = hash(target[0] 到 target[i-1])
        powBase[0] = 1;
        for (int i = 0; i < n; i++) {
            powBase[i + 1] = (int) ((long) powBase[i] * BASE % MOD);
            preHash[i + 1] = (int) (((long) preHash[i] * BASE + t[i]) % MOD); // 秦九韶算法计算多项式哈希
        }

        int maxLen = 0;
        for (String w : words) {
            maxLen = Math.max(maxLen, w.length());
        }
        Set<Integer>[] sets = new HashSet[maxLen];
        Arrays.setAll(sets, i -> new HashSet<>());
        for (String w : words) {
            long h = 0;
            for (int j = 0; j < w.length(); j++) {
                h = (h * BASE + w.charAt(j)) % MOD;
                sets[j].add((int) h); // 注意 j 从 0 开始
            }
        }

        int ans = 0;
        int curR = 0; // 已建造的桥的右端点
        int nxtR = 0; // 下一座桥的右端点的最大值
        for (int i = 0; i < n; i++) {
            int sz = calcSz(i, preHash, powBase, sets);
            nxtR = Math.max(nxtR, i + sz);
            if (i == curR) { // 到达已建造的桥的右端点
                if (i == nxtR) { // 无论怎么造桥,都无法从 i 到 i+1
                    return -1;
                }
                curR = nxtR; // 造一座桥
                ans++;
            }
        }
        return ans;
    }

    private int calcSz(int i, int[] preHash, int[] powBase, Set<Integer>[] sets) {
        // 开区间二分,left 一定满足要求,right 一定不满足要求
        int left = 0;
        int right = Math.min(preHash.length - 1 - i, sets.length) + 1;
        while (left + 1 < right) {
            int mid = (left + right) >>> 1;
            long subHash = (((long) preHash[i + mid] - (long) preHash[i] * powBase[mid]) % MOD + MOD) % MOD;
            if (sets[mid - 1].contains((int) subHash)) {
                left = mid;
            } else {
                right = mid;
            }
        }
        return left;
    }
}

由于本周赛第三题第四题一样,就写出了较为容易理解的最优解法。其他方法较为苦难。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值