首页 > 其他分享 >力扣第 415 场周赛

力扣第 415 场周赛

时间:2024-09-16 21:24:00浏览次数:3  
标签:周赛 target int dfs 力扣 415 long words 字符串

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

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

标签:周赛,target,int,dfs,力扣,415,long,words,字符串
From: https://blog.csdn.net/qq_43227507/article/details/142306791

相关文章

  • 力扣热题100 - 二叉树:二叉搜索树中第 K 小的元素
    题目描述:题号:230给定一个二叉搜索树的根节点 root ,和一个整数 k ,请你设计一个算法查找其中第 k 小的元素(从1开始计数)。解题思路:思路一:中序遍历二叉树+ 计数根据二叉搜索树的性质,中序遍历得到的节点的顺序是从小到大递增的。所以可以一边中序遍历,一边计数......
  • 力扣热题100 - 二叉树:二叉树展开为链表
    题目描述:题号:114给你二叉树的根结点 root ,请你将它展开为一个单链表:展开后的单链表应该同样使用 TreeNode ,其中 right 子指针指向链表中下一个结点,而左子指针始终为 null 。展开后的单链表应该与二叉树 先序遍历 顺序相同。解题思路:思路一:前序遍历后......
  • 力扣刷题(6)
    两数之和II-输入有序数组两数之和II-输入有序数组-力扣思路:因为该数组是非递减顺序排列,因此可以设两个左右下标当左右下标的数相加大于target时,则表示右下标的数字过大,因此将右下标--当左右下标的数相加小于target时,则表示左下标的数字过小,因此将左下标++当相......
  • 力扣136.只出现一次的数字
    题目描述:给你一个 非空 整数数组 nums ,除了某个元素只出现一次以外,其余每个元素均出现两次。找出那个只出现了一次的元素。你必须设计并实现线性时间复杂度的算法来解决此问题,且该算法只使用常量额外空间。解题思路:看到数组刚开始想的是排序后遍历,但是时间复杂度太高了......
  • 力扣热题100 - 二叉树:验证二叉搜索树
    题目描述:题号:98给你一个二叉树的根节点 root ,判断其是否是一个有效的二叉搜索树。有效 二叉搜索树定义如下:节点的左子树只包含 小于 当前节点的数。节点的右子树只包含 大于 当前节点的数。所有左子树和右子树自身必须也是二叉搜索树。解题思路:思路一......
  • 力扣最热一百题——螺旋矩阵
    目录题目链接:54.螺旋矩阵-力扣(LeetCode)题目描述示例提示:解法一:模拟1.边界初始化2.循环遍历矩阵3.从左到右遍历上边界4.从上到下遍历右边界5.从右到左遍历下边界6.从下到上遍历左边界7.结束条件代码执行流程总结Java写法:运行时间以及内存消耗C++写法......
  • 洛谷题单指南-分治与倍增-P2415 集合求和
    原题链接:https://www.luogu.com.cn/problem/P2415题意解读:计算集合所有子集中元素之和。解题思路:集合的特性:互异性,元素各不相同来看样例:23,可以组成的子集有空23232和3都出现2次再比如:123,可以组成的子集有空12312 13231231,2,3各出现4次由于在集合中......
  • 力扣494-目标和(Java详细题解)
    题目链接:494.目标和-力扣(LeetCode)前情提要:因为本人最近都来刷dp类的题目所以该题就默认用dp方法来做。最近刚学完01背包,所以现在的题解都是以01背包问题为基础再来写的。如果大家不懂01背包的话,建议可以去学一学,01背包问题可以说是背包问题的基础。如果大家感兴趣,......
  • 力扣刷题——2398. 预算内的最多机器人数目
    由题目中求“最多可以连续运行的机器人数目”可知,求的是数组子数组的长度,那么就可以直接使用滑动窗口求解。配合前缀和,可以快速的求得滑动窗口内的运行时间和。那么编写代码如下:intmaximumRobots(vector<int>&chargeTimes,vector<int>&runningCosts,longlongbudget){......
  • 题解 力扣 LeetCode 105 从前序与中序遍历序列构造二叉树 C/C++
    题目传送门:105.从前序与中序遍历序列构造二叉树-力扣(LeetCode)https://leetcode.cn/problems/construct-binary-tree-from-preorder-and-inorder-traversal/description/每次在中序遍历序列片段中找到前序遍历序列队首,这是该层的根节点该位置左侧是左子树,右侧是右子树再......