⭐️ 本文已收錄到 AndroidFamily,技術和職場問題,請關注公眾號 [彭旭銳] 和 [BaguTree Pro] 知識星球提問。
學習資料結構與演演算法的關鍵在於掌握問題背後的演演算法思維框架,你的思考越抽象,它能覆蓋的問題域就越廣,理解難度也更復雜。在這個專欄裡,小彭與你分享每場 LeetCode 周賽的解題報告,一起體會上分之旅。
本文是 LeetCode 上分之旅系列的第 33 篇文章,往期回顧請移步到文章末尾~
T1. 特殊元素平方和(Easy)
T2. 陣列的最大美麗值(Medium)
T3. 合法分割的最小下標(Medium)
T4. 最長合法子字串的長度(Hard)
https://leetcode.cn/problems/sum-of-squares-of-special-elements/
簡單模擬題,列舉每個下標檢查是否能被 n 整除,同時記錄結果。
class Solution {
public:
int sumOfSquares(vector<int>& nums) {
int ret = 0;
int n = nums.size();
for (int i = 0; i < nums.size(); i++) {
if (n % (i + 1) == 0) ret += nums[i] * nums[i];
}
return ret;
}
};
複雜度分析:
事實上,當下標 i 可以被 n 整除時,那麼有下標 n / i 也可以被 n 整除,因此我們只需要檢查 [0, \sqrt(n)] 的範圍。
class Solution {
public:
int sumOfSquares(vector<int>& nums) {
int ret = nums[0] * nums[0];
int n = nums.size();
if (n < 2) return ret;
ret += nums[n - 1] * nums[n - 1];
for (int i = 2; i <= sqrt(n); i++) {
if (n % i != 0) continue;
ret += nums[i - 1] * nums[i - 1];
if (i != n / i) {
ret += nums[n / i - 1] * nums[n / i - 1];
}
}
return ret;
}
};
複雜度分析:
其他語言解法見 LeetCode 題解頁:列舉優化的 O(sqrt(n) 時間解法(C++/Python/Kotlin)
https://leetcode.cn/problems/maximum-beauty-of-an-array-after-applying-operation/
根據題目操作描述,每個元素都可以修改為範圍在 [nums[i] - k, nums[i] + k] 之間的任意元素,我們把兩個元素的差視為元素的相似度,那麼差值小於 2*k 的兩個數就能夠轉換為相等數(增大較小數,同時減小較大數)。
由於美麗值和陣列順序無關,我們先對陣列排序,然後列舉元素作為左值,再尋找最遠可匹配的右值(nums[i] + 2 * k),可以使用二分查詢尋找不大於右值的最大元素。
class Solution {
public:
int maximumBeauty(vector<int>& nums, int k) {
sort(nums.begin(), nums.end());
int ret = 0;
for (int i = 0; i < nums.size(); i++) {
int left = i;
int right = nums.size() - 1;
while (left < right) {
int mid = (left + right + 1) / 2;
if (nums[mid] > nums[i] + 2 * k) {
right = mid - 1;
} else {
left = mid;
}
}
ret = max(ret, left - i + 1);
}
return ret;
}
};
複雜度分析:
根據題目操作描述,每個元素都可以修改為範圍在 [nums[i] - k, nums[i] + k] 之間的任意元素,我們把這個範圍視為一個可選區間。那麼問題的最大美麗值正好就是所有區間的最多重疊數,這就是經典的 leetcode 253. 會議室 II 問題
由於區間重疊數和順序無關,我們可以對所有元素排序(由於區間長度相等,等價於按照結束時間排序),使用同向雙指標求解:
class Solution {
public:
int maximumBeauty(vector<int>& nums, int k) {
sort(nums.begin(), nums.end());
int i = 0;
int ret = 0;
for (int j = 0; j < nums.size(); j++) {
while (nums[j] - k > nums[i] + k) i++;
ret = max(ret, j - i + 1);
}
return ret;
}
};
複雜度分析:
其他語言解法見 LeetCode 題解頁:會議室問題求最大重疊區間數、同向雙指標(C++/Python/Kotlin/TypeScript)
https://leetcode.cn/problems/minimum-index-of-a-valid-split/
根據題目描述,支配元素是指陣列中的眾數,同時要求出現次數嚴格大於陣列一半長度,所以支配元素可能是 -1。其實,支配元素的定義與經典題目 169. 多數元素 和 劍指 Offer 39. 陣列中出現次數超過一半的數位 定義是相同的。
容易證明,無論陣列如何分割,子陣列的支配元素要麼不存在,要麼就等於原陣列的支配元素:
因此,我們的演演算法是:
class Solution {
public:
int minimumIndex(vector<int>& nums) {
// 計算支配元素
unordered_map<int, int> cnts;
int x = -1;
for (int i = 0; i < nums.size(); i++) {
++cnts[nums[i]];
if (x == -1 || cnts[nums[i]] > cnts[x]) {
x = nums[i];
}
}
// 列舉分割點
int leftXCnt = 0;
for (int i = 0; i < nums.size(); i++) {
if (nums[i] != x) continue;
leftXCnt++;
if (leftXCnt * 2 > i + 1 && (cnts[x] - leftXCnt) * 2 > nums.size() - 1 - i) return i;
}
return -1;
}
};
複雜度分析:
題解一中使用雜湊表求原陣列的支配元素,可以使用摩爾投票演演算法來優化空間複雜度:
class Solution {
public:
int minimumIndex(vector<int>& nums) {
// 計算支配數
int x = -1;
int count = 0;
for (int i = 0; i < nums.size(); i++) {
if (0 == count) x = nums[i];
if (nums[i] == x) count++; else count --;
}
// 計算支配數出現次數
int total = 0;
for (int i = 0; i < nums.size(); i++) {
if (nums[i] == x) total ++;
}
// 列舉分割點
int leftXCnt = 0;
for (int i = 0; i < nums.size(); i++) {
if (nums[i] != x) continue;
leftXCnt++;
if (leftXCnt * 2 > i + 1 && (total - leftXCnt) * 2 > nums.size() - 1 - i) return i;
}
return -1;
}
};
複雜度分析:
其他語言解法見 LeetCode 題解頁:數學、前字尾分解、摩爾投票 O(1) 空間(C++/Python/Kotlin)
https://leetcode.cn/problems/length-of-the-longest-valid-substring/
這道題中 forbidden[i] 字串的長度不超過 10,說明檢查字串匹配的時間常數是比較低的,我們先考慮暴力的解法。
class Solution {
fun longestValidSubstring(word: String, forbidden: List<String>): Int {
val forbiddenSet = forbidden.toHashSet()
var ret = 0
for (i in 0 until word.length) {
for (j in i until word.length) {
if (!check(forbiddenSet, word, i, j)) break // 後續子串不可能合法
ret = Math.max(ret, j - i + 1)
}
}
return ret
}
// return:是否合法
private fun check(set: Set<String>, word: String, i: Int, j: Int): Boolean {
// 檢查 [i,j] 中以新增字母 nums[j] 為右端點的所有子串方案是否被禁用
for (k in j downTo i) {
val key = word.substring(k, j + 1)
if (set.contains(key)) return false
}
return true
}
}
複雜度分析:
提示:我們可以使用捲動雜湊優化 check 的時間複雜度到 O(M),但由於 M 本身很小,優化效果不高。
這道題需要結合 KMP 思想。
題解一中的 check 會重複計算多次子串,需要想辦法剪枝:
class Solution {
fun longestValidSubstring(word: String, forbidden: List<String>): Int {
// word = "leetcode", forbidden = ["de","le","e"]
val forbiddenSet = forbidden.toHashSet()
var ret = 0
var i = 0
for (j in 0 until word.length) {
// 不合法
while (true) {
val pivot = check(forbiddenSet, word, i, j)
if (-1 != pivot) i = pivot + 1 else break
}
ret = Math.max(ret, j - i + 1)
}
return ret
}
// return:最早的非法子串的起始位置
private fun check(set: Set<String>, word: String, i: Int, j: Int): Int {
// 檢查 [i,j] 中以新增字母 nums[j] 為右端點的所有子串方案是否被禁用
for (k in Math.max(i, j - 10) .. j) {
val key = word.substring(k, j + 1)
if (set.contains(key)) return k
}
return -1
}
}
複雜度分析:
推薦閱讀
LeetCode 上分之旅系列往期回顧:
⭐️ 永遠相信美好的事情即將發生,歡迎加入小彭的 Android 交流社群~