前言
- 本题是基础DP,思路较为简单。
- HRBUST 1835对时间复杂度要求不高,用这题作为DP的入门题非常合适,可以从最简单的 O ( N 2 ) O(N^2) O(N2)的时间复杂度的DP开始考虑,再慢慢优化至 O ( N log N ) O(N\log N) O(NlogN)的DP。循序渐进的方式有助于对DP的理解
- 本篇解析不是特别清晰,DP还是需要多做题和多思考才会有自己一套清晰的理解。DP有点“只可意会不可言传”的意思。
- 欢迎交流
题目
给出一个数字序列求其最长的递增子序列例如序列(1,7,3,5,9,4,8).
(1,7)和(3,4,8)是其递增子序列但其最长的递增子序列是(1,3,5,8)。
Input
本题有多组测试数据,对于每组测试数据第一行是一个整数n(n<=100)代表序列长度。
第二行是n个整数。
Output
最长递增子序列长度
Sample Input
7
1 7 3 5 9 4 8
Sample Output
4
算法思路
- DP的基本思路是:递推+记忆化。DP数组本身已经实现了记忆化存储,关键是递推。而递推务必要考虑初始条件。因此DP的思路在于:递推+初始条件
- 递推要找出DP数组的特定含义,以满足最优子结构(最优化原理):整体最优,局部必定最优。只有满足这个条件,才能从局部最优推出整体最优。
- 什么是从局部最优推出整体最优?这里我们定义DP数组为: d p [ M ] dp[M] dp[M]=以数字序列中 S M S_M SM为结尾的最长(严格)递增子序列的长度。
- 假定以 S M S_M SM结尾的最长(严格)递增子序列是 S i 1 , S i 2 , . . . , S M − 1 , S M S_{i1}, S_{i2}, ..., S_{M-1}, S_{M} Si1,Si2,...,SM−1,SM,则该序列中 S i 1 , S i 2 , . . . , S i t S_{i1}, S_{i2}, ...,S_{it} Si1,Si2,...,Sit一定是以 S i t S_{it} Sit结尾的最长(严格)递增子序列,否则可以找到一个更长的以 S i t S_{it} Sit结尾的(严格)递增子序列,且可以把 S i ( t + 1 ) , . . . , S M S_{i(t+1)}, ..., S_{M} Si(t+1),...,SM接在 S i t S_{it} Sit后面,得到更长的以 S M S_M SM结尾的(严格)递增子序列。这就和原本的以 S M S_M SM结尾的最长(严格)递增子序列是 S i 1 , S i 2 , . . . , S M − 1 , S M S_{i1}, S_{i2}, ..., S_{M-1}, S_{M} Si1,Si2,...,SM−1,SM矛盾。用反证法证明这样定义的dp数组满足最优子结构
- 若满足最优子结构,则以 S M S_M SM结尾的最长(严格)递增子序列就可以以递推的形式来构造。
- 递推:考察以 S M S_M SM结尾的最长(严格)递增子序列,它一定是由以 S M − 1 S_{M-1} SM−1结尾的最长(严格)递增子序列+ S M S_M SM构造而成。而 S M − 1 S_{M-1} SM−1可以是在 S M S_M SM前面的任意一个比 S M S_M SM小的数。要想得到以 S M S_M SM结尾的最长(严格)递增子序列,只需要在 S M S_M SM前面的所有比它小的数中,找到 d p [ i ] dp[i] dp[i]最大的 S i S_i Si。
d p [ M ] dp[M] dp[M]=以数字序列中 S M S_M SM为结尾的最长(严格)递增子序列的长度。
- 初始条件:显然,数字序列中的每个数在刚开始都不知道以它为结尾的最长递增子序列的长度,也就是不知道它前面有几个数,但它唯一知道的是最长递增子序列的长度至少为1,即只有它自己的情况。
- 没有最优子结构能否使用递推?显然不能。因为递推是在dp数组上递推,dp数组的含义常常是题目所需的最优值,而最优子结构性质保证了全体最优则局部最优,意味着 d p [ i ] dp[i] dp[i]和 d p [ j ] , ( j < i ) dp[j], (j<i) dp[j],(j<i)的含义可以统一。若含义不统一,则无法在dp数组上递推。
- 以本题为例子,若以 S M S_M SM结尾的最长严格递增子序列 S i 1 , . . . S M − 1 , S M S_{i1}, ... S_{M-1}, S_M Si1,...SM−1,SM中,以 S M − 1 S_{M-1} SM−1结尾的最长递增子序列不是 S i 1 , . . . S M − 1 S_{i1}, ... S_{M-1} Si1,...SM−1,那么显然 d p [ M ] ! = d p [ M − 1 ] + 1 dp[M] != dp[M-1]+1 dp[M]!=dp[M−1]+1,就无法实现递推了。
- 故本题的解题过程是
- 找到符合最优子结构性质的dp数组
- 递推dp数组
- 初始条件
实现
#include <cstdio>
#include <iostream>
#include <vector>
#include <algorithm>
#define MAX_SIZE 1000
using namespace std;
unsigned N=0, Max=0;
//N: 数据数 Max: dp数组中的最大值
int main(int argc, char const *argv[])
{
unsigned num[MAX_SIZE];//输入数字串
unsigned dp[MAX_SIZE];
//dp数组,含义:dp[i]=数组中以第i个数为结尾的最长递增子序列的长度
//每个数为结尾的递增子序列长度最少是1,即只有自己的序列
// ios::sync_with_stdio(false);
while(~scanf("%u", &N))
// while(cin >> N)
{
Max=0;
for(unsigned i=1; i<=N; ++i)
{
scanf("%u", &num[i]);
// cin >> num[i];
dp[i]=1;
}
dp[0]=0;//第0个数不存在,故子序列长度为0
for(unsigned i=1; i<=N; ++i)//当前考察num[i]
{
for(unsigned j=1; j<=i-1; ++j)//num[j] 遍历num[i]前面的所有数
{
if(num[j] < num[i])//num[i]接在num[j]后面成为一个递增子序列的结尾的必要条件是:num[j] < num[i]
dp[i]=max(dp[i], dp[j]+1);
}
Max= Max>dp[i] ?Max :dp[i];//更新dp数组中的最大值
}
printf("%d\n", Max);
// cout << Max << endl;
}
return 0;
}
关键要点
- 注意处理本题有多组输入输出
- 处理多组输入输出时的两种方法
while (~scanf(...))
:scanf函数返回成功读入的数据项数,读入数据时遇到了“文件结束”则返回EOF。EOF在ASCII中是255,即1111 1111。当且仅当scanf返回值是EOF时,取反后得到全0,跳出while循环。while (cin >> N)
- 输入输出提速
- 使用scanf和printf进行输入输出
- 使用cin和cout,在所有cin和cout出现之前写
ios::sync_with_stdio(false);
。该语句不需要头文件,只需要使用using namspace std;
即可。使用该语句时,不可以使用 scanf和printf