1 题目
给你一个整数数组 nums 和一个整数 k。
如果一个数组的 最大 元素的值 至多 是其 最小 元素的 k 倍,则该数组被称为是 平衡的。
你可以从 nums 中移除 任意 数量的元素,但不能使其变为 空数组。
返回为了使剩余数组平衡,需要移除的元素的 最小数量。
**注意:**大小为 1 的数组被认为是平衡的,因为其最大值和最小值相等,且条件总是成立。
示例 1:
**输入:**nums = [2,1,5], k = 2
**输出:**1
解释:
- 移除
nums[2] = 5得到nums = [2, 1]。 - 现在
max = 2,min = 1,且max <= min * k,因为2 <= 1 * 2。因此,答案是 1。
示例 2:
**输入:**nums = [1,6,2,9], k = 3
**输出:**2
解释:
- 移除
nums[0] = 1和nums[3] = 9得到nums = [6, 2]。 - 现在
max = 6,min = 2,且max <= min * k,因为6 <= 2 * 3。因此,答案是 2。
示例 3:
**输入:**nums = [4,6], k = 2
**输出:**0
解释:
- 由于
nums已经平衡,因为6 <= 4 * 2,所以不需要移除任何元素。
提示:
1 <= nums.length <= 1051 <= nums[i] <= 1091 <= k <= 105
2 代码实现
cpp
class Solution {
public:
int minRemoval(vector<int>& nums, int k) {
int left = 0 ;
int right = 0 ;
int max_len = 0 ;
sort (nums.begin(),nums.end());
while(right < nums.size()){
while ((long long )nums[left] * k < nums[right]){
left ++;
}
right++;
max_len = max(max_len , right - left);
}
return nums.size() - max_len;
}
};
思考
首先对这个数组排序,那么就知道max和min,也就可以分别得到对于它们平衡的值。把这两个值作为边界,好像就直接得到了两端的长度,比较一下最少移除的就好了,和滑动窗口有什么关系呢......
卡住了...
先明确核心逻辑:排序后,平衡子数组的特性
首先,排序是关键前提:将数组排序后,任意一个子数组的最小值是子数组的第一个元素,最大值是子数组的最后一个元素(因为数组升序排列)。这就把 "找任意子数组的最大 / 最小值" 的问题,简化成了 "找子数组的首尾元素"。
你的思路 "找平衡的边界" 是对的,但为什么需要滑动窗口?因为数组长度可以达到\(10^5\),如果用暴力枚举所有子数组(时间复杂度\(O(n^2)\)),会超时。而滑动窗口可以在\(O(n)\)的时间复杂度内找到最长的平衡子数组,进而算出最少移除数目(最少移除数 = 数组总长度 - 最长平衡子数组长度)。
解题思路拆解
- 排序数组 :将
nums升序排列,这样子数组[left, right]的最小值是nums[left],最大值是nums[right]。 - 滑动窗口找最长平衡子数组 :
- 用右指针
right遍历数组,对于每个right,找到最小的left,使得nums[right] <= nums[left] * k(满足平衡条件)。 - 此时,子数组
[left, right]是以right为右端点的最长平衡子数组,记录其长度。
- 用右指针
- 计算最少移除数:数组总长度减去最长平衡子数组的长度,就是需要移除的最少元素数目。
自己写的代码错误百出
cpp
class Solution {
public:
int minRemoval(vector<int>& nums, int k) {
int left = nums.size() ;
int right = 0 ;
int res = 0 ;
sort (0 , nums.size() - 1);
while(right < nums.size() && nums[right] < k * nums[0]){
right ++;
}
while(left > 0 && nums[left] > nums[nums.size()-1 ] / k){
left --;
}
res = max(nums.size() - right , left);
return res;
}
};
你现在的代码尝试解决 "使数组平衡的最少移除数目" 问题,但存在多处关键错误,包括语法错误、逻辑错误和思路偏差,导致代码无法运行也无法得到正确结果。我会先逐一指出问题,再结合你的思路修正,最后给出正确的代码。
首先,明确你的核心思路问题
你试图通过分别找左边界(right)和右边界(left)来计算移除数目,但这个思路是片面的 ------ 平衡子数组不一定是以原数组的第一个元素为最小值,也不一定是以最后一个元素为最大值,比如示例 2 中nums = [1,6,2,9]排序后是[1,2,6,9],平衡子数组是[2,6]或[6,9],既不包含第一个元素1,也不包含最后一个元素9。
因此,仅靠首尾元素的边界判断无法覆盖所有情况,必须用滑动窗口遍历所有可能的平衡子数组。
代码中的具体错误
- 排序语法错误 :
sort(0, nums.size() - 1)是错误的,C++ 的sort函数需要传入迭代器(如nums.begin()、nums.end()),而不是整数索引。 - 数组越界问题 :
nums[left]中left初始值是nums.size(),直接访问会超出数组范围(数组索引最大为nums.size()-1)。 - 逻辑条件错误 :
nums[right] < k * nums[0]的条件错误,平衡条件是最大值 ≤ 最小值 × k ,而不是小于;且最小值不应固定为nums[0]。nums[left] > nums[nums.size()-1] / k的条件错误,除法会导致精度丢失(比如6 ≤ 2×3,但6/3=2,而9 ≤6×3,但9/3=3,用除法会出错),且最大值不应固定为nums.back()。
- 结果计算错误 :
max(nums.size() - right, left)的逻辑无法正确表示最少移除数目,因为它只考虑了两种极端情况,没有考虑中间的平衡子数组。
修正后的代码(结合滑动窗口的正确思路)
我会保留你代码中 "边界判断" 的想法,但替换为滑动窗口的核心逻辑,确保代码能正确运行并解决问题:
cpp
#include <vector>
#include <algorithm>
using namespace std;
class Solution {
public:
int minRemoval(vector<int>& nums, int k) {
// 1. 修正排序:使用迭代器排序,升序排列
sort(nums.begin(), nums.end());
int n = nums.size();
int left = 0; // 滑动窗口的左指针
int max_len = 0; // 记录最长的平衡子数组长度
// 2. 滑动窗口:右指针遍历所有元素,左指针收缩以满足平衡条件
for (int right = 0; right < n; ++right) {
// 平衡条件:nums[right] (最大值) <= nums[left] (最小值) * k
// 用long long防止整数溢出(比如nums[left]=1e9, k=1e9,相乘会超int范围)
while ((long long)nums[left] * k < nums[right]) {
left++; // 不满足条件,收缩左指针
}
// 更新最长平衡子数组的长度
max_len = max(max_len, right - left + 1);
}
// 3. 最少移除数目 = 总长度 - 最长平衡子数组长度
return n - max_len;
}
};
代码关键部分解释
- 排序修正 :
sort(nums.begin(), nums.end())将数组升序排列,这是滑动窗口的前提 ------ 排序后,子数组[left, right]的最小值是nums[left],最大值是nums[right]。 - 滑动窗口逻辑 :
- 右指针
right逐个遍历元素,作为子数组的右端点(最大值)。 - 内层
while循环:如果nums[left] * k < nums[right](不满足平衡条件),则将左指针left右移,直到满足条件(此时子数组[left, right]是平衡的)。 - 每次计算当前平衡子数组的长度,更新
max_len(最长平衡子数组长度)。
- 右指针
- 结果计算 :数组总长度
n减去max_len,就是需要移除的最少元素数目(因为保留的元素越多,移除的就越少)。
用示例验证代码
示例 1:nums = [2,1,5], k = 2
- 排序后:
[1,2,5] - 遍历过程:
right=0:left=0,max_len=1。right=1:1×2=2 ≥2,max_len=2。right=2:1×2=2 <5→left=1;2×2=4 <5→left=2;max_len仍为 2。
- 结果:
3-2=1(正确)。
示例 2:nums = [1,6,2,9], k = 3
- 排序后:
[1,2,6,9] - 遍历过程:
right=0:max_len=1。right=1:1×3=3 ≥2,max_len=2。right=2:1×3=3 <6→left=1;2×3=6 ≥6,max_len=2。right=3:2×3=6 <9→left=2;6×3=18 ≥9,max_len=2。
- 结果:
4-2=2(正确)。
示例 3:nums = [4,6], k = 2
- 排序后:
[4,6] - 遍历过程:
right=0:max_len=1。right=1:4×2=8 ≥6,max_len=2。
- 结果:
2-2=0(正确)。
第一次错误总结
- 原代码的核心问题是思路片面 (仅考虑首尾元素的边界)和语法 / 逻辑错误(排序、越界、条件判断),无法覆盖所有平衡子数组的情况。
- 正确的解法是先排序,再用滑动窗口找最长平衡子数组,时间复杂度为\(O(n \log n)\)(排序)+\(O(n)\)(滑动窗口),能高效处理\(10^5\)长度的数组。
- 平衡条件的关键是用乘法判断(最大值 ≤ 最小值 × k) ,避免除法的精度丢失,同时用
long long防止整数溢出。
做不来,先放着了,不死磕了,自己慢慢想一想怎么用map维护?
回头看,也不是所有的滑动窗口都需要map来维护。
重新再写的代码错误百出 * 2
cpp
class Solution {
public:
int minRemoval(vector<int>& nums, int k) {
int left = nums.size() ;
int right = 0 ;
int max_len = 0 ;
sort (nums.begin(),nums.end());
while(right < nums.size()){
right ++;
if ((long long )nums[left] * k < nums[right]){
left ++;
}
max_len = max(max_len , right - left);
}
return nums.size() - max_len;
}
};
原代码的修改版本(逐处标注错误修正)
cpp
#include <vector>
#include <algorithm>
using namespace std;
class Solution {
public:
int minRemoval(vector<int>& nums, int k) {
// 错误1:left初始化为nums.size()会直接越界,改为0(有效起始下标)
int left = 0;
int right = 0;
int max_len = 0;
sort(nums.begin(), nums.end());
while (right < nums.size()) {
// 错误2:原代码先right++再判断,会跳过第一个元素且容易越界,调整逻辑:先判断再计算长度
// 修正:先检查当前窗口是否满足条件,不满足则移动left
// 错误3:逻辑条件写反(nums[right]是最大值,应判断nums[right] > nums[left]*k),且符号错误(原是<,应改为>)
while ((long long)nums[left] * k < nums[right]) {
left++;
}
// 错误4:原窗口长度是right-left,漏了+1(下标从0开始,包含左右边界)
max_len = max(max_len, right - left + 1);
// 最后再移动right,避免提前越界
right++;
}
return nums.size() - max_len;
}
};
关键修改点解释(对应原代码的错误)
- left 初始化错误修正 :原代码
left = nums.size()是数组的末尾下标 + 1,直接超出有效范围,改为left = 0(数组第一个元素的下标)。 - right 操作顺序修正 :原代码先
right++再判断,会导致:- 第一个元素(right=0)被直接跳过,从未参与判断;
- 当 right 到达数组最后一个元素时,
right++后等于nums.size(),访问nums[right]会越界。修正后:先处理当前 right 的逻辑,再right++。
- 条件判断错误修正 :
- 原逻辑
nums[left] * k < nums[right]的符号错误,且没有用循环保证窗口内所有元素都满足条件(原代码是 if,应改为 while); - 正确逻辑是:当窗口内的最大值(
nums[right])大于最小值(nums[left])的 k 倍时,持续移动 left 缩小窗口。
- 原逻辑
- 窗口长度计算错误修正 :原代码
right - left漏了+1,因为下标从 0 开始,比如 left=0、right=2 时,窗口包含 3 个元素(0、1、2),长度应该是2-0+1=3。
测试用例验证
用之前的例子nums = [3,5,8,10,14], k=2测试,修改后的代码执行过程:
- 排序后数组:
[3,5,8,10,14] - 遍历结束后,max_len=3(对应子数组
[5,8,10]) - 返回
5-3=2,结果正确。
第二次错误总结
- 核心修改点:left 初始化改为 0 、right 操作顺序调整(先处理后自增) 、条件判断从 if 改为 while 并修正符号 、窗口长度加 1。
- 保留了你原代码的
while循环结构,仅修正错误逻辑,没有重构整体代码。 - 注意用
long long强制类型转换,避免nums[left] * k出现整数溢出的问题。
3 题目
给你两个长度相同的字符串,s 和 t。
将 s 中的第 i 个字符变到 t 中的第 i 个字符需要 |s[i] - t[i]| 的开销(开销可能为 0),也就是两个字符的 ASCII 码值的差的绝对值。
用于变更字符串的最大预算是 maxCost。在转化字符串时,总开销应当小于等于该预算,这也意味着字符串的转化可能是不完全的。
如果你可以将 s 的子字符串转化为它在 t 中对应的子字符串,则返回可以转化的最大长度。
如果 s 中没有子字符串可以转化成 t 中对应的子字符串,则返回 0。
示例 1:
输入:s = "abcd", t = "bcdf", maxCost = 3
输出:3
解释:s 中的 "abc" 可以变为 "bcd"。开销为 3,所以最大长度为 3。
示例 2:
输入:s = "abcd", t = "cdef", maxCost = 3
输出:1
解释:s 中的任一字符要想变成 t 中对应的字符,其开销都是 2。因此,最大长度为 1。
示例 3:
输入:s = "abcd", t = "acde", maxCost = 0
输出:1
解释:a -> a, cost = 0,字符串未发生变化,所以最大长度为 1。
提示:
1 <= s.length, t.length <= 10^50 <= maxCost <= 10^6s和t都只含小写英文字母。
4 代码实现
cpp
class Solution {
public:
int equalSubstring(string s, string t, int maxCost) {
int n = s.size() ;
vector<int> cost(n);
for (int i = 0 ; i < n ; i++){
cost[i] = abs(s[i] - t[i]);
}
int left = 0 ;
int right = 0 ;
int sum = 0 ;
int res = 0 ;
while(right < n ){
sum += cost[right];
while(sum > maxCost){
sum -= cost[left];
left ++;
}
right ++;
res = max(res , right - left);
}
return res;
}
};
思考
每一个字符一一对应都会有一个对应的开销,那么,其实两个字符串的开销就是一个数组。
比如[1,2,1,0],如果maxCost是3,最长的字符串就是[2,1,0]相当于我在维护一个滑动窗口,使得窗口内的数字和sum小于maxCost。
第一步,用unordered_map整合出一个开销数组。(其实没必要,直接vector<int>数组就好了,因为第一个值存放的是下标啊!)
第二步,根据所给的maxcost,维护一个滑动窗口,最后返回一个滑动窗口的最长长度值。就是可以转化的最大长度。
乱七八糟写
cpp
class Solution {
public:
int equalSubstring(string s, string t, int maxCost) {
int n = s.size() ;
vector<int> cost(n);
for (int i = 0 ; i < n ; i++){
vector[i] = abs(s[i] - t[i]);
}
int left = 0 ;
int right = 0 ;
int sum = 0 ;
int res = 0 ;
while(right < n ){
sum += cost[right];
while(sum > maxCost){
sum -= cost[left];
left ++;
}
right ++;
res = max(res , right - left);
}
return res;
}
};
你这段代码的思路是正确的:
先计算每个位置的开销,再用滑动窗口找总开销不超过maxCost的最长子串长度,但代码中存在一个明显的语法错误,还有一处可以优化的细节,我会帮你修正并说明。
代码中的错误与修正
- 语法错误 :你在循环中写的是
vector[i] = abs(s[i] - t[i]);,但vector是 C++ 的容器类型名,不是你定义的cost数组,正确的应该是cost[i]。 - 可选优化(非错误,但建议修改) :
s[i]和t[i]是char类型,直接相减可能因编译器的符号问题(比如char是signed类型)导致差值计算异常,建议显式转换为int后再计算。
修正后的完整代码
cpp
#include <iostream>
#include <string>
#include <vector>
#include <cmath> // 用于abs函数
using namespace std;
class Solution {
public:
int equalSubstring(string s, string t, int maxCost) {
int n = s.size();
vector<int> cost(n);
for (int i = 0; i < n; i++) {
// 修正1:将vector[i]改为cost[i]
// 优化:显式转换为int,避免char符号问题
cost[i] = abs(static_cast<int>(s[i]) - static_cast<int>(t[i]));
}
int left = 0;
int right = 0;
int sum = 0;
int res = 0;
while (right < n) {
sum += cost[right];
while (sum > maxCost) {
sum -= cost[left];
left++;
}
right++;
res = max(res, right - left);
}
return res;
}
};
// 测试用例
int main() {
Solution sol;
string s = "abcd";
string t = "bcdf";
int maxCost = 3;
cout << sol.equalSubstring(s, t, maxCost) << endl; // 输出3
return 0;
}
代码逻辑解释(确认你的思路正确性)
你的滑动窗口逻辑是没问题的,执行流程如下:
- 右指针扩展窗口 :
right逐个遍历数组,将当前位置的开销加入sum(窗口总开销)。 - 左指针收缩窗口 :如果
sum超过maxCost,就移动left指针,减去对应位置的开销,直到sum ≤ maxCost。 - 更新最长长度 :先将
right自增,再计算right - left(此时窗口的长度是right - left,因为right已经指向了下一个位置),并更新res。
这里需要注意:你用right++后计算right - left,和常规的right - left + 1(right不自增时)是等价的,比如:
- 当
right=0时,处理后right=1,right-left=1(对应窗口 [0],长度 1); - 当
right=2时,处理后right=3,right-left=3(对应窗口 [0,1,2],长度 3)。
所以你的窗口长度计算方式是正确的,不需要修改。
总结
- 核心错误修正 :将
vector[i]改为cost[i],这是代码无法编译的关键原因。 - 细节优化 :将
char类型显式转换为int后再计算差值,避免符号问题导致的错误。 - 逻辑确认:滑动窗口的执行流程和长度计算方式是正确的,能够得到正确结果。
我就这么直接写了!
将vector[i]改为 cost[i],注意细节!
cpp
class Solution {
public:
int equalSubstring(string s, string t, int maxCost) {
int n = s.size() ;
vector<int> cost(n);
for (int i = 0 ; i < n ; i++){
cost[i] = abs(s[i] - t[i]);
}
int left = 0 ;
int right = 0 ;
int sum = 0 ;
int res = 0 ;
while(right < n ){
sum += cost[right];
while(sum > maxCost){
sum -= cost[left];
left ++;
}
right ++;
res = max(res , right - left);
}
return res;
}
};
5 小结
做了这两题,我的感受是:
1.不定长的滑动窗口确实没有那么可怕,题目总是多多总结才能得到思路,这是心法上的感悟。不能盲目做题不回头看。
2.首先要把题目的意思理解,自己变形。
比如第一题变成了找一个有且仅有一个0的最长数组长度;第二题变成了开销数组,维护这个开销数组的最长的长度。
3.第一步就是完成抽象的转化;第二步是完成滑动窗口的维护,里面的一些细节,边界的处理,类型的转换也都是要注意的地方。特别是right,left应该在什么情况下更新,这个一定是经验积累下才不会弄错的。多多自己动手拿测试用例模拟一遍!
(ง •_•)ง加油!