数字小镇 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;
}
}
给你一个大小为 4 的整数数组 a
和一个大小 至少为 4 的整数数组 b
。
你需要从数组 b
中选择四个下标 i0
, i1
, i2
, 和 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];
}
}
给你一个字符串数组 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