第一节 动态规划的基本模型
1258:【例9.2】数字金字塔
这个问题是一个典型的动态规划问题,可以通过自底向上的方法来解决。基本思路是从金字塔的底部开始,逐步向上计算每个点到底部的最大路径和,直到达到顶部。这样,到达每个点的最大路径和就是从它下方两个点的最大路径和中较大的那个加上当前点的值。
下面是一个C++的示例解答,展示了如何实现这个思路:
#include <iostream>
#include <vector>
using namespace std;
int main() {
int R;
cin >> R; // 读入行数
vector<vector<int>> pyramid(R); // 使用vector存储金字塔的每一行
// 读入金字塔数据
for(int i = 0; i < R; ++i) {
pyramid[i].resize(i + 1);
for(int j = 0; j <= i; ++j) {
cin >> pyramid[i][j];
}
}
// 动态规划,自底向上计算每个点到底部的最大路径和
for(int i = R - 2; i >= 0; --i) { // 从倒数第二行开始向上
for(int j = 0; j <= i; ++j) {
// 更新当前点的最大路径和
pyramid[i][j] += max(pyramid[i + 1][j], pyramid[i + 1][j + 1]);
}
}
// 输出顶点的最大路径和,即整个金字塔的最大路径和
cout << pyramid[0][0] << endl;
return 0;
}
这段代码首先读入行数R
,然后读入每一行的数字存储到一个二维vector
中。之后,它使用一个自底向上的动态规划方法来计算到达金字塔每一层每一个点的最大路径和。对于金字塔中的每一个点,我们查找它下面两个点的最大路径和,将其加到当前点的值上。这样,当我们到达金字塔的顶部时,顶点的值就是整个金字塔的最大路径和。
注意:
- 这种方法的时间复杂度是
O(R^2)
,其中R
是金字塔的行数,因为我们需要遍历金字塔中的每一个点。 - 空间复杂度也是
O(R^2)
,因为我们需要存储金字塔中每一行的数据。
1259:【例9.3】求最长不下降序列
这个问题可以通过动态规划的方法来解决,具体的方法是使用一个数组dp
来记录到当前元素为止的最长不下降子序列的长度,另外使用一个数组parent
来记录每个元素在最长不下降子序列中的前一个元素的索引,以便于最后回溯构造出整个序列。
动态规划算法步骤
- 初始化一个长度为
n
的数组dp
,其中dp[i]
表示以b[i]
结尾的最长不下降子序列的长度,初始时每个元素的值为1,因为每个元素本身至少可以构成长度为1的不下降子序列。 - 同时,初始化一个数组
parent
用于记录构造最长不下降子序列的路径,初始时每个元素的值为-1,表示没有前驱。 - 遍历数列,对于每个
b[i]
,再遍历b[j]
(j < i
),如果b[j] <= b[i]
并且dp[j] + 1 > dp[i]
,则更新dp[i] = dp[j] + 1
并且设置parent[i] = j
。 - 找到
dp
数组中的最大值及其索引,这个最大值即为最长不下降子序列的长度,索引用于回溯构造序列。 - 从找到的索引开始,使用
parent
数组回溯构造出最长不下降子序列。
如果您希望在找到最长不下降子序列的长度之后,直接通过回溯打印出一种可能的序列,而不是首先构造序列再打印,您可以通过递归的方式来实现。下面是修改后的代码,其中添加了一个递归函数printSequence
用于直接回溯并打印序列:
C++代码示例:
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;
// 递归函数,用于回溯并打印序列
void printSequence(int currentIndex, const vector<int>& parent, const vector<int>& b) {
if (currentIndex == -1) return; // 递归基,当当前索引为-1时停止回溯
// 先回溯到序列的前一个元素
printSequence(parent[currentIndex], parent, b);
// 打印当前元素
cout << b[currentIndex] << " ";
}
int main() {
int n;
cin >> n; // 读入数列的长度
vector<int> b(n), dp(n, 1), parent(n, -1); // b存储数列,dp存储到当前元素为止的最长不下降子序列长度,parent用于回溯序列
for (int i = 0; i < n; ++i) {
cin >> b[i];
}
int maxLength = 1, maxIndex = 0;
for (int i = 1; i < n; ++i) {
for (int j = 0; j < i; ++j) {
if (b[j] <= b[i] && dp[j] + 1 > dp[i]) {
dp[i] = dp[j] + 1;
parent[i] = j;
if (dp[i] > maxLength) {
maxLength = dp[i];
maxIndex = i;
}
}
}
}
// 输出最长不下降子序列的长度
cout << "max=" << maxLength << endl;
// 使用递归函数直接回溯并打印序列
printSequence(maxIndex, parent, b);
cout << endl;
return 0;
}
在这个修改后的版本中,我添加了一个printSequence
函数,它接受当前元素的索引currentIndex
、parent
数组和原数列b
作为参数。printSequence
函数首先检查是否到达了递归的基(currentIndex == -1
),如果没有,则先递归地回溯到前一个元素,然后打印当前元素。这样,当我们从最长不下降子序列的最后一个元素开始调用printSequence
函数时,它会逐步回溯并打印出整个序列,直到到达序列的开始。这种方法直接在回溯过程中打印序列,避免了先构造序列再打印的需要。
1260:【例9.4】拦截导弹(Noip1999)
基于提供的代码,解决了NOIP1999的“拦截导弹”问题。这个问题被分成两个子问题:
- 最多能拦截的导弹数:这通过计算最长递减子序列(LDS)来实现。
- 要拦截所有导弹最少要配备的系统数:这通过动态构建一系列的递减子序列来实现,每个子序列代表一个导弹拦截系统。
解题思路概述
最长递减子序列 (LDS)
- 首先,初始化一个名为
length
的向量,其大小与输入的导弹高度向量Tall
相同,并将所有元素设为1。这个向量用于存储以每个导弹结尾的最长递减子序列的长度。 - 通过双重循环,对于每个导弹
i
,检查所有在它之前的导弹j
。如果Tall[j]
大于等于Tall[i]
,则尝试更新length[i]
为length[j] + 1
的最大值,以此找到以Tall[i]
结尾的最长递减子序列。 - 最终,
maxn
变量存储了最长递减子序列的长度,即最多能拦截的导弹数。
动态构建递减子序列
- 使用一个名为
dp
的向量来动态地存储当前所有递减子序列的最末元素(每个元素代表一个导弹拦截系统的当前最高可拦截高度)。 - 对于输入的每个导弹高度
num
,使用lower_bound
在dp
中找到第一个大于等于num
的元素的迭代器it
。这个步骤是尝试找到一个已有的系统,其当前最高可拦截高度大于等于当前导弹高度,以此来拦截当前导弹。- 如果这样的系统存在(
it != dp.end()
),则更新该系统的最高可拦截高度为num
,因为num
更低,能够使得这个系统在未来有更大的灵活性。 - 如果不存在这样的系统(即所有现有系统的最高可拦截高度都小于
num
),则需要新增一个系统,以num
为其最高可拦截高度。
- 如果这样的系统存在(
- 最终,
dp
的大小即为要拦截所有导弹最少要配备的系统数。
代码示例:
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;
int main() {
int x;
vector<int> Tall;
vector<int> dp;//用于存储多少个导弹系统
while(cin>>x)
{
Tall.push_back(x);
}
vector<int> length(Tall.size(),1);//最长下降子序列 '
int maxn = 1;//记录走过的最大长度
for(int i=1; i<Tall.size(); i++)
{
for(int j=0; j<i; j++)
{
if(Tall[i] <= Tall[j])
{
length[i] = max(length[i],length[j] + 1);
}
}
maxn = max(maxn,length[i]);
}
for(int num : Tall)//要拦截所有导弹至少要配备的系统数
{
//遍历Tall去dp里面找到第一个大于等于这个高度的替换掉
auto it = lower_bound(dp.begin(),dp.end(),num);
//如果没有比num高的 就重新以他的高度开一个系统
if(it == dp.end()) dp.push_back(num);
else *it = num;//如果有就修改成这个num的高度
}
cout<<maxn<<endl<<dp.size();
return 0;
}
总结
这个解法的关键在于两个动态规划的应用:
- 第一个动态规划解决了找到一个序列中最长递减子序列的问题,即一个系统最多能拦截的导弹数。
- 第二个“动态规划”(更接近于贪心算法)解决了将导弹分配到尽可能少的系统中的问题,通过为每个导弹寻找一个最佳的拦截系统,如果不存在则新建一个系统。
这种方法兼顾了效率和实用性,不仅找出了单个系统的最大拦截能力,也最小化了总体所需的系统数量,以全面拦截所有来袭导弹。