⭐️ 本文已收录到 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 交流社群~