目录
1.P2196 [NOIP 1996 提高组] 挖地雷(洛谷)
1.P2196 [NOIP 1996 提高组] 挖地雷(洛谷)
1.1题目描述
在一个地图上有 N (N≤20) 个地窖,每个地窖中埋有一定数量的地雷。同时,给出地窖之间的连接路径。当地窖及其连接的数据给出之后,某人可以从任一处开始挖地雷,然后可以沿着指出的连接往下挖(仅能选择一条路径),当无连接时挖地雷工作结束。设计一个挖地雷的方案,使某人能挖到最多的地雷。
输入格式
有若干行。
第 1 行只有一个数字,表示地窖的个数 N。
第 2 行有 N 个数,分别表示每个地窖中的地雷个数。
第 3 行至第 N+1 行表示地窖之间的连接情况:
第 3 行有 n−1 个数(0 或 1),表示第一个地窖至第 2 个、第 3 个 … 第 n 个地窖有否路径连接。如第 3 行为 11000⋯0,则表示第 1 个地窖至第 2 个地窖有路径,至第 3 个地窖有路径,至第 4 个地窖、第 5 个 … 第 n 个地窖没有路径。
第 4 行有 n−2 个数,表示第二个地窖至第 3 个、第 4 个 … 第 n 个地窖有否路径连接。
……
第 n+1 行有 1 个数,表示第 n−1 个地窖至第 n 个地窖有否路径连接。(为 0 表示没有路径,为 1 表示有路径)。
输出格式
第一行表示挖得最多地雷时的挖地雷的顺序,各地窖序号间以一个空格分隔,不得有多余的空格。
第二行只有一个数,表示能挖到的最多地雷数。
输入输出样例
输入
5
10 8 4 7 6
1 1 1 0
0 0 0
1 1
1
输出
1 3 4 5
27
1.2核心思路
- 🎉动态规划解决最大地雷数问题🎉
1.2.1🌻状态表示
dp[ i ]表示挖地雷时以点i为终点所能挖到的最大地雷数。
⚠️注意⚠️
根据问题的复杂程度,dp
数组可以是一维、二维甚至更高维度。一般来说,如果问题只与一个变量有关,如时间、位置等,可能可以用一维 dp
数组解决;如果问题与两个变量有关,如物品和容量(在背包问题中),则可能需要二维 dp
数组。
1.2.2🌻状态计算
可以参考 “ 最长 上升/下降 子序列 ” 的思想。
-->本题只考虑两点之间有没有路的问题,不用考虑这两点之间是直接连接还是间接连接的
核心代码:(以最长上升子序列为例)
for (int i = 1; i <= n; i++) { for (int j = 1; j < i; j++) { if (a[j] < a[i]) { dp[i] = max(dp[i] + 1, dp[i]); } } }
状态转移:
//rd数组用于记录两点之间是否有路
//a数组记录每个地窖的地雷数
//pre数组用于记录每个地窖的前驱
for (int i = 2; i <= n; i++) {
for (int j = 1; j < i; j++) {
if (rd[j][i] == 1) {
dp[i] = max(dp[j] + a[i], dp[i]);
pre[i] = j;
}
}
}
- 🎉用DFS解决输出最长路径问题🎉
📍DFS(深度优先搜索)主要用于输出能挖到最大地雷数的路径。📍
DFS:动态规划完成后,我们就得到了最大地雷数以及对应的终点
pos
。此时调用DFS(pos)
函数,利用之前记录的前趋信息,从终点开始回溯,从而输出完整的路径。示例说明:
假设路径是
1 -> 3 -> 5
,调用DFS(5)
时,函数会先递归调用DFS(3)
,接着再递归调用DFS(1)
,之后依次输出1 3 5
,实现了路径的倒序输出。
1.3代码展示
#include<iostream>
#include<vector>
#include<algorithm>
#include<cstring>
#define int long long
using namespace std;
int n, a[25], dp[25], rd[25][25], pre[25];//dp[i]表示以i结尾的路径的能获得的最大地雷数
void dfs(int pos) {
if (pre[pos]) { //如果该点存在前驱就一直递归下去
dfs(pre[pos]);
}
cout << pos << ' ';
}
void cal() {
//初始化
for (int i = 1; i <= n; i++) {
dp[i] = a[i];
}
//状态转移
int ma = -1e9, pos = 0;
for (int i = 2; i <= n; i++) {
for (int j = 1; j < i; j++) {
if (rd[j][i] == 1) {
if (dp[j] + a[i] > dp[i]) {
dp[i] = dp[j] + a[i];
pre[i] = j;
}
}
}
if (dp[i] > ma) {
ma = dp[i];
pos = i;
}
}
//dfs部分
dfs(pos);
cout << endl;
cout << ma << endl;
}
signed main() {
cin >> n;
for (int i = 1; i <= n; i++) {
cin >> a[i];
}
for (int i = 1; i <= n - 1; i++) {
for (int j = i + 1; j <= n; j++) {
cin >> rd[i][j];
}
}
cal();
return 0;
}
😭教训😭
pos
记录错误:原代码每次循环都更新pos
为当前i
,致使pos
最终记录为最后一个位置n
,而非最大地雷数对应的位置。修正后,仅在dp[i]
大于当前ma
时更新pos
为i
。pre
数组更新逻辑有误:原代码直接更新pre[i]
为j
,未考虑dp[i]
是否真的被更新。修正后,添加判断条件,仅当dp[j] + a[i]
大于dp[i]
时,才更新pre[i]
为j
。
2.线性DP相关知识点补充
2.1如何判断一道题目是否为线性DP
1. 问题具有线性结构
- 问题涉及一个线性的序列,如数组、字符串等。例如,给定一个整数数组,要求找出满足某种条件的子数组;或者给定一个字符串,要求找出其中最长的满足特定规则的子串。
2. 状态具有线性递推关系
- 问题的状态可以按照一个线性的顺序进行递推,即当前状态可以由前面的一个或多个状态推导出来。状态之间的依赖关系是线性的,不会出现复杂的树形或图状依赖。比如在斐波那契数列中,第
i
个状态只依赖于第i - 1
个和第i - 2
个状态;在最长上升子序列中,以第i
个元素结尾的状态依赖于前面比它小的元素对应的状态。3. 具有最优子结构和重叠子问题
- 最优子结构:原问题的最优解包含子问题的最优解。例如在最长上升子序列问题中,以第
i
个元素结尾的最长上升子序列,一定包含了前面某个元素结尾的最长上升子序列。- 重叠子问题:在求解过程中,会多次重复计算相同的子问题。通过动态规划记录子问题的解,可以避免重复计算,提高效率。例如在斐波那契数列中,计算
F(5)
时会多次用到F(3)
和F(2)
的值。
2.2如何判断一道题目是线性DP还是区间DP
🌻问题的特征🌻
- 线性 DP:
- 通常涉及到对一个线性序列进行操作,例如数组、字符串等。问题的求解过程是按照一定的顺序依次考虑序列中的每个元素,每个元素的状态只与它前面的元素状态有关,具有明显的线性递推关系。
- 例如,在最长上升子序列问题中,给定一个整数序列,要求找出其中最长的上升子序列的长度。我们从序列的第一个元素开始,依次考虑每个元素,计算以该元素结尾的最长上升子序列的长度,这个长度只与该元素之前的元素所构成的子问题的解有关。
- 区间 DP:
- 主要关注的是区间上的最优解问题,通常需要考虑对一个区间进行划分或合并等操作,问题的求解过程是从小区间逐步扩展到大区间,每个区间的状态与它的子区间状态有关。
- 例如,在石子合并问题中,有一排石子,每次可以将相邻的两堆石子合并成一堆,合并的代价为两堆石子的数量之和。要求计算将所有石子合并成一堆的最小代价。这里我们需要考虑不同区间内石子合并的情况,通过枚举区间的分割点,将大区间的问题转化为小区间的问题来求解。
🌻状态的定义🌻
- 线性 DP:
- 状态通常定义为与序列中某个位置相关的量,例如
dp[i]
表示以第i
个元素为结尾的某种最优值,或者表示处理到第i
个元素时的最优解等。- 比如在背包问题中,
dp[i][j]
可以表示前i
个物品放入容量为j
的背包中所能获得的最大价值,这里的i
就是线性地遍历物品的数量,j
是背包的容量,也是一个线性的维度。- 区间 DP:
- 状态一般定义为与一个区间相关的量,例如
dp[i][j]
表示区间[i, j]
上的某种最优值,如区间的最大和、最小合并代价等。- 以矩阵连乘问题为例,
dp[i][j]
表示计算矩阵链A[i]A[i + 1]...A[j]
的最少乘法次数,通过枚举分割点k
,将区间[i, j]
划分为[i, k]
和[k + 1, j]
两个子区间来计算dp[i][j]
的值。🌻转移方程的特点🌻
- 线性 DP:
- 转移方程通常是根据当前位置的状态和它前面的一个或多个位置的状态来推导的,具有线性的递推关系。例如
dp[i] = max(dp[i - 1], dp[i - 2] + value[i])
,这种形式表示当前位置i
的状态dp[i]
是由前一个位置i - 1
的状态dp[i - 1]
或者前两个位置i - 2
的状态dp[i - 2]
加上当前位置的某个值value[i]
得到的。- 以爬楼梯问题为例,假设你要爬上
n
层楼梯,每次可以爬1
层或2
层,问有多少种不同的方法爬到楼顶。定义dp[i]
表示爬到第i
层楼梯的方法数,则dp[i] = dp[i - 1] + dp[i - 2]
,因为到达第i
层可以从第i - 1
层爬1
层上来,也可以从第i - 2
层爬2
层上来。- 区间 DP:
- 转移方程是通过枚举区间的分割点,将大区间的状态转化为小区间的状态来计算的。例如
dp[i][j] = min(dp[i][k] + dp[k + 1][j] + cost[i][j])
,其中k
是区间[i, j]
内的一个分割点,cost[i][j]
表示合并区间[i, k]
和[k + 1, j]
的代价。🌻遍历顺序🌻
- 线性 DP:
- 一般是按照序列的顺序从左到右(或从小到大)依次计算每个位置的状态,先计算出前面位置的状态,再根据前面的状态推导出后面位置的状态。例如在计算最长公共子序列时,我们从两个字符串的开头开始,依次比较每个位置的字符,根据前面位置的比较结果来计算当前位置的最长公共子序列长度。
- 区间 DP:
- 通常先枚举区间的长度,从小到大遍历所有可能的区间长度,对于每个长度的区间,再枚举区间的起始位置,从而确定每个区间的状态。例如在凸多边形三角剖分问题中,我们先从长度为
3
的区间(即三角形)开始计算,然后逐步扩展到长度更大的区间,通过枚举区间的分割点来计算每个凸多边形的最小三角剖分代价。