1. 问题背景与核心需求
这道算法题来自某编程竞赛的第487场周赛第二题,题目编号3828。作为一道中等难度的数组操作题,它考察的是对子数组删除操作后剩余元素性质的理解和计算能力。在实际编程面试中,类似需要对数组进行特定操作后分析结果的题目非常常见。
问题的核心可以概括为:给定一个整数数组,我们需要删除其中一个子数组(连续元素序列),然后计算剩余元素按原始顺序拼接后的数组中,第一个和最后一个元素是否相等。最终需要统计所有可能的子数组删除方案中,满足首尾元素相等的方案数量。
注意:这里的"删除子数组"是指从原数组中移除一个连续的片段,剩下的前后两部分会直接拼接在一起形成新数组。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 问题形式化描述与示例分析
2.1 正式题目描述
给定一个长度为n的整数数组nums,我们需要考虑所有可能的子数组删除操作。对于每一种删除方式:
- 选择一个子数组nums[l..r](0 ≤ l ≤ r < n)
- 删除这个子数组,将剩余元素按原顺序拼接
- 得到的新数组如果非空,检查其首元素和尾元素是否相等
- 统计所有满足首尾相等的删除方案数量
2.2 具体示例解析
假设输入数组为[1,2,3,4,5]:
- 删除子数组[2,3](即l=1,r=2):
- 剩余部分拼接为[1,4,5]
- 首元素1 ≠ 尾元素5 → 不计入
- 删除子数组[2,3,4](l=1,r=3):
- 剩余[1,5]
- 1 ≠ 5 → 不计入
- 删除整个数组(l=0,r=4):
- 剩余[] → 忽略
- 删除空子数组(l=r=2):
- 相当于不删除,原数组[1,2,3,4,5]
- 1 ≠ 5 → 不计入
- 删除子数组[1,2](l=0,r=1):
- 剩余[3,4,5]
- 3 ≠ 5 → 不计入
经过全部枚举后,发现这个示例中没有任何删除方案能满足条件,因此最终答案为0。
再看一个满足条件的例子[1,2,3,1]:
- 删除子数组[2,3](l=1,r=2):
- 剩余[1,1]
- 1 == 1 → 计入
- 删除子数组[2](l=1,r=1):
- 剩余[1,3,1]
- 1 == 1 → 计入
这个例子中有2种有效方案。
3. 解题思路与算法分析
3.1 暴力解法及其局限性
最直观的方法是枚举所有可能的子数组,然后对每种情况进行验证:
python复制def countValidSubarrays(nums):
n = len(nums)
count = 0
for l in range(n):
for r in range(l, n):
# 构造剩余数组
remaining = nums[:l] + nums[r+1:]
if len(remaining) > 0 and remaining[0] == remaining[-1]:
count += 1
return count
这种方法的时间复杂度是O(n³):
- 双重循环枚举子数组:O(n²)
- 每次数组拼接操作:O(n)
对于n=1000的情况,这显然会超时。我们需要更高效的算法。
3.2 优化思路的关键观察
通过分析,我们可以发现以下重要性质:
- 剩余数组的首元素只能是原数组的前缀元素(nums[0..l-1]的第一个)
- 剩余数组的尾元素只能是原数组的后缀元素(nums[r+1..n-1]的最后一个)
- 当且仅当nums[l-1] == nums[r+1]时,这种删除方案才有效
因此,问题可以转化为:统计所有满足nums[l-1] == nums[r+1]的(l,r)对数量。
3.3 高效算法设计
基于上述观察,我们可以:
- 预处理每个值出现的位置
- 对于每个可能的x,统计有多少对(l,r)满足nums[l-1] = nums[r+1] = x
- 将这些统计结果累加
具体实现步骤:
- 创建一个字典记录每个值对应的所有索引位置
- 特别处理边界情况(l=0或r=n-1)
- 对于每个值x,计算其所有出现位置i和j,使得i+1 ≤ j-1(即中间有可删除的子数组)
- 统计这些有效对的数量
4. 最优解实现与代码解析
4.1 预处理索引位置
python复制from collections import defaultdict
def countValidSubarrays(nums):
n = len(nums)
pos = defaultdict(list)
for i, num in enumerate(nums):
pos[num].append(i)
这里我们使用defaultdict来记录每个数值在数组中出现的所有位置索引。
4.2 边界情况处理
需要考虑两种特殊情况:
- 删除从开头到某个位置的子数组(l=0)
- 删除从某个位置到末尾的子数组(r=n-1)
对于这两种情况,剩余数组的首或尾元素是固定的:
- l=0时,剩余数组的首元素是nums[r+1](如果r+1 < n)
- r=n-1时,剩余数组的尾元素是nums[l-1](如果l-1 ≥ 0)
4.3 主算法实现
python复制 count = 0
# 情况1:删除非前缀非后缀的子数组
for x in pos:
indices = pos[x]
m = len(indices)
for i in range(m):
for j in range(i+1, m):
l = indices[i] + 1
r = indices[j] - 1
if l <= r:
count += 1
# 情况2:删除前缀子数组(l=0)
for r in range(n):
remaining = nums[r+1:]
if len(remaining) > 0 and remaining[0] == remaining[-1]:
count += 1
# 情况3:删除后缀子数组(r=n-1)
for l in range(n):
remaining = nums[:l]
if len(remaining) > 0 and remaining[0] == remaining[-1]:
count += 1
return count
4.4 进一步优化
上述实现中情况1的时间复杂度在最坏情况下(所有元素相同)仍然是O(n²)。我们可以通过数学公式进一步优化:
对于每个值x,如果有k个出现位置,那么有效(l,r)对数量为C(k,2) = k*(k-1)/2,减去那些i+1 > j-1的对。
优化后的实现:
python复制 count = 0
for x in pos:
indices = pos[x]
k = len(indices)
count += k * (k - 1) // 2
# 减去相邻位置对(无法形成有效子数组)
for i in range(k-1):
if indices[i] + 1 == indices[i+1]:
count -= 1
这样可以将情况1的时间复杂度降到O(n)。
5. 完整代码实现
结合所有优化,最终的Python实现如下:
python复制from collections import defaultdict
def countValidSubarrays(nums):
n = len(nums)
if n == 0:
return 0
pos = defaultdict(list)
for i, num in enumerate(nums):
pos[num].append(i)
count = 0
# 情况1:删除中间子数组,首尾相同
for x in pos:
indices = pos[x]
k = len(indices)
count += k * (k - 1) // 2
for i in range(k-1):
if indices[i] + 1 == indices[i+1]:
count -= 1
# 情况2:删除前缀子数组(l=0)
# 剩余数组的首尾都是nums[r+1](如果r+1 < n)
# 所以需要nums[r+1] == nums[-1]
for r in range(n):
if r+1 < n and nums[r+1] == nums[-1]:
count += 1
# 情况3:删除后缀子数组(r=n-1)
# 剩余数组的首尾都是nums[l-1](如果l-1 >= 0)
# 所以需要nums[0] == nums[l-1]
for l in range(1, n):
if nums[0] == nums[l-1]:
count += 1
# 情况4:不删除任何元素(l=r=i)
if nums[0] == nums[-1]:
count += n
return count
6. 复杂度分析与边界情况
6.1 时间复杂度
- 预处理位置索引:O(n)
- 情况1处理:O(n)(每个元素最多被处理两次)
- 情况2和3处理:O(n)
- 总体时间复杂度:O(n)
6.2 空间复杂度
- 存储位置索引:O(n)
- 其他临时变量:O(1)
- 总体空间复杂度:O(n)
6.3 边界情况测试
需要特别注意以下边界情况:
- 空数组:应返回0
- 单元素数组:只有不删除的方案,是否满足条件取决于nums[0] == nums[0]
- 所有元素相同:所有删除方案都满足条件
- 没有满足条件的方案:如严格递增/递减数组
7. 实际应用与变种问题
7.1 实际应用场景
这类数组操作问题在实际中有多种应用:
- 数据流分析:在连续数据流中寻找满足特定条件的子序列
- 基因组比对:在生物信息学中寻找DNA序列的特定模式
- 日志分析:从系统日志中提取满足前后条件的异常事件序列
7.2 相关问题变种
- 删除子数组后剩余数组的和为特定值
- 删除子数组后剩余数组是回文
- 删除多个不相交子数组后的条件满足
- 允许非连续元素删除的情况
8. 解题心得与经验分享
在解决这类数组操作问题时,我总结了一些实用技巧:
- 从暴力解法开始:先写出最直观的解法,再寻找优化空间
- 寻找不变量:分析操作前后哪些性质保持不变,这往往是优化的关键
- 预处理是王道:提前计算并存储需要频繁访问的信息
- 边界情况先行:先考虑空数组、单元素等边界情况,避免后续调试麻烦
- 可视化帮助理解:在纸上画出数组和操作过程,有助于发现规律
重要提示:在编程竞赛中,遇到数组操作题时,先花2-3分钟在纸上分析示例,往往能发现关键性质,节省大量编码时间。
