1. 项目背景与学习目标
最近在准备计算机考研复试的过程中,我发现复旦大学的408机试环节特别注重考察算法与数据结构的实际应用能力。作为过来人,我想记录下自己备战复旦机试的第四天学习历程,希望能给同样在准备复试的同学们一些参考。
第四天的学习重点主要集中在动态规划和图论这两个高频考点上。复旦机试的题目往往不会直接考察课本上的基础算法,而是会将这些算法融入实际应用场景中进行考察。因此,在复习时不能只停留在理解算法原理的层面,更要注重算法在实际问题中的应用能力。
2. 动态规划专题精讲
2.1 动态规划核心思想
动态规划是复旦机试中的必考内容,几乎每年都会出现1-2道相关题目。在复习时,我特别注重理解动态规划的三个核心要素:
- 最优子结构:问题的最优解包含子问题的最优解
- 重叠子问题:递归算法会重复计算相同的子问题
- 状态转移方程:定义如何从一个状态转移到另一个状态
以经典的背包问题为例,我重新推导了状态转移方程:
dp[i][j] = max(dp[i-1][j], dp[i-1][j-w[i]] + v[i])其中dp[i][j]表示前i个物品放入容量为j的背包的最大价值。
2.2 动态规划解题模板
通过分析历年真题,我总结出一个适用于大多数动态规划题目的解题模板:
- 定义dp数组的含义
- 确定初始条件
- 推导状态转移方程
- 确定遍历顺序
- 举例验证dp数组
在练习时,我特别注重第5步的验证过程。很多同学在考试时容易忽略这一步,导致写出的代码虽然看起来正确,但实际上存在逻辑错误。
2.3 动态规划优化技巧
复旦机试对算法的时间复杂度要求很高,因此必须掌握动态规划的空间优化技巧。常见的优化方法包括:
- 滚动数组:将二维dp数组优化为一维
- 状态压缩:使用位运算等技巧减少状态表示
- 单调队列优化:适用于特定类型的状态转移方程
我重点练习了将二维dp数组优化为一维的技巧。以背包问题为例,优化后的状态转移方程为:
dp[j] = max(dp[j], dp[j-w[i]] + v[i])需要注意的是,这种情况下遍历顺序需要从后往前,以避免重复计算。
3. 图论算法实战
3.1 图的表示方法
复旦机试中的图论题目通常不会给出图的显式表示,而是需要考生根据题目描述自行构建图模型。常见的图的表示方法包括:
- 邻接矩阵:适合稠密图
- 邻接表:适合稀疏图
- 边列表:适合某些特定算法
我练习了使用vector实现的邻接表表示法:
vector<vector<int>> adj(n); for(int i=0; i<m; i++){ int u, v; cin >> u >> v; adj[u].push_back(v); adj[v].push_back(u); // 无向图需要双向添加 }3.2 最短路径算法
Dijkstra算法是复旦机试中的高频考点。在复习时,我特别注意以下几点:
- 优先队列的实现方式
- 如何处理负权边(不能使用Dijkstra)
- 路径记录的实现方法
我实现了一个带路径记录的Dijkstra算法:
vector<int> dist(n, INF); vector<int> pre(n, -1); priority_queue<pair<int,int>> pq; dist[start] = 0; pq.push({0, start}); while(!pq.empty()){ auto [d, u] = pq.top(); pq.pop(); if(-d > dist[u]) continue; for(auto [v, w] : adj[u]){ if(dist[v] > dist[u] + w){ dist[v] = dist[u] + w; pre[v] = u; pq.push({-dist[v], v}); } } }3.3 最小生成树算法
Kruskal和Prim算法都需要熟练掌握。我重点练习了Kruskal算法的实现,特别是并查集的使用:
vector<int> parent(n); iota(parent.begin(), parent.end(), 0); function<int(int)> find = [&](int x){ return parent[x] == x ? x : parent[x] = find(parent[x]); }; sort(edges.begin(), edges.end()); int res = 0; for(auto [w, u, v] : edges){ u = find(u); v = find(v); if(u != v){ res += w; parent[u] = v; } }4. 真题实战演练
4.1 动态规划真题解析
我选择了一道复旦往年的动态规划真题进行练习:
题目描述:给定一个正整数数组,找出其中不相邻元素组成的子序列的最大和。
这道题是典型的动态规划问题。我按照之前总结的解题模板:
- 定义dp[i]为前i个元素中不相邻子序列的最大和
- 初始条件:dp[0]=nums[0], dp[1]=max(nums[0],nums[1])
- 状态转移方程:dp[i] = max(dp[i-1], dp[i-2]+nums[i])
- 最终结果为dp[n-1]
实现代码如下:
int rob(vector<int>& nums) { int n = nums.size(); if(n == 1) return nums[0]; vector<int> dp(n); dp[0] = nums[0]; dp[1] = max(nums[0], nums[1]); for(int i=2; i<n; i++){ dp[i] = max(dp[i-1], dp[i-2]+nums[i]); } return dp[n-1]; }4.2 图论真题解析
另一道图论真题是:
题目描述:给定一个n个节点的有向图,判断是否存在从节点0到节点n-1的路径。
这道题可以使用BFS或DFS解决。我选择用BFS实现:
bool canReach(vector<vector<int>>& graph) { int n = graph.size(); queue<int> q; vector<bool> visited(n, false); q.push(0); visited[0] = true; while(!q.empty()){ int u = q.front(); q.pop(); if(u == n-1) return true; for(int v : graph[u]){ if(!visited[v]){ visited[v] = true; q.push(v); } } } return false; }5. 常见错误与调试技巧
5.1 动态规划常见错误
在练习过程中,我总结了几种常见的动态规划错误:
- 初始条件设置不当:特别是边界情况的处理
- 状态转移方程错误:没有考虑所有可能的情况
- 遍历顺序错误:特别是空间优化后的遍历顺序
- 数组越界:没有正确处理索引范围
调试技巧:
- 打印dp数组的中间结果
- 使用小规模测试用例手动验证
- 特别注意边界条件(n=0,1等)
5.2 图论常见错误
图论算法中容易出现的错误包括:
- 图的表示错误:有向图/无向图混淆
- 访问标记遗漏:导致无限循环
- 优先队列的比较函数错误
- 并查集的路径压缩或按秩合并实现错误
调试技巧:
- 可视化小规模图的遍历过程
- 检查每个节点的邻接表是否正确
- 使用断言验证不变量
6. 学习心得与时间规划
经过第四天的学习,我对动态规划和图论的理解更加深入了。最大的收获是建立了系统的解题思路,而不是单纯地记忆算法模板。
在时间规划方面,我采用"专题突破+真题演练"的模式:
- 上午:专题知识梳理与模板代码实现
- 下午:真题练习与错题分析
- 晚上:复习巩固与知识拓展
对于准备复旦机试的同学,我的建议是:
- 重视基础算法的深入理解
- 多做真题,熟悉出题风格
- 注重代码实现的细节和效率
- 建立错题本,定期复习易错点