题目就不多说了,链接–> POJ-3903-最长上升子序列
解法一
这个题目用动态规划来做是没问题的,大家一般都会想到这种解法:
D
P
[
i
]
:
表
示
下
标
从
0
到
i
的
序
列
的
最
长
上
升
子
序
列
长
度
DP[i]:表示下标从0到i的序列的最长上升子序列长度
DP[i]:表示下标从0到i的序列的最长上升子序列长度
然后动态转移方程如下:
记第i位前的所有位置为j(到时候要遍历的),如果sequence[i]>sequence[j],则dp[i]=dp[j]+1,然后对所有j都考虑一下取最大的那个。
代码差不多会写成这个样子,然后AC不了,这里的复杂度是O(n^2)
,题目卡的就是这个O(n^2)
:
#include<iostream>
#include<algorithm>
using namespace std;
#define MAX_L 100000
int sequence[MAX_L];
int dp[MAX_L];
int main()
{
int n;
while(cin >> n)
{
// 清空数组
memset(sequence,0,sizeof(sequence));
memset(dp, 0, sizeof(dp));
// 输入数据
for (int j = 0;j < n;j++)
{
cin>>sequence[j];
dp[j]=1; // 先全部初始化为1
}
for (int i = 0;i < n;i++)
{
for (int j = 0;j < i;j++)
{
if(sequence[i]>sequence[j])
{
dp[i]=max(dp[i],dp[j]+1);
}
}
}
// 找出结果的最大值
int res=-1;
for (int i = 0;i < n;i++)
{
res=max(res,dp[i]);
}
cout<< res <<endl;
}
return 0;
}
解法二
然后我看到书上有另一种巧妙的办法,愣是看了半小时才搞懂,它把DP的记录的东西变了一下,下面我结合自己的理解稍加修改后放出来,如下:
d
p
[
i
]
:
所
有
长
度
为
i
的
上
升
子
序
列
中
,
结
尾
最
小
的
那
个
序
列
的
结
尾
数
字
(
多
读
几
遍
。
。
。
)
dp[i]:所有长度为i的上升子序列中,结尾最小的那个序列的结尾数字 (多读几遍。。。)
dp[i]:所有长度为i的上升子序列中,结尾最小的那个序列的结尾数字(多读几遍。。。)
!!!(dp下标从1开始)!!!,这里从1开始,也会更加便于理解
我先举个例子来说明,然后再给出怎么转移的,现在假设有5个数字,分别为{4,2,3,1,5},好的,我们一个一个数字来考虑:
-
按照上述dp的含义,最开始,dp[1]就是4了,没问题吧?(dp下标从1开始,别忘了)
-
然后数字2来了,2<4,那么dp[1]应该更新为2了,这个时候dp[2]也没办法知道,也根本不存在dp[2]
-
然后3来了,2<3<4,于是 4 2 3这样的序列中,dp[1]还是2不变,而所有长度为2的上升子序列中(这里只有2 3),结尾最小的那个序列是什么? 因为只有 2 3序列, 所以dp[2]就是3了。这是我们人工列举出来的结果。
细想一下,我们之前求得dp[1]=2,那么现在来的数字3,比它大,也就是说,现在可以有长度为2的上升子序列了,这不就是我们刚才列举出来的dp[2]吗?对的,所以以后每次新来一个数字时,我们都去已经求解的dp[]数组中找一个比当前数字小的值(这个值可能会有多个,取最大的那个),记为dp[i],那么我们下一步就是要更新 d p [ i + 1 ] = m i n ( d p [ i + 1 ] , a j ) dp[i+1]=min(dp[i+1],a_j) dp[i+1]=min(dp[i+1],aj) -
然后1来了,按照步骤3中的说法,好像找不到一个满足条件的dp[i]啊? 没关系,找不到就算了。但是每次新来一个数的时候,都要和dp[1]去比一下,因为1个孤零零的数字自己也是一个上升子序列,只不过长度为1嘛。也就是 d p [ 1 ] = m i n ( d p [ 1 ] , a i ) dp[1]=min(dp[1],a_i) dp[1]=min(dp[1],ai)
-
最后5来了,5可以找到dp[i]=3,其中i=2,然后按照步骤3,dp[i+1]即dp[3]=5。
初始化全为MAX_INT
,上述过程如下图所示:
然后是这样的动态转移方程:
如果在之前求解的dp中,找到第一个dp[i],使得a[j]>dp[i],那么dp[i+1]=min(dp[i+1],a[j])。
当然,也要注意每次和dp[1]比较一下,因为每个值自己都是一个长度为1的上升子序列。(在代码中会发现其实并不需要这么干,因为使用的lower_bound()这个函数,具体看代码中的注释吧。)
#include<iostream>
#include<algorithm>
using namespace std;
#define MAX_L 100000
int sequence[MAX_L];
int dp[MAX_L+1];
int main()
{
int n;
while (cin >> n)
{
// 清空数组
memset(sequence, 0, sizeof(sequence));
fill(dp,dp+ MAX_L+1, INT_MAX);
// 输入数据
for (int j = 0;j < n;j++)
{
cin >> sequence[j];
}
for (int j =0;j < n;j++)
{
// 注意,这里我们是从1下标开始的,所以在指针的开始和结尾都加上了一个int的长度
// 在这里用了lower_bound这个内置函数,它会二分查找第一个大于给定值的指针位置 (可以自己推导一下,dp是单调递增的)
int* this_i_plus_1=lower_bound(dp+1, dp + MAX_L+1,sequence[j]);
// 即动态转移方程的dp[i+1]=min(dp[i+1],a[j])部分
*this_i_plus_1=min(*this_i_plus_1,sequence[j]);
// 和长度为1的上升序列比较
dp[1]=min(dp[1],sequence[j]); // 这里这句话其实可以不要写,因为lower_bound没找到大于给定值时,会指向第一个,也就下面要比的dp[1]
}
// 找出结果的最大值
// 先找到从下标1开始往后的第一个MAX_INT,也就是自从初始化以来从没改过的值
int *max_value_pos =lower_bound(dp +1, dp + MAX_L+1, INT_MAX);
// 再找到dp数组的头指针,这里从下标1 开始的
int *head=dp+1;
// 两个相减就是最长的上升子序列长度了
cout << max_value_pos-head << endl;
}
return 0;
}
指针是真的强大,指哪打哪,真的好用啊!!!