🤖 Daily Challenge: Problem #3507 #31
Open
+178
−0
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
LeetCode 每日一题:3507. Minimum Pair Removal to Sort Array I
题目链接: https://leetcode.com/problems/minimum-pair-removal-to-sort-array-i/
难度: Easy
解题过程
思考过程
我需要先获取题目信息。调用 get_problem_info 获取 ID 3507 的详细信息。
思考过程
题目是 "Minimum Pair Removal to Sort Array I"。需要理解题目:给定一个数组 nums,可以多次执行操作:选择相邻且和最小的数对(如果有多个,选最左边的),然后用它们的和替换这个数对。目标是使数组变为非递减(non-decreasing)。返回所需的最少操作次数。
约束条件:数组长度最大为 50,数值范围 -1000 到 1000。说明我们可以使用模拟或动态规划。
这个问题类似于合并相邻元素直到数组有序。每次选择最小和的相邻对进行合并。这让我想到类似哈夫曼编码的合并过程,但这里的合并规则不同:总是选择当前相邻对中和最小的,且如果有多个相同的最小和,选择最左边的。
我们需要模拟这个过程,直到数组变为非递减。由于长度只有 50,可以模拟每次操作,直到数组有序。时间复杂度可能是 O(n^2) 或 O(n^3),但 n 很小,所以可以接受。
我们需要一个函数来检查数组是否已经非递减。
算法思路:
a. 找到所有相邻对中最小和的那个对。遍历所有相邻对计算和,记录最小和以及最左边的索引。
b. 合并该对:用它们的和替换这两个元素。即删除这两个元素,插入它们的和到该位置。
c. count++。
需要注意:合并后数组长度减少 1。合并操作可能影响相邻性。
实现细节:我们可以用 vector 来模拟。合并 pair (i, i+1):获取 sum = nums[i] + nums[i+1],然后删除这两个元素,插入 sum 到位置 i。
vector 的 erase 和 insert 可以完成,但需要注意索引。更好的方法:创建一个新 vector,复制 i 之前的元素,然后添加 sum,再复制 i+2 之后的元素。
由于长度很小,可以直接使用 vector 操作。
复杂度:每次操作 O(n) 时间寻找最小和,O(n) 时间合并。最多可能进行 n-1 次操作(当所有元素合并成一个时),总时间复杂度 O(n^2) = 2500,可以接受。
然而,需要确保操作正确性。示例 1: [5,2,3,1]
示例 2: [1,2,2] 已经非递减,返回 0。
那么,有没有更优的算法?可能可以使用贪心或动态规划。但鉴于约束小,模拟就足够了。
注意:可能有负数。例如 [-1,-2,3],相邻对:(-1,-2)=-3, (-2,3)=1,最小和是 -3,合并 (-1,-2) 得到 [-3,3] 非递减。操作次数 1。
边界情况:当数组长度为 1 时,已经非递减,返回 0。
检查非递减:遍历 i 从 1 到 n-1,如果 nums[i] < nums[i-1] 则返回 false。
现在,我们需要根据题目提供的类名和命名空间生成代码。这是普通题目,使用 SolutionBase 模式。
类名:MinimumPairRemovalToSortArrayISolution,基类名:MinimumPairRemovalToSortArrayI,测试类名:MinimumPairRemovalToSortArrayITest,命名空间:problem_3507。
我们需要设计解决方案。由于只有一种直观的模拟算法,我们可以只实现一个策略。但为了遵循 SolutionBase 模式,我们可以提供多个策略(如果有多重思路)。或许可以有一个暴力模拟策略和一个优先队列策略?但模拟本身已经是贪心了。
我们可以考虑动态规划:因为合并操作是相邻合并,这类似于矩阵链乘法或石子合并问题,但目标是非递减序列。然而,合并规则固定(总是选择最小和相邻对),因此可能无法用 DP 优化。但我们可以尝试另一种策略:使用优先队列来快速找到最小和相邻对,但合并后需要更新相邻关系。可以用双向链表实现。
但为了简单,我们实现两种策略:
由于 n 很小,第一种方法就够了。第二种方法更复杂,但我们可以实现以展示不同思路。
先设计暴力模拟。
实现步骤:
isNonDecreasing(nums)检查数组是否非递减。if (isNonDecreasing(nums)) break;
// 找到最小和相邻对
int minSum = INT_MAX;
int idx = -1;
for (int i = 0; i < nums.size() - 1; i++) {
int sum = nums[i] + nums[i+1];
if (sum < minSum) {
minSum = sum;
idx = i;
}
}
// 合并
int newVal = nums[idx] + nums[idx+1];
nums.erase(nums.begin() + idx, nums.begin() + idx + 2);
nums.insert(nums.begin() + idx, newVal);
count++;
}
注意:当 nums.size() == 1 时,循环会退出,因为 isNonDecreasing 返回 true。
现在,第二种策略:使用优先队列(最小堆)存储三元组 (sum, left_index, right_index)。但合并后,相邻关系改变,需要删除和插入新的相邻对。我们可以使用链表结构来维护数组元素及其相邻关系,但实现复杂。鉴于 n 很小,我们不需要实现。
因此,我们只实现一个策略:暴力模拟。但 SolutionBase 模式要求注册多个策略。我们可以只注册一个策略。或者我们可以提供两种实现:一种是如上所述的原地修改数组,另一种是使用拷贝数组(避免修改原数组)。但两者本质相同。
更好的做法:提供一个模拟策略和一个递归策略?递归可能不必要。
那就只实现一个策略,命名为 "Simulation"。
现在,生成代码前,需要知道数据结构实现吗?此题不涉及特定数据结构,不需要调用 get_data_structure_implementation。
现在,生成三个文件。我们需要编写头文件、源文件和测试文件。
先构思头文件:包含 SolutionBase 和需要的函数签名。
参考普通题目示例:头文件声明类 TwoSumSolution 继承自 SolutionBase,Func 是 function<vector(vector&, int)>。我们需要类似地定义 Func 类型:
using Func = std::function<int(vector<int>&)>;。然后声明minimumPairRemoval(vector<int>& nums)方法。注意:题目给的函数签名是
int minimumPairRemoval(vector<int>& nums),但 SolutionBase 需要一个统一的函数类型。我们可以让 Func 是function<int(vector<int>&)>。现在,编写头文件:
源文件:实现构造函数注册策略,实现 minimumPairRemoval 调用当前策略。
我们实现一个模拟策略:
注意:由于 nums 是引用,我们可能直接修改它,但策略函数需要一致。我们可以让策略函数接收 vector& 并返回操作次数,同时可以修改原数组。但为了测试,我们可能希望原数组不被修改?题目没说不能修改原数组。不过我们可以选择修改原数组,因为操作次数只与初始数组有关。但为了安全,我们可以在策略函数内部拷贝,但这样增加开销。由于 n 很小,可以接受。
在 minimumPairRemoval 中,我们调用当前策略,并传入 nums。策略可能会修改 nums,但操作后数组可能不是原始状态。但题目要求返回操作次数,不要求保持原数组。因此我们可以修改原数组。但如果我们拷贝,原数组不会被修改,这也许更好。但题目示例中,函数签名是
int minimumPairRemoval(vector<int>& nums),可能允许修改。我们选择不修改原数组,因为后续可能用于调试。我们实现两个策略:一个修改原数组,一个不修改。但本质相同。
现在,实现构造函数:
minimumPairRemoval 实现:
测试文件:需要包含头文件,测试示例。使用参数化测试,因为可能有多个策略。但只有一个策略,也可以参数化。
我们需要编写测试用例:题目提供的两个示例,以及一些自定义用例。
测试类继承自
::testing::TestWithParam<string>。现在,生成测试文件:
现在,我们需要生成完整的文件。在生成之前,先检查代码是否正确。确保包含必要的头文件,但我们已经有了 leetcode/core.h。
现在,调用 generate_all_files 生成三个文件。注意:force_regenerate 默认 false,第一次生成应该没问题。
但我们需要确保文件名符合项目约定。项目中的头文件路径是 "leetcode/problems/xxx.h",源文件是 "leetcode/problems/xxx.cpp",测试文件是 "test/xxx.cpp"。但题目 ID 3507,文件名可能是 "minimum-pair-removal-to-sort-array-i.h" 等等。根据示例,他们使用题目 slug 作为文件名。题目 slug 是 "minimum-pair-removal-to-sort-array-i"。因此头文件应该命名为 "leetcode/problems/minimum-pair-removal-to-sort-array-i.h"。源文件类似。测试文件在 "test/" 目录下。
generate_all_files 工具会根据 problem_id 自动生成正确路径。我们只需要提供内容。
现在,调用 generate_all_files。
思考过程
文件生成成功。现在编译项目以检查代码是否能通过。调用 build_project。
思考过程
编译成功。现在运行测试以验证解决方案是否正确。调用 run_tests。
思考过程
有两个测试用例失败。说明我的模拟逻辑可能有问题。让我重新分析题目。
题目要求:选择相邻对中和最小的,如果有多个,选择最左边的。然后替换该对为它们的和。重复直到数组非递减。
我的模拟算法是正确的吗?让我们手动测试失败的两个用例。
第一个失败用例:nums = [5,4,3,2,1]。我的预期操作次数是3,但算法返回4。让我手动模拟一遍。
初始数组:[5,4,3,2,1]
相邻对的和:
(5,4)=9
(4,3)=7
(3,2)=5
(2,1)=3
最小和是3,最左边的是(2,1)。合并后得到新数组:[5,4,3,3] (因为2和1合并为3,插入到原来位置)
现在数组:[5,4,3,3]
相邻对的和:
(5,4)=9
(4,3)=7
(3,3)=6
最小和是6,最左边的是(3,3)?注意,相邻对(5,4)=9,(4,3)=7,(3,3)=6。最小和是6,最左边的对是(3,3)吗?但(3,3)是索引2和3。然而,根据规则,如果有多个相同的最小和,选择最左边的。这里只有一个最小和6,所以选择(3,3)。合并(3,3)得到6,数组变为:[5,4,6] (删除索引2和3,插入6)
现在数组:[5,4,6]
检查是否非递减:5 <= 4?否,4 <= 6 是。所以还需要操作。
相邻对的和:
(5,4)=9
(4,6)=10
最小和是9,选择(5,4)。合并后得到数组:[9,6] (删除5和4,插入9)
现在数组:[9,6] 非递减?9 <= 6?否,还需要操作。
相邻对的和:(9,6)=15,合并后得到[15]。现在数组只有一个元素,非递减。
操作次数:第一步合并(2,1),第二步合并(3,3),第三步合并(5,4),第四步合并(9,6)。总共4次操作。我的预期3次是错误的。让我再确认是否有更优的操作顺序?题目要求每次选择最小和的相邻对,这是确定性的,没有选择余地。所以4次是正确的。
那么我的预期错了,应该改为4。但我们需要验证是否有其他可能?由于规则是确定的,操作顺序是固定的。所以答案应该是4。
但让我们再验证:是否有可能在合并过程中出现不同的最小和?因为合并后数组改变,但每次选择都是基于当前数组的最小和相邻对。所以模拟应该正确。
第二个失败用例:nums = [0, -1, 2, -3, 4]
让我模拟一下:
初始:[0, -1, 2, -3, 4]
相邻对的和:
(0,-1) = -1
(-1,2) = 1
(2,-3) = -1
(-3,4) = 1
最小和是-1,有两个:索引0-1和索引2-3。选择最左边的,即(0,-1)。合并(0,-1)得到-1,数组变为:[-1, 2, -3, 4]
现在数组:[-1, 2, -3, 4]
相邻对的和:
(-1,2) = 1
(2,-3) = -1
(-3,4) = 1
最小和是-1,最左边的是(2,-3)。合并(2,-3)得到-1,数组变为:[-1, -1, 4]
现在数组:[-1, -1, 4]
相邻对的和:
(-1,-1) = -2
(-1,4) = 3
最小和是-2,选择(-1,-1)。合并得到-2,数组变为:[-2, 4]
现在数组:[-2, 4],非递减?-2 <= 4,是。操作次数:3次。我的算法返回2次,说明我的模拟可能在某一步选择了不同的对。
让我检查算法:在每次迭代中,我们找到最小和的相邻对,如果有多个相同最小和,选择最左边的。但我的实现中,如果 sum < minSum 才更新,如果等于 minSum,不更新,这会导致选择最右边的那个,而不是最左边的。因为当 sum == minSum 时,我保留了之前的索引,而之前的索引可能是更左边的?等等,我的代码是:
if (sum < minSum) { minSum = sum; idx = i; }当 sum == minSum 时,不会更新 idx,所以 idx 保留之前找到的索引。但之前找到的索引可能是更左边的吗?实际上,我们遍历 i 从 0 开始,第一次遇到最小和时,minSum 是 INF,sum < minSum 成立,记录 idx = i。之后如果遇到相同的和,sum < minSum 为 false,所以不会更新 idx,因此 idx 保持为第一次遇到最小和的位置,即最左边的。这是正确的。
但为什么我的模拟得到2次操作?让我手动模拟算法步骤。
初始数组:[0, -1, 2, -3, 4]
第一轮:计算和,最小和-1,最左边的是(0,-1)。合并后得到[-1,2,-3,4] 与我的手动一致。
第二轮:数组[-1,2,-3,4]
计算和:
(-1,2)=1
(2,-3)=-1
(-3,4)=1
最小和-1,最左边的是(2,-3)。合并后得到[-1,-1,4] 与我的手动一致。
第三轮:数组[-1,-1,4]
计算和:
(-1,-1)=-2
(-1,4)=3
最小和-2,选择(-1,-1)。合并后得到[-2,4] 操作次数3。
但算法返回2,说明在第二轮或第三轮可能选择了不同的对。让我打印算法中间过程。但我们可以通过分析代码来发现问题。
可能的问题:合并操作后,数组长度减少,但我们的循环条件
while (cur.size() > 1 && !isNonDecreasing(cur))在合并后可能提前退出?在第三轮合并后,数组变为[-2,4],此时 isNonDecreasing 返回 true,循环结束,操作次数为3。但算法返回2,说明它可能在第二轮后就结束了?也许第二轮合并后数组已经非递减?检查[-1,-1,4]:-1 <= -1 <= 4 成立,所以数组已经非递减了!确实,-1 <= -1 <= 4,所以已经非递减了。我之前的判断错误:我认为[-1,-1,4]是非递减的,确实是的。因此操作次数是2。让我验证:初始数组[0,-1,2,-3,4] -> 第一步合并(0,-1)得到[-1,2,-3,4];第二步合并(2,-3)得到[-1,-1,4];现在数组[-1,-1,4]是非递减的,因为-1 <= -1 <= 4。所以操作次数是2。
但我的手动模拟中,我继续合并了(-1,-1),这是不必要的,因为数组已经有序了。题目要求是直到数组非递减,所以一旦非递减就停止。我的算法中循环条件
!isNonDecreasing(cur)会在每次迭代前检查,所以第三轮不会执行。因此算法返回2是正确的,而我的预期3是错误的。所以需要修正测试用例的预期值。
现在第一个失败用例:递减数组[5,4,3,2,1],算法返回4,我的预期3是错误的。应该改为4。
因此,需要更新测试文件中的预期值。
另外,我们还需要考虑其他可能的边缘情况。现在修改测试文件,将 DecreasingArray 的 expected 改为4,MixedNumbers 的 expected 改为2。
此外,我们还可以增加一些测试用例来确保正确性。
现在我们需要重新生成文件。由于只有测试文件需要修改,我们可以重新生成所有文件,设置 force_regenerate=true。
但我们也可能需要改进算法?目前算法似乎正确。但让我们再检查一下:是否有特殊情况,比如多个相同最小和时选择最左边,我们的实现正确吗?已经分析过了,正确。
另外,需要注意整数溢出吗?数值范围 -1000 到 1000,两个数相加范围 -2000 到 2000,n最大50,多次累加可能超出 int 范围?最坏情况:每次合并和可能累加,最多合并49次,每次和可能增长,但每个数最大1000,最小-1000。最坏情况:所有正数1000,合并后总和为 50*1000 = 50000,在 int 范围内(约2e9)。所以安全。
现在,修改测试文件内容,然后重新生成。由于头文件和源文件不变,我们可以只修改测试文件,但 generate_all_files 会生成所有三个文件。我们可以只更新测试文件,但使用现有工具需要全部重新生成。我们可以只修改测试文件内容,然后调用 generate_all_files 并设置 force_regenerate=true,覆盖旧文件。
现在,准备新的测试文件内容。我们将更新两个测试用例的预期值,并可能添加更多测试。
另外,我注意到 isNonDecreasing 函数在源文件中是 static 辅助函数,可以。
现在,生成新的测试文件内容,与之前相同,只修改两个预期值。
DecreasingArray 预期改为4,MixedNumbers 预期改为2。
此外,我们可以增加一些测试用例,例如所有元素相同,或长度2的情况。
现在,生成新的测试文件内容:我们将保持大部分相同,只修改那两个值。
为了完整性,我重新编写测试文件内容如下,并添加一些额外测试。
现在,调用 generate_all_files,force_regenerate=true。
思考过程
现在重新编译项目。
思考过程
运行测试。
思考过程
所有测试通过!很好。现在我们已经成功解决了问题。
现在,总结思考过程和解题步骤。
解题报告
题目理解
题目要求通过重复操作使数组变为非递减序列。每次操作需要选择相邻元素中和最小的数对(若有多个相同最小和,选择最左边的),并将该数对替换为它们的和。目标是计算所需的最小操作次数(即直到数组非递减为止的操作次数)。
算法设计
由于数组长度最多为 50,可以采用直接模拟的方法:
关键点
isNonDecreasing检查数组是否已经有序。时间复杂度与空间复杂度
测试用例设计
除了题目给出的两个示例外,还设计了多种边界和特殊情况:
所有测试用例均通过,验证了算法的正确性。
总结
本题是一个简单的模拟问题,关键在于正确理解操作规则并实现每一步的模拟。由于数据规模小,直接模拟是最直观且高效的解法。在实现时注意处理多个相同最小和时选择最左边的数对,以及及时停止操作(当数组已有序时)。
✅ 解题完成!代码已通过编译和测试。
本报告由 AI 自动生成,包含完整的思考过程和解题步骤。