1. 东华OJ18~21题目解析与实战攻略
作为一名在算法竞赛领域摸爬滚打多年的老选手,我深知东华OJ平台题目的独特风格。18~21这组题目看似简单,实则暗藏玄机,特别适合用来检验基础算法的掌握程度。今天我就带大家逐题拆解,分享我的解题思路和优化技巧。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 题目18:最大子段和进阶版
2.1 问题重述
给定一个包含n个整数的序列,要求找出连续子序列的最大和。与经典问题不同的是,本题增加了限制条件:子序列长度不能超过k。
2.2 常规解法分析
最直观的暴力解法时间复杂度为O(nk),当n和k较大时(比如n=1e5,k=1e3)就会超时。我最初尝试用前缀和优化:
python复制max_sum = -float('inf')
prefix = [0]*(n+1)
for i in range(1, n+1):
prefix[i] = prefix[i-1] + nums[i-1]
for j in range(max(0, i-k), i):
max_sum = max(max_sum, prefix[i]-prefix[j])
这个解法虽然比纯暴力稍好,但最坏情况下仍是O(nk)复杂度。
2.3 单调队列优化
经过研究,我发现可以用单调队列将复杂度降到O(n)。核心思路是维护一个存储前缀和索引的队列,保证队列中的前缀和单调递增:
python复制from collections import deque
def maxSubarray(nums, k):
q = deque()
prefix = [0]*(len(nums)+1)
res = -float('inf')
for i in range(len(nums)+1):
if i > 0:
prefix[i] = prefix[i-1] + nums[i-1]
while q and i - q[0] > k:
q.popleft()
if q:
res = max(res, prefix[i] - prefix[q[0]])
while q and prefix[i] <= prefix[q[-1]]:
q.pop()
q.append(i)
return res
关键技巧:队列中存储的是前缀和数组的索引而非值,这样既能控制窗口大小,又能快速获取最小前缀和。
3. 题目19:特殊排列计数
3.1 问题描述
给定n个不同的元素,计算满足特定相邻条件的排列数量。具体来说,任意两个相邻元素的差值不能等于某个给定值d。
3.2 动态规划解法
这类排列计数问题通常考虑动态规划。我定义dp[mask][last]表示已使用mask代表的元素,且最后一个元素是last时的合法排列数:
python复制def countPermutations(n, d):
size = 1 << n
dp = [[0]*n for _ in range(size)]
# 初始化
for i in range(n):
dp[1<<i][i] = 1
for mask in range(size):
for last in range(n):
if not (mask & (1<<last)):
continue
for next in range(n):
if mask & (1<<next):
continue
if abs(next - last) == d:
continue
new_mask = mask | (1<<next)
dp[new_mask][next] += dp[mask][last]
return sum(dp[size-1])
这个解法时间复杂度O(n²·2ⁿ),适用于n≤20的情况。
3.3 状态压缩优化
当n较大时,可以考虑使用容斥原理或生成函数等数学方法。不过根据东华OJ的数据范围,上述DP解法已经足够。
4. 题目20:图的最短路径变种
4.1 问题变形
在标准单源最短路径基础上,增加了必须经过特定顶点集的要求。即从s到t的路径必须包含集合S中的所有顶点。
4.2 分层图建模技巧
我采用分层图技术,将原图复制为|S|+1层,每经过一个S中的顶点就上升一层:
python复制def shortestPathWithMandatoryNodes(graph, s, t, S):
k = len(S)
node_to_level = {v:i+1 for i,v in enumerate(S)}
# 构建分层图
layered = defaultdict(dict)
for u in graph:
for v in graph[u]:
weight = graph[u][v]
# 同层边
for layer in range(k+1):
layered[(u,layer)][(v,layer)] = weight
# 跨层边(当v在S中时)
if v in node_to_level:
for layer in range(k):
if layer+1 == node_to_level[v]:
layered[(u,layer)][(v,layer+1)] = weight
# Dijkstra算法
heap = [(0, (s,0))]
dist = defaultdict(lambda: float('inf'))
dist[(s,0)] = 0
while heap:
d, (u, layer) = heapq.heappop(heap)
if u == t and layer == k:
return d
for (v, new_layer) in layered[(u,layer)]:
if d + layered[(u,layer)][(v,new_layer)] < dist[(v,new_layer)]:
dist[(v,new_layer)] = d + layered[(u,layer)][(v,new_layer)]
heapq.heappush(heap, (dist[(v,new_layer)], (v,new_layer)))
return -1
注意事项:实际实现时需要处理顶点编号的映射,这里为清晰起见做了简化。分层图技术是处理带约束路径问题的利器。
5. 题目21:数据结构综合题
5.1 复杂操作需求
题目要求实现一个数据结构,支持以下操作:
- 插入元素
- 删除元素
- 查询第k小元素
- 查询小于x的元素个数
5.2 平衡树方案
最直接的思路是使用内置的SortedList(Python)或TreeSet(Java)。但为了深入理解原理,我手动实现了基于AVL树的结构:
python复制class AVLNode:
def __init__(self, val):
self.val = val
self.left = None
self.right = None
self.height = 1
self.size = 1
class AVLTree:
# 省略旋转等标准AVL操作
def count_less_than(self, x):
def helper(node):
if not node:
return 0
if node.val < x:
return 1 + (node.left.size if node.left else 0) + helper(node.right)
else:
return helper(node.left)
return helper(self.root)
def find_kth(self, k):
node = self.root
while node:
left_size = node.left.size if node.left else 0
if k <= left_size:
node = node.left
elif k == left_size + 1:
return node.val
else:
k -= left_size + 1
node = node.right
return None
5.3 离线处理技巧
当元素范围较小时(如1e6以内),可以考虑使用树状数组或线段树:
python复制class FenwickTree:
def __init__(self, size):
self.N = size + 2
self.tree = [0]*(self.N)
def update(self, index, delta=1):
while index < self.N:
self.tree[index] += delta
index += index & -index
def query(self, index):
res = 0
while index > 0:
res += self.tree[index]
index -= index & -index
return res
def count_less_than(self, x):
return self.query(x-1)
def find_kth(self, k):
index = 0
bit_mask = 1 << (self.N.bit_length())
while bit_mask != 0:
temp = index + bit_mask
if temp < self.N and self.tree[temp] < k:
index = temp
k -= self.tree[temp]
bit_mask >>= 1
return index + 1 if k == 1 else None
6. 调试与优化经验
6.1 对拍测试技巧
在实现复杂算法时,我习惯用暴力算法作为对照。例如对题目18:
python复制def brute_force(nums, k):
max_sum = -float('inf')
for i in range(len(nums)):
current = 0
for j in range(i, min(len(nums), i+k)):
current += nums[j]
max_sum = max(max_sum, current)
return max_sum
用随机数据生成器验证优化算法的正确性:
python复制import random
def test_case():
n = random.randint(1, 100)
k = random.randint(1, n)
nums = [random.randint(-100,100) for _ in range(n)]
return nums, k
for _ in range(1000):
nums, k = test_case()
assert maxSubarray(nums, k) == brute_force(nums, k)
6.2 性能优化要点
- 在题目20的分层图实现中,使用邻接表而非邻接矩阵存储,空间复杂度从O(kV²)降到O(kE)
- 对于C++选手,使用pbds库中的tree容器可以简化题目21的实现
- Python中可以使用bisect模块模拟部分有序集合操作,虽然时间复杂度不如平衡树优秀
7. 常见错误排查
-
单调队列实现中的索引错误:在题目18中,容易混淆原数组索引和前缀和数组索引的关系。建议在纸上画出示例数组和对应的前缀和数组,明确i和j的对应关系。
-
位运算的优先级问题:题目19的状态压缩DP中,条件判断要特别注意运算符优先级。建议多用括号明确优先级,如
if (mask & (1<<last)) == 0比if mask & 1<<last == 0更安全。 -
分层图的层数计算:题目20中容易搞混层数的编号方式。一个实用的调试技巧是打印出各层的转移关系,可视化检查是否正确。
-
平衡树的size维护:在题目21的AVL树实现中,旋转操作后容易忘记更新节点的size属性。建议在旋转函数中加入size的重新计算。
