1. 题目背景与问题解析
1996年全国青少年信息学奥林匹克竞赛(NOIP)提高组的"挖地雷"题目,是一道经典的动态规划问题。题目描述了一个由n个地窖组成的地窖系统,每个地窖中埋有一定数量的地雷。地窖之间存在单向连接通道,玩家需要从任意一个地窖出发,沿着连接通道挖取地雷,目标是获得最大数量的地雷。
这个问题本质上是一个有向无环图(DAG)上的最长路径问题,每个地窖代表图中的一个节点,地窖间的通道代表有向边,地雷数量就是节点的权值。我们需要找到一条路径,使得路径上所有节点的权值之和最大。
2. 算法思路与动态规划解法
2.1 动态规划状态定义
对于这类DAG上的最优路径问题,动态规划是最合适的解法。我们定义dp[i]表示以第i个地窖为终点的路径能够获得的最大地雷数量。同时,为了记录路径,我们需要维护一个pre数组,记录每个地窖的前驱节点。
状态转移方程为:
dp[i] = max(dp[j] + w[i]),其中j是所有能够直接到达i的地窖,w[i]是第i个地窖的地雷数量。
2.2 算法实现步骤
- 初始化:dp数组初始值为每个地窖自身的地雷数量,pre数组初始值为-1
- 拓扑排序:由于是DAG,我们需要按照拓扑序处理每个节点
- 状态转移:对于每个节点i,遍历所有能够到达i的节点j,更新dp[i]和pre[i]
- 结果提取:遍历dp数组找到最大值,然后通过pre数组回溯得到路径
3. 代码实现与关键细节
3.1 数据结构设计
我们需要用邻接表来存储图结构。对于n个地窖,可以用vector<vector
cpp复制const int MAXN = 100;
vector<int> G[MAXN]; // 邻接表
int w[MAXN]; // 每个地窖的地雷数
int dp[MAXN]; // DP数组
int pre[MAXN]; // 前驱数组
3.2 核心算法实现
cpp复制void solve() {
int n; // 地窖数量
cin >> n;
// 输入地雷数
for(int i = 1; i <= n; i++) {
cin >> w[i];
dp[i] = w[i];
pre[i] = -1;
}
// 建立图
for(int i = 1; i < n; i++) {
for(int j = i+1; j <= n; j++) {
int connected;
cin >> connected;
if(connected) {
G[i].push_back(j);
}
}
}
// DP过程
for(int u = 1; u <= n; u++) {
for(int v : G[u]) {
if(dp[u] + w[v] > dp[v]) {
dp[v] = dp[u] + w[v];
pre[v] = u;
}
}
}
// 找出最大值和终点
int max_val = 0, end = 0;
for(int i = 1; i <= n; i++) {
if(dp[i] > max_val) {
max_val = dp[i];
end = i;
}
}
// 回溯路径
vector<int> path;
while(end != -1) {
path.push_back(end);
end = pre[end];
}
reverse(path.begin(), path.end());
// 输出结果
for(int i = 0; i < path.size(); i++) {
if(i) cout << " ";
cout << path[i];
}
cout << endl << max_val << endl;
}
4. 算法优化与注意事项
4.1 时间复杂度分析
这个算法的时间复杂度主要由两部分组成:
- 建图:O(n^2)
- DP过程:O(n+m),其中m是边数
总体复杂度是O(n^2),对于n=100的数据规模完全足够。
4.2 常见错误与调试技巧
- 图的存储方式:注意题目中地窖编号是从1开始的,不是0
- 路径回溯:需要反向输出路径,所以要用栈或者reverse
- 初始化:dp数组要初始化为每个地窖自身的地雷数
- 边界条件:当所有地窖都不连通时,最大值就是单个地窖的最大值
提示:在竞赛中,建议先手动计算小样例,确保算法正确性再编码。
5. 变种与扩展思考
5.1 问题变种
-
如果地窖连接是双向的,如何处理?
- 这会形成可能有环的图,不能直接用DP,需要先用强连通分量缩点
-
如果要求输出所有可能的最大路径?
- 需要修改pre数组为vector,记录所有可能的前驱
-
如果地窖数量非常大(n=1e5)?
- 需要更高效的拓扑排序和DP实现
5.2 实际应用场景
这类算法在实际中有很多应用,比如:
- 项目管理中的关键路径分析
- 游戏中的最优路径规划
- 交通网络中的最长路径问题
6. 竞赛技巧与经验分享
在竞赛中处理这类题目时,我有几点经验:
- 先确保理解题意,画出样例的图结构
- 设计好数据结构再开始编码
- 对于DP问题,明确状态定义和转移方程
- 注意输出格式要求,特别是路径的顺序
- 使用更直观的变量名,如用max_val代替ans
在实际编码中,我发现使用邻接表比邻接矩阵更节省空间,特别是对于稀疏图。另外,在回溯路径时使用vector和reverse比用栈更不容易出错。