1. 牛客周赛Round137参赛体验回顾
上周六下午2点,我准时打开了牛客竞赛页面,准备参加周赛Round137。作为算法竞赛爱好者,这已经是我连续第9周参加牛客周赛。与往常不同的是,这次比赛采用了全新的题目编排方式——四道编程题全部聚焦动态规划专题,这在周赛历史上尚属首次。
比赛开始后,我快速浏览了题目列表:
- A题:基础DP应用(斐波那契数列变种)
- B题:二维状态转移(矩阵路径问题)
- C题:状态压缩DP(位运算优化)
- D题:树形DP(后序遍历应用)
这种专题化的题目设置让我既惊喜又紧张。惊喜的是可以集中检验某个算法领域的掌握程度,紧张的是如果恰好遇到知识盲区就会全面崩盘。实际比赛中,我在C题卡了整整40分钟,因为对状态压缩中位运算的优化技巧不够熟练。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 题目详解与解题思路复盘
2.1 A题:爬楼梯的代价(斐波那契变种)
题目描述一个特殊楼梯,每次可以跨1/2/3步,但第k步有额外代价c[k]。给定n级台阶和代价数组,求到楼顶的最小总代价。
标准解法是线性DP:
python复制def minCost(n, c):
dp = [float('inf')] * (n + 3)
dp[0] = 0
for i in range(n):
for step in [1, 2, 3]:
if i + step <= n:
dp[i + step] = min(dp[i + step], dp[i] + c[i])
return dp[n]
关键点在于:
- 初始化dp[0]=0(地面无代价)
- 代价c[i]是在离开第i步时支付
- 需要处理n<3的边界情况
实际比赛中,有选手因为没理解代价支付时机(以为是到达时支付)导致WA。这是典型的题意理解偏差。
2.2 B题:网格中的最大礼物(二维路径DP)
在m×n的网格中,每个格子有礼物价值v[i][j],从左上到右下,每次只能向右或向下移动,求最大收获价值。
经典二维DP问题:
python复制def maxGift(grid):
m, n = len(grid), len(grid[0])
dp = [[0]*n for _ in range(m)]
dp[0][0] = grid[0][0]
for i in range(m):
for j in range(n):
if i == 0 and j == 0: continue
up = dp[i-1][j] if i > 0 else -1
left = dp[i][j-1] if j > 0 else -1
dp[i][j] = max(up, left) + grid[i][j]
return dp[-1][-1]
进阶思考:
- 如果允许从四个方向移动(可能成环)怎么办?
- 如果礼物价值可能为负怎么处理?
- 空间优化到O(n)的方法?
2.3 C题:课程安排(状态压缩DP)
有n门课,某些课有先修要求。给定先修关系,问有多少种选课顺序满足所有先修条件。
这是典型的拓扑排序计数问题,但n≤20提示用状态压缩:
python复制def countOrders(n, prerequisites):
from collections import defaultdict
pre = defaultdict(int)
for u, v in prerequisites:
pre[v] |= 1 << u
dp = [0] * (1 << n)
dp[0] = 1
for mask in range(1 << n):
if not dp[mask]: continue
for u in range(n):
if not (mask & (1 << u)) and (mask & pre[u]) == pre[u]:
dp[mask | (1 << u)] += dp[mask]
return dp[(1 << n) - 1]
位运算技巧:
- pre[v]的二进制位表示v的所有先修课
- mask & pre[u] == pre[u] 检查是否满足先修条件
- 时间复杂度O(n*2^n) 在n=20时约为2千万次运算
2.4 D题:公司派对(树形DP)
公司层级结构是二叉树,每个员工有快乐值。选择参会员工集合满足:如果某员工参会,则其直接下属不能参会。求最大快乐值。
树形DP模板题:
python复制def maxHappiness(root):
def dfs(node):
if not node: return (0, 0)
l_choose, l_not = dfs(node.left)
r_choose, r_not = dfs(node.right)
choose = node.val + l_not + r_not
not_choose = max(l_choose, l_not) + max(r_choose, r_not)
return (choose, not_choose)
return max(dfs(root))
后序遍历特性:
- 需要先处理子节点才能决定父节点状态
- 返回二元组(选择当前节点的最大值,不选择的最大值)
- 空间复杂度O(h)递归栈空间
3. 比赛策略与时间管理分析
3.1 开题顺序选择
我采用的策略是:A(15min)→B(20min)→D(30min)→C(剩余时间)。这个选择基于:
- A题明显最简单(基础DP)
- B题是经典模型(见过类似题目)
- D题虽然没做过树形DP,但二叉树结构更直观
- C题状态压缩最不熟悉(留到最后)
对比其他选手的提交时间戳,前50名选手的共同特点是:
- 都在前30分钟解决A、B两题
- 对C、D的选择各占50%
- 最终排名差异主要取决于C题的解决速度
3.2 调试技巧分享
在B题调试时遇到一个典型错误:
python复制# 错误写法
dp = [[0]*n]*m # 这样创建的二维数组行是引用复制!
正确初始化方式应该:
python复制dp = [[0 for _ in range(n)] for _ in range(m)]
这个错误导致所有行的值同步变化,浪费了15分钟调试时间。类似的Python陷阱还有:
- 默认参数的可变对象问题
- 浅拷贝与深拷贝
- 循环中闭包变量捕获
4. 动态规划专题学习建议
4.1 知识体系构建
根据本次比赛内容,完整的DP知识体系应包括:
- 线性DP
- 斐波那契变种
- LIS/LCS问题
- 背包问题
- 二维DP
- 矩阵路径问题
- 字符串编辑距离
- 状态压缩DP
- 位运算技巧
- 子集枚举
- 树形DP
- 后序遍历框架
- 状态设计(选/不选)
4.2 推荐练习题库
针对性训练建议:
- LeetCode:
-
- Climbing Stairs(A题基础)
-
- Minimum Path Sum(B题变种)
-
- Number of Ways to Wear Different Hats(C题类似)
-
- House Robber III(D题简化版)
-
- 牛客专项:
- NC19 子数组最大乘积
- NC35 编辑距离
- NC61 最长上升子序列
4.3 常见错误模式
根据比赛提交数据,高频错误包括:
- 初始化错误(忘记处理边界)
- 状态转移条件遗漏(如B题只考虑向右移动)
- 空间优化时的遍历顺序错误
- 树形DP忘记处理空节点
- 位运算优先级混淆(C题常见)
5. 参赛收获与后续计划
这次专题赛让我清晰认识到自己在状态压缩DP方面的薄弱。赛后我做了两件事:
- 重新学习了《算法竞赛入门经典》中位运算章节
- 用Python重写了10道经典状压DP题目
接下来的训练重点:
- 每周至少3道状压DP题目
- 建立错题本记录位运算技巧
- 参加AtCoder常规比赛(其DP题目质量较高)
特别提醒想提高DP能力的同学:不要满足于AC,要尝试多种解法。比如B题还可以:
- 改用DFS+记忆化
- 使用滚动数组优化空间
- 输出具体路径而不仅是最大值
