算法第三次上机A题
问题描述
一个数的序列 bi,当 b1 < b2 < … < bS的时候,我们称这个序列是上升的。对于给定的一个序列( a1, a2, …, aN),我们可以得到一些上升的子序列( ai1, ai2, …, aiK),这里1 <= i1 < i2 < … < iK <= N。比如,对于序列(1, 7, 3, 5, 9, 4, 8),有它的一些上升子序列,如(1, 7), (3, 4, 8)等等。这些子序列中最长的长度是4,比如子序列(1, 3, 5, 8).
对于给定的序列,求出最长上升子序列的长度。
解法一 : 动态规划
思路:
将问题简化为相同求解方式但规模更小的子问题
1)n个数的最长上升子序列 = 前n-1个数的最长上升子序列 + 比较第n个数
2)前1个数的最长上升子序列 = 1
具体解法:
1)从第一个数开始,寻找以A[i]结尾的最长上升子序列d[i]。
2)寻找d[i]时,比较A[j]=A[1]到A[i-1],当A[j]<A[i],取d[i] 为d[i]和d[j]+1的较大值。
3)最后比较所有d,选出最大的为整个数组的d[i]。
注意:
无法求出最长上升子序列的具体元素
例子:
A = [ 1 7 3 5 9 4 8 ]
d[1] = 1, D[1] = [1]
d[2] = 2, D[2] = [ 1 7 ]
d[3] = 2, D[3] = [ 1 3 ]
d[4] = 3, D[4] = [ 1 3 5 ]
d[5] = 4, D[5] = [ 1 3 5 9 ]
d[6] = 3, D[6] = [ 1 3 4 ]
d[7] = 4, D[7] = [ 1 3 5 8 ]
d = max { d[1] … d[7] } = 4
具体实现:
问题定义:D [ i ] 代表以 A [ i ] 结尾的 LIS 的长度
转子问题:D [ i ] = max { D [ j ] + 1 ,D [ i ] } (1 <= j < i,A[ j ] < A[ i ])
问题终点:D [ i ] = 1 (1 <= i <= n)
时间复杂度:O (n^2)
code
#include <iostream>
#include <cstdio>
#include <algorithm>
#include <cstdlib>
#include <cstring>
#include <cmath>
using namespace std;
const int maxn = 103, INF = 0x7f7f7f7f;
int a[maxn], f[maxn];
int n,ans = -INF;
int main()
{
scanf("%d", &n);
for(int i=1; i<=n; i++)
{
scanf("%d", &a[i]);
f[i] = 1;
}
for(int i=1; i<=n; i++)
for(int j=1; j<i; j++)
if(a[j] < a[i])
f[i] = max(f[i], f[j]+1);
for(int i=1; i<=n; i++)
ans = max(ans, f[i]);
printf("%d\n", ans);
return 0;
}
解法二:贪心+二分
思路:
1)新建一个 low 数组,low [ i ]表示长度为i的LIS结尾元素的最小值。
对于一个上升子序列,显然其结尾元素越小,越有利于在后面接其他的元素,也就越可能变得更长。
2)对于每一个a[ i ],如果a[ i ] > low [当前最长的LIS长度],就把 a [ i ]接到当前最长的LIS后面,即low [++当前最长的LIS长度] = a [ i ]。
3)维护 low 数组
对于每一个a [ i ],如果a [ i ]能接到 LIS 后面,就接上去;
否则,就用 a [ i ] 取更新 low 数组。
具体解法:
1)在low数组中找到第一个大于等于a [ i ]的元素low [ j ],用a [ i ]去更新 low [ j ]。
2)如果从头到尾扫一遍 low 数组的话,时间复杂度仍是O(n^2)。我们注意到 low 数组内部一定是单调不降的,所有我们可以二分 low 数组,找出第一个大于等于a[ i ]的元素。
注意:
二分一次 low 数组的时间复杂度的O(lgn),所以总的时间复杂度是O(nlogn)。
例子:
A = [ 1 7 3 5 9 4 8 ]
A[ ] = 3 1 2 6 4 5 10 7,求LIS长度。
定义B[ i ]来储存可能的排序序列,len 为LIS长度,min为最小末尾
依次把A[ i ]有序地放进B[ i ]里。
A[1] = 1,B[1] = A[1] = 1 => B = [ 1 ], len = 1,min=1
A[2] = 7,7>1,B[2] = A[2] = 7 => B = [ 1 7 ],len = 2,min = 7
A[3] = 3,3<7,B[2] = A[3] = 3 => B = [ 1 3 ],len = 2,min = 3
A[4] = 5,5>3,B[3] = A[4] = 5 => B = [ 1 3 5 ],len = 3,min = 5
A[5] = 9,9>5,B[4] = A[5] = 9 => B = [ 1 3 5 9 ],len = 4,min = 9
A[6] = 4,4<5,B[3] = A[6] = 5 => B = [ 1 3 4 9 ],len = 4,min = 9
A[7] = 8,8<9,B[4] = A[7] = 8 => B = [ 1 3 4 8 ],len = 4,min = 8
最终LIS长度=4
but but but but but
这里的1 3 4 8很明显并不是正确的最长上升子序列。因此,B序列并不一定表示最长上升子序列,它只表示相应最长子序列长度的排好序的最小序列。这有什么用呢?我们最后一步用8替换9并没有增加最长子序列的长度,而这一步的意义,在于记录最小序列,代表了一种“最可能性”,只是此种算法为计算LIS而进行的一种替换。
具体实现:
问题定义:B [ i ] 代表包含 A [ i ] 的和LIS 的长度相同的一个递增序列
转子问题:
- B [ len ] < A [ i ]
B [ ++len ] = A [ i ] - 二分法查找第一个 j,B [ j ] < A [ i ]
B [ j ] = A [ i ]
问题终点:B [ i ] = 1 (1 <= i <= n)
时间复杂度:O (nlogn)
code
#include <cmath>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <iostream>
#include <algorithm>
using namespace std;
const int maxn =1000, INF = 0x7f7f7f7f;
int low[maxn], a[maxn];
int n, ans;
int binary_search(int *a, int n, int x)
//二分查找,返回a数组中第一个>=x的位置
{
int l = 1, mid;
while(l <= n)
{
mid = (l+n) >> 1;
if(a[mid] <= x)
l = mid + 1;
else
n = mid - 1;
}
return l;
}
int main()
{
scanf("%d", &n);
for(int i=1; i<=n; i++)
{
scanf("%d", &a[i]);
low[i] = INF; //由于low中存的是最小值,所以low初始化为INF
}
low[1] = a[1];
ans = 1; //初始时LIS长度为1
for(int i=2; i<=n; i++)
{
if(a[i] > low[ans]) //若a[i]>=low[ans],直接把a[i]接到后面
low[++ans] = a[i];
else //否则,找到low中第一个>=a[i]的位置low[j],用a[i]更新low[j]
low[binary_search(low, ans, a[i])] = a[i];
}
printf("%d\n", ans); //输出答案
return 0;
}
本文详细介绍了如何使用动态规划和贪心+二分搜索算法解决最长上升子序列问题。动态规划解法通过维护以每个元素结尾的最长子序列长度,最终找到全局最长长度。贪心+二分搜索解法通过维护一个单调不降的low数组,利用二分查找提高效率,达到O(nlogn)的时间复杂度。
19万+

被折叠的 条评论
为什么被折叠?



