目錄
- 題目A - 陣列的最大因子得分
- 題目B - 字串轉換後的長度Ⅰ
- 題目C - 最大公約數相等的子序列數量
- 題目D - 字串轉換後的長度Ⅱ
- 總結與思考
題目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
,常數空間
總結與思考
感覺題目還是不錯的,上了大分~~~