给定一个长度为N的数列,求数值严格单调递增的子序列的长度最长是多少。
输入格式
第一行包含整数N。
第二行包含N个整数,表示完整序列。
输出格式
输出一个整数,表示最大长度。
数据范围
1≤N≤1000, −1e9≤数列中的数≤1e9
dp状态定义:
f[i]表示以第i个数a[i]结尾的最长上升子序列
对于f[i],表示的状态集合中,最长长上升子序列的最后一个一定是a[i],然后枚举i之前的数a[j](j<i),在所有的j中取一个最大值:f[i]=max(f[i],f[j]+1)
如果只要求输出长度:
const int N = 1e3+5;
int n,a[N],f[N];
int main()
{
cin>>n;
for(int i=1;i<=n;i++) cin>>a[i];
for(int i=1;i<=n;i++)
{
f[i]=1; //一开始只有自己,初始化为1
for(int j=1;j<i;j++) //从1枚举到i-1
if(a[j]<a[i]) f[i]=max(f[i],f[j]+1); //在所有满足的j中取一个最大值
}
int res=0;
for(int i=1;i<=n;i++) res=max(res,f[i]);
cout<<res<<endl;
return 0;
}
如果要求打印出最长上升子序列,可以开一个pre数组,记录状态i是由哪一个状态j转移而来的
const int N = 1e3+5;
int n,a[N],f[N],pre[N];
int main()
{
cin>>n;
for(int i=1;i<=n;i++) cin>>a[i];
for(int i=1;i<=n;i++)
{
f[i]=1; pre[i]=0;
for(int j=1;j<i;j++)
if(a[j]<a[i])
{
if(f[i]<f[j]+1)
{
f[i]=f[j]+1;
pre[i]=j;
}
}
}
int k=1;
for(int i=1;i<=n;i++)
if(f[k]<f[i]) k=i;
cout<<f[k]<<endl;
//打印最长方案
while(k) //这里是倒着打印出来的,实际操作过程中可以先存下来,在正序输出
{
cout<<a[k]<<" ";
k=pre[k];
}
return 0;
}
最长上升子序列 加强版:
给定一个长度为N的数列,求数值严格单调递增的子序列的长度最长是多少。
输入格式
第一行包含整数N。
第二行包含N个整数,表示完整序列。
输出格式
输出一个整数,表示最大长度。
数据范围
1≤N≤1e5, −1e9≤数列中的数≤1e9
由于n是1e5,O(n2)做法会超时 ,需要一个O(n logn )的做法
、
、
观察前面的代码发现,时耗主要有两个:
一是枚举每一个数a[i]。
二是对于每一数a[i]找到前面比它小的所有的数a[j],在所有的f[j]中取一个最大值。
首先,第一步枚举a[i]肯定无法在优化了,因为每一个数都必须去枚举。
思考如何优化第二步的过程?
我们可以对所有的上升子序列分一个类,长度为1的一类,为2的一类……
然后开一个b数组维护每一类的上升子序列结尾的最小值。
b[k]的含义是:在所有长度为k的上升子序列中,序列结尾数的最小值。
因为上升子序列的结尾越小,它后面可以衔接上的数的范围就越大。
比如说,有长度为4的上升子序列x1和x2,x1以6结尾,x2以8结尾,那么我们只需要记住x1的6就好了,因为所有8可以衔接上的数,6都可以,而且6还可以接上7,8.
在来看b数组有什么特点?
我们可以证明:
b数组一定是一个严格单调上升的数组,因为b中维护的是每一类子序列结尾的最小值。
对于b[i]和b[i+1],也就是长度为i的上升子序列结尾的最小值和长度为i+1的上升子序列结尾的最小值,b[i+1]一定大于b[i]
反证法:
假设b[i+1]<=b[i], 那么该长度为i+1的子序列的第i个数一定小于b[i](因为是上升子序列),这与b[i]是所有长度为i的上升子序列的结尾数的最小值矛盾了
所以,对于每一个数a[i],我们不用去枚举它前面的每一个数,只需要去b数组中找到比它小的最大的一个数在哪一个位置,比如说在b[k]这个位置,那么f[i]=k+1,至于这个找数的过程就可以用二分来解决(因为b数组严格单调递增)
const int N = 1e5+5, inf = 0x3f3f3f3f;
int n,a[N],f[N];
vector<int> b(N + 1, inf); //初始化b数组,一开始所有数都为无穷大
int main()
{
IOS;
cin >> n;
for(int i = 1; i <= n; i++) cin >> a[i];
for(int i = 1; i <= n; i++)
{
f[i]=1; b[1]=min(b[1],a[i]); //更新长度为1的上升子序列结尾的最小值
int l = 0, r = n + 1; //二分找比a[i]小的最大的一个数
while(l+1 != r)
{
int mid = l + r >> 1;
if(b[mid] < a[i]) l = mid;
else r = mid;
}
f[i] = l + 1;
b[l+1] = min(b[l+1], a[i]); //更新长度为l+1的上升子序列结尾的最小值
}
int res = 0;
for(int i = 1; i <= n; i++)
res = max(res, f[i]);
cout << res << endl;
return 0;
}
简洁版:
const int N = 1e5+5;
int n, a[N];
int len, b[N];
int main()
{
IOS;
cin >> n;
for(int i = 1; i <= n; i++) cin >> a[i];
//memset(b, 0x3f, sizeof b); 不需要初始化为无穷大
b[0] = -inf; //哨兵,处理边界
for(int i = 1; i <= n; i++)
{
int l = -1, r = len+1; //[0,len]
while(l+1 != r)
{
int mid = l + r >> 1;
if(b[mid] < a[i]) l = mid;
else r = mid;
}
len = max(len, l+1);
b[l+1] =a[i];
//b[l+1] = min(b[l+1], a[i]),并不用比较在取最小值,因为a[i]一定比b[l+1]小
}
cout << len << endl;
return 0;
}