最长递增子序列

概述

最长递增子序列(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的长度

  1. 首先,把arr[1]放到seq里,令seq[1] = 3,就是说当只有一个数字3时,长度为1的LIS的最小末尾是3,这时len为1
  2. 然后,把arr[2]有序的放到seq里,令seq[1] = 2,就是说长度为1的LIS最小末尾是1,seq[1] = 3已经没用了,这时len仍为1(ps:应该比较好理解)
  3. 接着,arr[3] = 6, arr[3] > seq[1],所以令seq[1 + 1] = seq[2] = arr[3] = 6,也就是说长度为2的LIS的最小末尾是6,这时len为2
  4. 再来,arr[4] = 4,它正好在2, 6之间,放在2的位置显然不合适,因为2  < 4,长度为1的LIS最小末尾应该为2,这样很容易推理出长度为2的LIS最小末尾应该为4,于是把6淘汰,这时seq[2] = {2, 4}, len为2
  5. 继续,arr[5] = 7,它在seq[2]后面,因此seq[2 + 1] = seq[3] = 7,这时len为3
  6. 第6个,arr[6] = 5, 它在4和7之间,因此把7替换掉,seq[3] = {2, 4, 5}, len为3
  7. 第7个,arr[7] = 9, 它在seq[3]后面,因此seq[4] = {2, 4, 5, 9}, len为4
  8. 第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

动态规划的问题

最长递增子序列_第1张图片

用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名,得瑟一下:

最长递增子序列_第2张图片

这里可以看到,高手都是苦逼过来的,继续加油!



你可能感兴趣的:(C/C++,算法)