由于我之前写过一篇关于dp算法的文章了,所以,这里就不多阐释关于dp的内容了,详情请见这篇文章:【C++竞赛】note:动态规划(DP)
接下来让我们看几道题,来让我们更好感受一下dp的使用。
(版权说明:以下所有题目均来自于洛谷)
例题一、模版题:B3637 最长上升子序列
题目如下:
B3637 最长上升子序列
题目描述
这是一个简单的动规板子题。
给出一个由 n ( n ≤ 5000 ) n(n\le 5000) n(n≤5000) 个不超过 10 6 10^6 106 的正整数组成的序列。请输出这个序列的最长上升子序列的长度。
最长上升子序列是指,从原序列中按顺序尽可能多 取出一些数字排在一起,这些数字是逐渐增大的。
输入格式
第一行,一个整数 n n n,表示序列长度。
第二行有 n n n 个整数,表示这个序列。
输出格式
一个整数表示答案。
输入输出样例 #1
输入 #1
6
1 2 4 1 3 4
输出 #1
4
说明/提示
分别取出 1 1 1、 2 2 2、 3 3 3、 4 4 4 即可。
题目分析
如果一个上升子序列的最大值小于一个在它后面的数,那么这个数和这个子序列可以拼成一个更长的上升子序列。
意思是:给你一串数字,要你按顺序挑出尽量多的数字,而且挑出来的数字必须一个比一个大(严格递增)。
接下来,我们分析一下样例一:
在样例一中的数据 1 2 4 1 3 4,可以挑 1,2,3,4(位置是 1,2,5,6),长度是 4。也可以挑 1,2,4,但只有 3,所以最长是 4。
而我们要做的是这样一件事:
不管整个序列怎么组成,我只管'以当前这个数字作为结尾最长能有多长。要想知道它,我就扫描它前面所有比它小的数字,看谁结尾的长度最大,我就在谁后面接上自己。
所以,接下来,我们要一个a数组用来存数据,dp数组用来进行动态规划的部分。
- 每个元素本身可以单独构成一个长度为 1 的子序列,所以
dp[i] = 1。 - 对于每个 i i i枚举所有 j < i j<i j<i
- 如果 a j < a i a_j<a_i aj<ai那么就可以把
a[i]接在a[j]结尾的最长上升子序列后面,形成一个新的更长的子序列。 - 因此状态转移方程为:
d p i = max ( 1 , max j < i a j < a i ( d p j + 1 ) ) dpi = \max\left(1, \max_{\substack{j < i \\ aj < ai}} (dpj + 1)\right) dpi=max(1,maxj<iaj<ai(dpj+1))
到此,题目分析结束,可以开始写代码了。
AC代码
cpp
#include <bits/stdc++.h>
using namespace std;
int n;
int a[5005]; // 原始序列
int dp[5005]; // dp[i] 表示以 i 结尾的最长上升子序列长度
int main() {
cin >> n;
for (int i = 1; i <= n; i++) {
cin >> a[i];
}
int ans = 0;
for (int i = 1; i <= n; i++) {
dp[i] = 1; // 以自身结尾,长度至少为 1
for (int j = 1; j < i; j++) {
if (a[j] < a[i]) { // 满足上升条件
dp[i] = max(dp[i], dp[j] + 1);
}
}
ans = max(ans, dp[i]); // 更新全局最优解
}
cout << ans;
return 0;
}
接下来,我们就要学会如何使用这个模板了,
下面一道题是dp的应用:
例题二、B3637 合唱队形
P1091 NOIP 2004 提高组 合唱队形
题目描述
n n n 位同学站成一排,音乐老师要请其中的 n − k n-k n−k 位同学出列,使得剩下的 k k k 位同学排成合唱队形。
合唱队形是指这样的一种队形:设 k k k 位同学从左到右依次编号为 1 , 2 , 1,2, 1,2, ... , k ,k ,k,他们的身高分别为 t 1 , t 2 , t_1,t_2, t1,t2, ... , t k ,t_k ,tk,则他们的身高满足 t 1 < ⋯ < t i > t i + 1 > t_1< \cdots <t_i>t_{i+1}> t1<⋯<ti>ti+1> ... > t k ( 1 ≤ i ≤ k ) >t_k(1\le i\le k) >tk(1≤i≤k)。
你的任务是,已知所有 n n n 位同学的身高,计算最少需要几位同学出列,可以使得剩下的同学排成合唱队形。
输入格式
共二行。
第一行是一个整数 n n n( 2 ≤ n ≤ 100 2\le n\le100 2≤n≤100),表示同学的总数。
第二行有 n n n 个整数,用空格分隔,第 i i i 个整数 t i t_i ti( 130 ≤ t i ≤ 230 130\le t_i\le230 130≤ti≤230)是第 i i i 位同学的身高(厘米)。
输出格式
一个整数,最少需要几位同学出列。
输入输出样例 #1
输入 #1
8
186 186 150 200 160 130 197 220
输出 #1
4
说明/提示
对于 50 % 50\% 50% 的数据,保证有 n ≤ 20 n \le 20 n≤20。
对于全部的数据,保证有 n ≤ 100 n \le 100 n≤100。
题目分析
由题得"他们的身高满足 t 1 < ⋯ < t i > t i + 1 > t_1< \cdots <t_i>t_{i+1}> t1<⋯<ti>ti+1> ... > t k ( 1 ≤ i ≤ k ) >t_k(1\le i\le k) >tk(1≤i≤k)",那意思是这个序列是先上升,后下降的。那我们很容易就能看出我们需要正着算一遍序列,再反着算一遍序列,再给他们拼在一起,减去中间那个计算重复的,那就是答案。
思路理清后,我们就可以开始写代码了:
AC代码
cpp
#include <bits/stdc++.h>
using namespace std;
int n;
int a[5005];
int dp1[5005];
int dp2[5005];
int main()
{
cin >> n;
for (int i = 1; i <= n; i++)
{
cin >> a[i];
}
// 上升
for (int i = 1; i <= n; i++)
{
dp1[i] = 1;
for (int j = 1; j < i; j++)
{
if (a[j] < a[i])
{
dp1[i] = max(dp1[i], dp1[j] + 1);
}
}
}
// 下降
for (int i = n; i >= 1; i--)
{
dp2[i] = 1;
for (int j = n; j > i; j--)
{
if (a[j] < a[i])
{
dp2[i] = max(dp2[i], dp2[j] + 1);
}
}
}
int ans = 0;
for (int i = 1; i <= n; i++)
{
ans = max(ans, dp1[i] + dp2[i] - 1);
}
cout << n - ans << endl;
}