20241027LeetCode421周赛
目录
题目A - 数组的最大因子得分
题目概述
给你一个整数数组 nums。
因子得分 定义为数组所有元素的最小公倍数(LCM)与最大公约数(GCD)的 乘积。
在 最多 移除一个元素的情况下,返回 nums 的 最大因子得分。
注意,单个数字的 LCM 和 GCD 都是其本身,而 空数组 的因子得分为 0。
解题思路
- 暴力枚举每一个元素作为被移除元素在此题也是可以的,因为n的大小只有100。
- 考虑优化,我们可以用一个前缀数组和一个后缀数组,去记录当前前缀或后缀的
gcd/lcm
- 最后遍历前后缀数组即可,注意
gcd(a, b, c) = gcd(gcd(a, b), c)
和lcm(a, b, c) = lcm(lcm(a, b), c)
代码实现
public long maxScore(int[] nums) {
int n = nums.length;
if (n == 1) return 1l * nums[0] * nums[0];
long[] preGCD = new long[n], sufGCD = new long[n];
long[] preLCM = new long[n], sufLCM = new long[n];
preGCD[0] = nums[0];
preLCM[0] = nums[0];
for (int i = 1; i < n; i++) {
preGCD[i] = gcd(preGCD[i - 1], nums[i]);
preLCM[i] = lcm(preLCM[i - 1], nums[i]);
}
sufGCD[n - 1] = nums[n - 1];
sufLCM[n - 1] = nums[n - 1];
for (int i = n - 2; i >= 0; i--) {
sufGCD[i] = gcd(sufGCD[i + 1], nums[i]);
sufLCM[i] = lcm(sufLCM[i + 1], nums[i]);
}
long ans = sufGCD[0] * sufLCM[0];
for (int i = 0; i < n; i++) {
long curGCD, curLCM;
if (i == 0) {
curGCD = sufGCD[1];
curLCM = sufLCM[1];
} else if (i == n - 1) {
curGCD = preGCD[n - 2];
curLCM = preLCM[n - 2];
} else {
curGCD = gcd(preGCD[i - 1], sufGCD[i + 1]);
curLCM = lcm(preLCM[i - 1], sufLCM[i + 1]);
}
ans = Math.max(ans, curGCD * curLCM);
}
return ans;
}
private static long gcd(long a, long b) {
return b == 0 ? a : gcd(b, a % b);
}
private static long lcm(long a, long b) {
return (a / gcd(a, b)) * b;
}
复杂度分析
- 时间复杂度:
O(n)
,处理前后缀数组O(n)
,遍历数组O(n)
- 空间复杂度:
O(n)
,用于存储前后缀数组
题目B - 字符串转换后的长度Ⅰ
题目概述
给你一个字符串 S
和一个整数 t
,表示要执行的 转换 次数。每次转换需要根据以下规则替换字符串 S
中的每个字符:
- 如果字符是
'z'
,则将其替换为字符串"ab"
。 - 否则,将其替换为字母表中的下一个字符。例如,
'a'
替换为'b'
,'b'
替换为'c'
,依此类推。
返回恰好执行 t
次转换后得到的字符串的长度。
由于答案可能非常大,返回其对 (10^9 + 7)
取余的结果。
解题思路
- 先看数据范围,
t
最大为10^5
且只有小写字母,所以模拟每一次变化的情况是可行的 - 用两个长度为26的长整形数组,其中一个初始化每个位置设置为1,然后按照题意逐步模拟各个元素出现的变化
- 维护一个count数组,计算源字符串中各个字母出现的频数
- 对结果求和,求和逻辑为
count[i] * dp[i]
,边求和边取模防止溢出
代码实现
public int lengthAfterTransformations(String s, int t) {
int n = s.length(), MOD = 1000000007;
long[] dp1 = new long[26], dp2 = new long[26];
Arrays.fill(dp1, 1);
for (int step = 1; step <= t; step++) {
for (int c = 0; c < 26; c++) {
if (c < 25) {
dp2[c] = dp1[c + 1];
} else {
dp2[c] = (dp1[0] + dp1[1]) % MOD;
}
}
for (int i = 0; i < 26; i++) {
dp1[i] = dp2[i];
}
}
int[] count = new int[26];
for (char c : s.toCharArray()) {
count[c - 'a']++;
}
long ans = 0;
for (int c = 0; c < 26; c++) {
ans = (ans + dp1[c] * count[c]) % MOD;
}
return (int) ans;
}
复杂度分析
- 时间复杂度:
O(t + n)
,严格来说是确切的运算量是2 * 26 * t + n = 52t + n
- 空间复杂度:
O(1)
,使用的都是常数空间
题目C - 最大公约数相等的子序列数量
题目概述
给你一个整数数组 nums
。
请你统计所有满足一下条件的非空 子序列 对 (seq1, seq2)
的数量:
- 子序列
seq1
和seq2
不相交,意味着nums
中不存在同时出现在两个序列中的下标。 seq1
元素的 GCD 等于seq2
元素的 GCD。
返回满足条件的子序列对的总数。
由于答案可能非常大,请返回其对 (10^9 + 7)
取余的结果。
解题思路
- 遇事不决,先观察数据量,数组长度和元素大小都在200以内,而极端情况
200 * 200 * 200 = 8 * 10^6
是符合题意的 - 定义一个二维的
dp
数组,dp[i][j]
表示在已经选择的元素形成两个子序列的GCD
分别为i
和j
的方案数 - 对于数组中的每个元素
num
,创建一个新的动态规划状态,分为 (不加入,加入seq1
,加入seq2
) 三种状态 - 将
gcd
从1
到max(nums)
的情况累加即可
代码实现
public static int subsequencePairCount(int[] nums) {
int n = nums.length, maxNum = Integer.MIN_VALUE;
int MOD = 1000000007;
for (int i = 0; i < n; i++) maxNum = Math.max(maxNum, nums[i]);
int[][] dp = new int[maxNum + 1][maxNum + 1];
dp[0][0] = 1;
for (int num : nums) {
int[][] temp = new int[maxNum + 1][maxNum + 1];
for (int i = 0; i <= maxNum; i++) {
for (int j = 0; j <= maxNum; j++) {
int prev = dp[i][j];
if (prev == 0) continue;
temp[i][j] = (temp[i][j] + prev) % MOD;
int i1 = i == 0 ? num : gcd(num, i);
temp[i1][j] = (temp[i1][j] + prev) % MOD;
int j1 = j == 0 ? num : gcd(num, j);
temp[i][j1] = (temp[i][j1] + prev) % MOD;
}
}
dp = temp;
}
int ans = 0;
for (int i = 1; i <= maxNum; i++) {
ans = (ans + dp[i][i]) % MOD;
}
return ans;
}
private static int gcd(int a, int b) {
return b == 0 ? a : gcd(b, a % b);
}
复杂度分析
- 时间复杂度:
O(n * k ^ 2)
, 其中k
为数组最大值,主要时间复杂度在处理dp
状态 - 空间复杂度:
O(k ^ 2)
, 用于进行dp
状态转移
题目D - 字符串转换后的长度Ⅱ
题目概述
给你一个由小写英文字母组成的字符串 s
,一个整数 t
表示要执行的 转换 次数,以及一个长度为 26 的数组 nums
。每次 转换 需要根据以下规则对字符串 s
中的每个字符:
-
将
s[i]
替换为字母表中后续的nums[s[i] - 'a']
个连续字符。例如,如果s[i] = 'a'
且nums[0] = 3
,则字符'a'
转换为后面 3 个连续字符,结果为"bcd"
。 -
如果转换超过了
'z'
,则回绕到字母表开头。例如,如果s[i] = 'y'
且nums[24] = 3
,则字符'y'
转换为后面 3 个连续字符,结果为"zab"
。
返回 恰好 执行 t
次转换后得到的字符串的 长度。
由于答案可能非常大,返回答案对 10^9 + 7
取余的结果。
解题思路
- 先看数据范围,
t
最大为10^9
,那每次模拟过程变化显然是不现实了,由于是一个线性的递推关系,考虑使用矩阵快速幂优化 - 对于一个简单的子问题,有这样一个转移方程
dp[i][j] = (dp[i - 1][j + 1] + ······ + dp[i - 1][j + nums[j]])
- 初始假设每个元素出现次数为
1
,初始化为一个26 * 1
的矩阵prev
, 考虑单次转换过程,其实是去乘一个26 * 26
大小的矩阵,设矩阵为matrix
- 单次的状态转移应该是:
curr = matrix * prev
,最终的运算结果应该是res = matrix ^ t * prev
- 最后计算每个元素在原始串中出现的次数,对各种情况求和即可,边求和边取模防止溢出
代码实现
public static int lengthAfterTransformations(String s, int t, List<Integer> nums) {
int MOD = 1000000007, n = 26;
long[][] matrix = new long[n][n];
for (int i = 0; i < n; i++) {
int len = nums.get(i);
for (int j = 0; j < len; j++) {
int k = (j + 1 + i) % n;
matrix[k][i] = (matrix[k][i] + 1) % MOD;
}
}
long[][] quickPowMatrix = matrixPower(matrix, t, MOD);
long[] cnt1 = new long[n], cnt2 = new long[n];
for (int i = 0; i < s.length(); i++) cnt1[s.charAt(i) - 'a']++;
for (int i = 0; i < n; i++) {
for (int j = 0; j < n; j++) {
cnt2[i] = (cnt2[i] + quickPowMatrix[i][j] * cnt1[j]) % MOD;
}
}
long ans = 0;
for (int i = 0; i < n; i++) ans = (ans + cnt2[i]) % MOD;
return (int) ans;
}
private static long[][] matrixPower(long[][] matrix, int t, int mod) {
int n = matrix.length;
long[][] ans = new long[n][n];
for (int i = 0; i < n; i++) {
ans[i][i] = 1;
}
while (t > 0) {
if ((t & 1) == 1) {
ans = multiplyMatrix(ans, matrix, mod);
}
matrix = multiplyMatrix(matrix, matrix, mod);
t >>= 1;
}
return ans;
}
private static long[][] multiplyMatrix(long[][] a, long[][] b, int mod) {
int n = a.length;
long[][] ans = new long[n][n];
for (int i = 0; i < n; i++) {
for (int k = 0; k < n; k++) {
if (a[i][k] == 0) continue;
for (int j = 0; j < n; j++) {
if (b[k][j] == 0) continue;
ans[i][j] = (ans[i][j] + a[i][k] * b[k][j]) % mod;
}
}
}
return ans;
}
复杂度分析
- 时间复杂度:
O(n + t * 26 ^ 3)
, 遍历数组O(n)
, 处理矩阵O(t * 26 ^ 3)
- 空间复杂度:
O(1)
, 与字符集相关,该题为26
,常数空间
总结与思考
感觉题目还是不错的,上了大分~~~