1. 定义
动态规划(Dynamic Programming),是一种算法设计技术,通常用于解决涉及最优化问题的问题。它通过将复杂问题分解成更小、相互关联的子问题,并存储每个子问题的解,避免了重复计算,从而提高了效率。动态规划通常适用于那些具有“最优子结构”和“重叠子问题”的问题,比如最长公共子序列、背包问题、斐波那契数列等。通过构建一个表格(状态转移方程)来逐步求解最终结果。
2.基本思路
1. 最优子结构:如果一个问题的最优解包含其子问题的最优解,则该问题具有最优子结构。通过找到子问题的最优解,可以构建原问题的最优解。
2. 重叠子问题:在求解过程中,很多子问题被多次计算。通过存储已经计算过的子问题的解,可以避免重复计算,从而提高效率。
3. 状态定义:状态是指问题的一个特定阶段的情况。通过定义状态,可以将问题分解为多个子问题,并通过状态之间的关系来求解原问题。
4. 状态转移方程:状态转移方程描述了如何从一个状态转移到另一个状态。通过状态转移方程,可以逐步求解出所有状态的解,最终得到原问题的解。
5. 初始化和边界条件:确定初始状态的值。确定状态转移的边界条件,防止越界或无效状态。
6. 计算顺序:确定状态的计算顺序,通常是从小到大或从大到小。确保在计算某个状态时,其依赖的所有子问题的解已经计算完毕。
3.操作步骤
1. 定义状态:确定问题的每个阶段可以用哪些变量来表示。在爬楼梯问题中,dp[i]
表示爬到第 i
阶楼梯的方法数。
2. 状态转移方程:确定如何从一个状态转移到另一个状态。在爬楼梯问题中,dp[i] = dp[i-1] + dp[i-2]
,因为可以从第 i-1
阶或第 i-2
阶爬到第 i
阶。
3. 初始化:确定初始状态的值。在爬楼梯问题中,dp[0] = 1
和 dp[1] = 1
,因为从第 0 阶到第 0 阶有 1 种方法(不动),从第 0 阶到第 1 阶也有 1 种方法(爬 1 阶)。
4. 边界条件:确定状态转移的边界条件,防止越界或无效状态。在爬楼梯问题中,i
的范围是从 0 到 n
,确保不会访问负索引或超出数组范围。
5. 计算顺序:确定状态的计算顺序,通常是从小到大或从大到小。在爬楼梯问题中,从 i = 2
开始计算,直到 i = n
。
6. 返回结果:根据问题要求返回最终结果。在爬楼梯问题中,返回 dp[n]
,即爬到第 n
阶的方法数。
4.代码模板(以爬楼梯问题为例)
python模板:
def climbStairs(n):
if n == 0:
return 1
if n == 1:
return 1
# 1. 定义状态
dp = [0] * (n + 1)
# 2. 初始化
dp[0] = 1
dp[1] = 1
# 3. 状态转移
for i in range(2, n + 1):
dp[i] = dp[i - 1] + dp[i - 2]
# 4. 返回结果
return dp[n]
c++模板:
#include <iostream>
#include <vector>
using namespace std;
int climbStairs(int n) {
if (n == 0) return 1;
if (n == 1) return 1;
// 1. 定义状态
vector<int> dp(n + 1, 0);
// 2. 初始化
dp[0] = 1;
dp[1] = 1;
// 3. 状态转移
for (int i = 2; i <= n; ++i) {
dp[i] = dp[i - 1] + dp[i - 2];
}
// 4. 返回结果
return dp[n];
}
int main() {
int n;
cout << "请输入楼梯的阶数: ";
cin >> n;
int result = climbStairs(n);
cout << "爬到第 " << n << " 阶的方法数: " << result << endl;
return 0;
}
5.经典例题
1. [NOIP1999 提高组] 导弹拦截
题目描述
某国为了防御敌国的导弹袭击,发展出一种导弹拦截系统。但是这种导弹拦截系统有一个缺陷:虽然它的第一发炮弹能够到达任意的高度,但是以后每一发炮弹都不能高于前一发的高度。某天,雷达捕捉到敌国的导弹来袭。由于该系统还在试用阶段,所以只有一套系统,因此有可能不能拦截所有的导弹。
输入导弹依次飞来的高度,计算这套系统最多能拦截多少导弹,如果要拦截所有导弹最少要配备多少套这种导弹拦截系统。
输入格式
一行,若干个整数,中间由空格隔开。
输出格式
两行,每行一个整数,第一个数字表示这套系统最多能拦截多少导弹,第二个数字表示如果要拦截所有导弹最少要配备多少套这种导弹拦截系统。
样例输入 #1
389 207 155 300 299 170 158 65
样例输出 #1
6
2
提示
对于前 50% 数据(NOIP 原题数据),满足导弹的个数不超过 10^4 个。该部分数据总分共 100 分。可使用O(n^2) 做法通过。
对于后 50% 的数据,满足导弹的个数不超过 10^5 个。该部分数据总分也为 100 分。请使用 O(n\log n) 做法通过。
对于全部数据,满足导弹的高度为正整数,且不超过 5*10^4。
此外本题开启 spj,每点两问,按问给分。
NOIP1999 提高组 第一题
思路:
-
读取输入数据:
- 使用
while (cin >> x)
读取输入的整数,并将其存储在数组a
中。 a[++n] = x
用于将输入的数存储到数组a
中,并更新数组的长度n
。
- 使用
-
计算最长不下降子序列 (LIS):
- 使用
dp
数组来记录当前的最长不下降子序列。 - 遍历数组
a
,对于每个元素a[i]
:- 如果
a[i]
小于等于dp
数组的最后一个元素,则扩展dp
数组。 - 否则,使用二分查找找到
dp
数组中第一个大于a[i]
的位置,并更新该位置的值。
- 如果
- 使用
-
清空
dp
数组:- 使用
memset(dp, 0, sizeof(dp))
清空dp
数组,准备计算最长不上升子序列 (LDS)。
- 使用
-
计算最长不上升子序列 (LDS):
- 类似于计算 LIS,但条件相反。
- 遍历数组
a
,对于每个元素a[i]
:- 如果
a[i]
大于dp
数组的最后一个元素,则扩展dp
数组。 - 否则,使用二分查找找到
dp
数组中第一个小于等于a[i]
的位置,并更新该位置的值。
- 如果
-
输出结果:
- 输出
ans1
和ans2
,分别表示最长不下降子序列和最长不上升子序列的长度。
- 输出
代码:
#include <bits/stdc++.h>
using namespace std;
int dp[100005], a[100005] = {}, ans1, ans2, n = 0;
int main() {
int x;
// 读取输入数据
while (cin >> x) {
a[++n] = x; // 将输入的数存储到数组 a 中
}
// 计算最长不下降子序列 (LIS)
for (int i = 1; i <= n; i++) {
if (ans1 == 0 || a[i] <= dp[ans1]) {
dp[++ans1] = a[i]; // 如果当前元素小于等于 dp 数组的最后一个元素,则扩展 dp 数组
} else {
int l, r, mid, x = -1;
l = 1, r = ans1;
// 二分查找,找到 dp 数组中第一个大于 a[i] 的位置
while (l <= r) {
mid = (l + r) / 2;
if (dp[mid] < a[i]) {
r = mid - 1;
x = mid;
} else {
l = mid + 1;
}
}
dp[x] = a[i]; // 更新 dp 数组
}
}
// 清空 dp 数组,准备计算最长不上升子序列 (LDS)
memset(dp, 0, sizeof(dp));
// 计算最长不上升子序列 (LDS)
for (int i = 1; i <= n; i++) {
if (ans2 == 0 || a[i] > dp[ans2]) {
dp[++ans2] = a[i]; // 如果当前元素大于 dp 数组的最后一个元素,则扩展 dp 数组
} else {
int l, r, mid, x = -1;
l = 1, r = ans2;
// 二分查找,找到 dp 数组中第一个小于等于 a[i] 的位置
while (l <= r) {
mid = (l + r) / 2;
if (dp[mid] >= a[i]) {
r = mid - 1;
x = mid;
} else {
l = mid + 1;
}
}
dp[x] = a[i]; // 更新 dp 数组
}
}
// 输出结果
cout << ans1 << endl << ans2;
return 0;
}
2. [NOIP2005 普及组] 采药
题目描述
辰辰是个天资聪颖的孩子,他的梦想是成为世界上最伟大的医师。为此,他想拜附近最有威望的医师为师。医师为了判断他的资质,给他出了一个难题。医师把他带到一个到处都是草药的山洞里对他说:“孩子,这个山洞里有一些不同的草药,采每一株都需要一些时间,每一株也有它自身的价值。我会给你一段时间,在这段时间里,你可以采到一些草药。如果你是一个聪明的孩子,你应该可以让采到的草药的总价值最大。”
如果你是辰辰,你能完成这个任务吗?
输入格式
第一行有 2 个整数 T(1 <= T <= 1000)和 M(1 <= M <= 100),用一个空格隔开,T 代表总共能够用来采药的时间,M 代表山洞里的草药的数目。接下来的 M 行每行包括两个在 1 到 100 之间(包括 1 和 100)的整数,分别表示采摘某株草药的时间和这株草药的价值。
输出格式
输出在规定的时间内可以采到的草药的最大总价值。
样例输入 #1
70 3
71 100
69 1
1 2
样例输出 #1
3
提示
- 对于 30% 的数据,M <= 10;
- 对于全部的数据,M <= 100。
NOIP 2005 普及组第三题
思路:
-
引入头文件和命名空间:
#include <bits/stdc++.h>
包含了所有常用的头文件。using namespace std;
使标准库中的名称可以直接使用,无需前缀std::
。
-
定义变量:
f[i][j]
表示前i
个物品,在总时间不超过j
的情况下,可以达到的最大重量。w[i]
表示第i
个物品的重量。t[i]
表示第i
个物品所需的时间。
-
读取输入数据:
m
表示总时间限制。n
表示物品的数量。- 读取每个物品的重量
w[i]
和所需时间t[i]
。
-
动态规划求解:
- 外层循环遍历每个物品
i
。 - 内层循环遍历每个时间
j
。 f[i][j]
初始值为f[i-1][j]
,即不选择当前物品的情况。- 如果当前时间
j
大于等于物品i
的时间t[i]
,则考虑选择当前物品,更新f[i][j]
为选择当前物品和不选择当前物品的最大值。
- 外层循环遍历每个物品
-
输出最终结果:
- 输出
f[n][m]
,即前n
个物品在总时间不超过m
的情况下,可以达到的最大重量。
- 输出
代码:
#include <bits/stdc++.h>
using namespace std;
int f[11100][11100], w[110], t[110];
int main() {
int n, m;
cin >> m >> n;
// 读取每个物品的重量和时间
for (int i = 1; i <= n; i++) {
cin >> w[i] >> t[i];
}
// 动态规划求解
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= m; j++) {
// 继承上一个状态的值
f[i][j] = f[i - 1][j];
// 如果当前时间 j 大于等于物品 i 的时间 t[i]
if (j >= t[i]) {
// 更新 f[i][j],选择当前物品或不选择当前物品的最大值
f[i][j] = max(f[i][j], f[i - 1][j - t[i]] + w[i]);
}
}
}
// 输出最终结果
cout << f[n][m];
return 0;
}