概述
最长递增子序列(Longest Increasing Subsequence)长度有很多种解决方法,这里介绍两种,一种是动态规划的实现,一种是O(NlogN)的解决方法(参考编程之美)
动态规划
我们假定w0,w1,...,wn-1为一串正整数序列,前i个数的最长递增子序列的长度为A(i),则最优解结构为:
- A(0) = 1
- A(i) = max{A(j) + 1, 0 <= j < i}
时间复杂度为O(n * n)
实现代码也很简单,贴一下c的实现代码(11月7日增加了打印最长递增子序列的代码)
题目
题目描述:
给定一个整数序列,请问如何去掉最少的元素使得原序列变成一个全递增的序列。
输入:
输入的第一行包括一个整数N(1<=N<=10000)。
接下来的一行是N个满足题目描述条件的整数。
输出:
可能有多组测试数据,对于每组数据,
输出去掉最少的元素后的全递增序列。
样例输入:
8
186 186 150 200 160 130 197 220
样例输出:
150 160 197 220
提示:
如果有多个结果序列满足条件,输出相对位置靠前的那个序列。
ac代码
#include
#include
#include
#define N 10050
int dp[N], seq[N], pre[N];
int count, maxindex;
int longIS(int *arr, int n)
{
int i, j, lis;
dp[0] = 1;
memset(pre, 0, sizeof(pre));
for (i = 1, lis = 1, maxindex = 0; i < n; i ++) {
dp[i] = 1;
for (j = 0; j < i; j ++) {
if (arr[j] < arr[i] && dp[j] + 1 > dp[i]) {
dp[i] = dp[j] + 1;
pre[i] = j;
}
}
if (dp[i] > lis) {
lis = dp[i];
maxindex = i;
}
}
return lis;
}
void outputLis(int *arr, int lis)
{
for (count = 0; lis > 0; lis --, count ++) {
seq[count] = arr[maxindex];
maxindex = pre[maxindex];
}
}
int main(void)
{
int i, n, lis, *arr;
while (scanf("%d", &n) != EOF) {
arr = (int *)malloc(sizeof(int) * n);
for (i = 0; i < n; i ++) {
scanf("%d", arr + i);
}
lis = longIS(arr, n);
outputLis(arr, lis);
for (i = count - 1; i >= 0; i --) {
if (i == 0) {
printf("%d\n", seq[i]);
} else {
printf("%d ", seq[i]);
}
}
free(arr); arr = NULL;
}
return 0;
}
/**************************************************************
Problem: 1262
User: wangzhengyi
Language: C
Result: Accepted
Time:20 ms
Memory:1032 kb
****************************************************************/
O(nLogn)算法实现
这里参考了别人的实现算法,原文链接: http://www.felix021.com/blog/read.php?1587
算法思路
这里举一个例子来讲解这个算法,假设一个序列 arr[8] = {3, 2, 6, 4, 7, 5, 9, 10}, 可以看出这里的LIS长度为5.我们尝试一步步的推理一下:
我们定义一个序列seq,然后另i = 1 to 8考察这个序列,同时用len记录LIS的长度
- 首先,把arr[1]放到seq里,令seq[1] = 3,就是说当只有一个数字3时,长度为1的LIS的最小末尾是3,这时len为1
- 然后,把arr[2]有序的放到seq里,令seq[1] = 2,就是说长度为1的LIS最小末尾是1,seq[1] = 3已经没用了,这时len仍为1(ps:应该比较好理解)
- 接着,arr[3] = 6, arr[3] > seq[1],所以令seq[1 + 1] = seq[2] = arr[3] = 6,也就是说长度为2的LIS的最小末尾是6,这时len为2
- 再来,arr[4] = 4,它正好在2, 6之间,放在2的位置显然不合适,因为2 < 4,长度为1的LIS最小末尾应该为2,这样很容易推理出长度为2的LIS最小末尾应该为4,于是把6淘汰,这时seq[2] = {2, 4}, len为2
- 继续,arr[5] = 7,它在seq[2]后面,因此seq[2 + 1] = seq[3] = 7,这时len为3
- 第6个,arr[6] = 5, 它在4和7之间,因此把7替换掉,seq[3] = {2, 4, 5}, len为3
- 第7个,arr[7] = 9, 它在seq[3]后面,因此seq[4] = {2, 4, 5, 9}, len为4
- 第8个,arr[8] = 10,它在seq[4]后面,因此seq[5] = {2, 4, 5, 9, 10}, len为5
同时,可以发现每次往seq插入数据时,seq是有序的,也就是说我们可以用二分查找算法确定每次插入的位置,于是算法复杂度也就为
O(NlogN)
实现代码
#include
#include
void longest_increasing_seq(int *arr, int n)
{
int i, len, left, right, mid, *seq;
seq = (int *)malloc(sizeof(int) * n);
// 初始化
seq[0] = arr[0];
len = 1;
for (i = 1; i < n; i ++) {
if (arr[i] > seq[len - 1]) { // arr[i]比seq序列所有数字都大,则插入到seq末尾,len自加1
seq[len] = arr[i];
len += 1;
} else {
left = 0, right = len - 1;
while (left <= right) {
mid = left + ((right - left) >> 1); // 防止溢出
if (arr[i] < seq[mid])
right = mid - 1;
else if (arr[i] > seq[mid])
left = mid + 1;
else
break;
}
seq[mid] = arr[i];
}
}
printf("%d\n", len);
free(seq);
}
int main(void)
{
int i, n, *arr;
while (scanf("%d", &n) != EOF) {
arr = (int *)malloc(sizeof(int) * n);
for (i = 0; i < n; i ++)
scanf("%d", arr + i);
longest_increasing_seq(arr, n);
free(arr);
}
return 0;
}
起因
说下起因吧,也是因为在九度oj做题目,一道明显最长递增字串的题目,用动态规划做的时候,5个case只能通过三个,第4个case超时了,悲剧,所以学习了一种新的求最长递增字串的方法,挺不错的,顺便复习了二分查找(ps:注意我二分查找的几处细节,据说90%的程序员无法写出正确的二分查找算法,但是我一定是那百分之10以内的)
题目描述
题目描述:
在读高中的时候,每天早上学校都要组织全校的师生进行跑步来锻炼身体,每当出操令吹响时,大家就开始往楼下跑了,然后身高矮的排在队伍的前面,身高较高的就要排在队尾。突然,有一天出操负责人想了一个主意,想要变换一下队形,就是当大家都从楼上跑下来后,所有的学生都随机地占在一排,然后出操负责人从队伍中抽取出一部分学生,使得队伍中剩余的学生的身高从前往后看,是一个先升高后下降的“山峰”形状。据说这样的形状能够给大家带来好运,祝愿大家在学习的道路上勇攀高峰。(注,山峰只有一边也符合条件,如1,1、2,2、1均符合条件)
输入:
输入可能包含多个测试样例。
对于每个测试案例,输入的第一行是一个整数n(1<=n<=1000000):代表将要输入的学生个数。
输入的第二行包括n个整数:代表学生的身高(cm)(身高为不高于200的正整数)。
输出:
对应每个测试案例,输出需要抽出的最少学生人数。
样例输入:
6
100 154 167 159 132 105
5
152 152 152 152 152
样例输出:
0
4
动态规划的问题
用O(n * n)的解法会超时,因此改用二分查找这种解决方法
#include
#include
#include
void lis(int *queue, int n)
{
int i, k, l, r, mid, max, *seq, *left, *right;
seq = (int *)malloc(sizeof(int) * n);
left = (int *)malloc(sizeof(int) * n);
right = (int *)malloc(sizeof(int) * n);
seq[0] = queue[0];
left[0] = 1;
k = 1;
for (i = 1; i < n; i ++) {
if (queue[i] > seq[k - 1]) {
left[i] = k + 1;
seq[k] = queue[i];
k += 1;
} else {
left[i] = k;
l = 0, r = k - 1;
while (l <= r) {
mid = (l + r) >> 1;
if (seq[mid] > queue[i])
r = mid - 1;
else if (seq[mid] < queue[i])
l = mid + 1;
else {
l = mid;
break;
}
}
seq[l] = queue[i];
}
}
/* 打印测试
for (i = 0; i < n; i ++)
printf("%d ", left[i]);
printf("\n");
*/
memset(seq, 0, sizeof(seq));
seq[0] = queue[n - 1];
right[n - 1] = 1;
k = 1;
for (i = n - 2; i >= 0; i --) {
if (queue[i] > seq[k - 1]) {
right[i] = k + 1;
seq[k] = queue[i];
k += 1;
} else {
right[i] = k;
l = 0, r = k - 1;
while (l <= r) {
mid = (l + r) >> 1;
if (seq[mid] > queue[i])
r = mid - 1;
else if (seq[mid] < queue[i])
l = mid + 1;
else {
l = mid;
break;
}
}
seq[l] = queue[i];
}
}
/* 打印测试
for (i = 0; i < n; i ++)
printf("%d ", right[i]);
printf("\n");
*/
for (i = 0, max = 0; i < n; i ++) {
if (left[i] + right[i] - 1 > max)
max = left[i] + right[i] - 1;
}
printf("%d\n", n - max);
free(seq);
free(left);
free(right);
}
int main(void)
{
int i, n, *queue;
while (scanf("%d", &n) != EOF) {
// 初始化
queue = (int *)malloc(sizeof(int) * n);
for (i = 0; i < n; i ++)
scanf("%d", queue + i);
lis(queue, n);
free(queue);
}
return 0;
}
/**************************************************************
Problem: 1500
User: wangzhengyi
Language: C
Result: Accepted
Time:950 ms
Memory:16544 kb
****************************************************************/
后记
本来早就该发表这篇文章,无奈二分查找赋值那里浪费了一些时间,不过ac了这到题目我进入了九度oj排行榜的第10名,得瑟一下:
这里可以看到,高手都是苦逼过来的,继续加油!