【最长上升子序列LIS】O(n^2)和O(nlogn)算法简记

最长上升子序列(Longest Increasing Subsquence)是指对一个序列,其中满足i < j < k且a[i] < a[j] < a[k]的最长子序列a[]。比如1 4 2 6 3 7 9,则【1,2,3,7,9】就是它的LIS。

LIS普遍求法为动态规划。有两种算法。

第一种比较好写,复杂度O(n^2)。

设原序列为a[]。所有下标从1开始(即[1,n])。定义dp[i]为以a[i]结尾的最长上升子序列的长度。很容易得到转移方程:dp[i] = max{1, dp[j] + 1} 且 j < i。可以这么更新:

dp[i] = 1;

for (int j = 1; j < i; ++j) {

if (a[j] < a[i]) dp[i] = max(dp[i], dp[j] + 1);

}

这里选取poj2533来说明具体的实现。

#include <cstdio>
#include <cstring>
#include <algorithm>
using namespace std;

const int MAX = 1024;
const int INF = 0xfffffff;
int a[MAX];
int dp[MAX];
/*
	dp[i]: 以a[i]为结尾的最长上升子序列的长度
*/

inline int read() {
	char ch;
	while ((ch = getchar()) < '0' || ch > '9');
	int x = ch - '0';
	while ((ch = getchar()) >= '0' && ch <= '9') {
		x = (x << 3) + (x << 1) + ch - '0';
	}
	return x;
}

int main() {
	int n;
	while (~scanf(" %d", &n)) {
		for (int i = 1; i <= n; ++i) {
			a[i] = read();
		}
		int ans = 0;
		for (int i = 1; i <= n; ++i) {
			dp[i] = 1;
			for (int j = 1; j < i; ++j) {
				if (a[j] < a[i] && dp[j] + 1 > dp[i]) {
					dp[i] = dp[j] + 1;
				}
			}
			if (dp[i] > ans) ans = dp[i];
		}
		printf("%d\n", ans);
	}
	return 0;
}

很多情况下这种解法达不到我们需要的复杂度,因为实际情形下动辄就是几百万的数据量。。。这时就需要改进上述算法

增加数组d[],d[i]记录的是a[]数组中所有使得dp[j]=i的最小值。维护d[]使得d[]满足单调性(因为是上升子序列,这里就是单调增,如果求下降子序列扩展成单调减即可),另外维护一个值maxLen记录最长位置

举例如下(下标从1开始):

对于序列a[] = {1,5,7,2,3,6,8}

初始化d[] = INF.即任意d[i]都等于一个很大的值,避免影响答案。maxLen = 0即可。

但是d[0] = -INF,下面说原因。

第一次:dp[1] = 1(长度为1), maxLen = 1, d[1] = 1(最长上升子序列为1的最小结尾数字是a[1],等于1),剩下的d[i]不变(下同)。此时d[] = {-INF, 1, INF, INF...INF}

第二次:a[2] = 5 > d[maxLen]。则dp[2] = maxLen + 1 = 2 (长度为2),maxLen = 2,d[2] = 5 (最长上升子序列为2的最小结尾数字是5)。d[] = {-INF, 1, 5, INF,...,INF}

第三次:a[3] = 7 > d[maxLen],则dp[3] = maxLen + 1 = 3,maxLen = 3, d[3] = 7。此时d[] = {-INF, 1, 5, 7, INF, INF, INF...}

第四次:a[4] = 2 < d[maxLen],则从d[1]到d[maxLen]中找最后一个比2小的数,找到数字1,下标为ind = 1(d[1] = 1嘛~),dp[4] = dp[ind] + 1 = dp[1] + 1 = 2, maxLen为3不动, d[ind+1] = d[2] = 2。此时d[] = {-INF, 1, 2, 7, INF, INf, INf...}

第五次:a[5] = 3 < d[maxLen],则从d[1]到d[maxLen]找最后一个比3小的数,找到2,下标为ind = 2(d[2] = 2哦),dp[5] = dp[ind] + 1 = dp[2] + 1 = 3, maxLen为3不动,d[ind+1] = d[3] = 3.此时{-INF, 1, 2, 3, INF, INF, INF...}

第六次:a[6] = 6 > d[maxLen],直接dp[6] = maxLen + 1 = 4, maxLen = 4, d[4] = 6.此时d[] = {-INF, 1, 2, 3, 6, INF, INF...}

第七次:a[7] = 8 > d[maxLen], 直接dp[7] = maxLen + 1 = 4, maxLen = 5, d[5] = 8.此时d[] = {-INF, 1, 2, 3, 6, 8, INF, INF...}

上述就是更新过程,为什么d[0]要初始化为一个负无穷(-INF)呢?因为假设a[]都是正整数,之前记录的d[1] = 5.后面出来一个1,显然如果d[0]不是很小,就不好找所谓“比1小的最后一个数”了,没人比它小!!!

不过话说回来,找最后一个比它小的,更新值时又去更新的是后面一个数,那我们还不如找数组{d[1]到d[i]}中第一个比a[i]大的数呢!呵呵,就是这样的!

你不会到现在还没明白为什么要这么更新吧?我们大费周章让d[]单调递增,然后更新时刻意选择d[]中小于a[i]和大于a[i]的”交界点“,其实是为了可以使用二分搜索,从而加速整个算法呀!二分搜索可以达到O(logn)的复杂度,这样一来我们在更新时不需要遍历所有1<=j<i,只需要更新一个点,复杂度不就马上降下来了嘛!

还有,我们最后得到的答案不就是maxLen了嘛?还要dp[]数组作甚?对,如果只需要最长上升子序列的长度,这个dp数组就没用了。。。不过我还是习惯性地保留,万一用上了呢。。。

下面给出poj2533的实现:

#include <cstdio>
#include <cstring>
#include <algorithm>
using namespace std;

const int INF = 0xfffffff;
const int MAX = 1024;
int dp[MAX];
int a[MAX];
int d[MAX];

inline int read() {
	char ch;
	while ((ch = getchar()) < '0' || ch > '9');
	int x = ch - '0';
	while ((ch = getchar()) >= '0' && ch <= '9') {
		x = (x << 3) + (x << 1) + ch - '0';
	}
	return x;
}

int main() {
	int n;
	while (~scanf(" %d", &n)) {
		for (int i = 1; i <= n; ++i) {
			a[i] = read();
			//scanf(" %d", a + i);
		}

		//初始化
		fill(d, d + n + 1, INF);
		d[0] = -INF;	//-INF必须小于所有的a[i],否则可能影响算法,INF必须大于所有a[i] 

		dp[0] = 0;//下面说明初始化原因 

		int maxLen = 0;
		for (int i = 1; i <= n; ++i) {
			if (a[i] > d[maxLen]) {
				//此时把a[i]加到末尾可以获得更长的子序列
				//这里第一次迭代时必须被执行,我们必须保证a[1] > d[0]。这也是初始化d[0]=-INF的原因
				maxLen++;
				dp[i] = maxLen;
				d[maxLen] = a[i];
			} else {
				int ind = upper_bound(d, d + maxLen + 1, a[i]) - d;//upper_bound找{d[0],..,d[maxLen]}第一个大于a[i]的数,注意下标
				d[ind] = a[i];
				dp[i] = dp[ind-1] + 1;//试想,这里可能找到ind=1,那么就变成dp[0]+1了, 所以dp[0]初始化为0
			}
		}
		printf("%d\n", maxLen);
	}
	return 0;
}

toj4071:

#include <cstdio>
#include <cstring>
#include <algorithm>
using namespace std;

const int INF = 0xffffff;
const int MAX = 100007;
int a[MAX], d[MAX];

struct Node {
	int x, y;
	bool operator<(const Node& B)const {
		return x == B.x ? y < B.y : x < B.x;
	}
} bird[MAX];

inline int read() {
	char ch;
	while ((ch = getchar()) < '0' || ch > '9');
	int x = ch - '0';
	while ((ch = getchar()) >= '0' && ch <= '9') {
		x = (x << 3) + (x << 1) + ch - '0';
	}
	return x;
}

int main() {
	int T, n;
	T = read();
	//scanf(" %d", &T);
	while (T--) {
		n = read();
		//scanf(" %d", &n);
		for (int i = 1; i <= n; ++i) {
			bird[i].x = read();
			bird[i].y = read();
			//scanf(" %d %d", &bird[i].x, &bird[i].y);
		}
		sort(bird + 1, bird + n + 1);
		for (int i = 1; i <= n; ++i) {
			a[i] = bird[i].y;
		}

		fill(d + 1, d + n + 1, INF);
		int maxLen = 0, ind;

		for (int i = 1; i <= n; ++i) {
			if (a[i] >= d[maxLen]) {
				++maxLen;
				d[maxLen] = a[i];
			} else {
				ind = upper_bound(d + 1, d + maxLen + 1, a[i]) - d;
				d[ind] = a[i];
			}
		}
		printf("%d\n", maxLen);
	}
	return 0;
}
时间: 2024-10-01 20:47:53

【最长上升子序列LIS】O(n^2)和O(nlogn)算法简记的相关文章

poj1836——dp,最长上升子序列(lis)

poj1836——dp,最长上升子序列(lis) Alignment Time Limit: 1000MS   Memory Limit: 30000K Total Submissions: 13767   Accepted: 4450 Description In the army, a platoon is composed by n soldiers. During the morning inspection, the soldiers are aligned in a straight

最长上升子序列LIS模板

1 ///最长上升子序列LIS模板 2 int BinSerch(int l,int r,int cut) 3 { 4 while (l<=r) 5 { 6 int m=(l+r)>>1; 7 if (cut>d[m]&&cut<=d[m+1]) return m; 8 if (cut>d[m]) l=m+1; 9 else r=m-1; 10 } 11 return 0; 12 } 13 14 int LIS(int n) 15 { 16 int le

动态规划(DP),最长递增子序列(LIS)

题目链接:http://poj.org/problem?id=2533 解题报告: 状态转移方程: dp[i]表示以a[i]为结尾的LIS长度 状态转移方程: dp[0]=1; dp[i]=max(dp[k])+1,(k<i),(a[k]<a[i]) #include <stdio.h> #define MAX 1005 int a[MAX];///存数据 int dp[MAX];///dp[i]表示以a[i]为结尾的最长递增子序列(LIS)的长度 int main() { int

最长上升子序列LIS解法(n^n &amp;&amp; nlogn)

最长递增子序列问题 在一列数中寻找一些数满足 任意两个数a[i]和a[j] 若i<j 必有a[i]<a[j] 这样最长的子序列称为最长递增子序列LIS LIS问题有两种常见的解法 一种时间复杂度n^n 一种时间复杂度nlogn 下面我们先来说一下n^n的算法 设dp[i]表示以i结尾的最长上升子序列的长度 把问题分解 分解成序列中每一项最为终点的最大上升子序列 从第二项开始依次判断 最后找出最大的一项就是答案 则状态转移方程为 dp[i] = max{dp[j]+1}, 1<=j<

算法--字符串:最长递增子序列LIS

转自:labuladong公众号 很多读者反应,就算看了前文 动态规划详解,了解了动态规划的套路,也不会写状态转移方程,没有思路,怎么办?本文就借助「最长递增子序列」来讲一种设计动态规划的通用技巧:数学归纳思想.  最长递增子序列(Longest Increasing Subsequence,简写 LIS)是比较经典的一个问题,比较容易想到的是动态规划解法,时间复杂度 O(N^2),我们借这个问题来由浅入深讲解如何写动态规划. 比较难想到的是利用二分查找,时间复杂度是 O(NlogN),我们通过

最长递增子序列 LIS 时间复杂度O(nlogn)的Java实现

关于最长递增子序列时间复杂度O(n^2)的实现方法在博客http://blog.csdn.net/iniegang/article/details/47379873(最长递增子序列 Java实现)中已经做了实现,但是这种方法时间复杂度太高,查阅相关资料后我发现有人提出的算法可以将时间复杂度降低为O(nlogn),这种算法的核心思想就是替换(二分法替换),以下为我对这中算法的理解: 假设随机生成的一个具有10个元素的数组arrayIn[1-10]如[2, 3, 3, 4, 7, 3, 1, 6,

最长上升子序列 (LIS算法(nlong(n)))

设 A[t]表示序列中的第t个数,F[t]表示从1到t这一段中以t结尾的最长上升子序列的长度,初始时设F [t] = 0(t = 1, 2, ..., len(A)).则有动态规划方程:F[t] = max{1, F[j] + 1} (j = 1, 2, ..., t - 1, 且A[j] < A[t]). 现在,我们仔细考虑计算F[t]时的情况.假设有两个元素A[x]和A[y],满足 (1)x < y < t (2)A[x] < A[y] < A[t] (3)F[x] =

hdu1025 dp(最长上升子序列LIS)

题意:有一些穷国和一些富国分别排在两条直线上,每个穷国和一个富国之间可以建道路,但是路不能交叉,给出每个穷国和富国的联系,求最多能建多少条路 我一开始在想有点像二分图匹配orz,很快就发现,当我把穷国按顺序排了之后,富国写在它旁边,能够连接的富国就成了一个上升子序列,那么问题来了!上升子序列最长有多长? 想到了这个之后,代码就码起来吧,最开始我的做法是最土的那种,用 dp[i] 表示以 i 结尾的最长上升子序列的长度,每次对于一个 i 遍历 i 前面的所有数 j ,取小于 i 的所有 j 的最大

nlogn 求最长上升子序列 LIS

最近在做单调队列,发现了最长上升子序列O(nlogn)的求法也有利用单调队列的思想. 最长递增子序列问题:在一列数中寻找一些数,这些数满足:任意两个数a[i]和a[j],若i<j,必有a[i]<a[j],这样最长的子序列称为最长递增子序列. 设dp[i]表示以i为结尾的最长递增子序列的长度,则状态转移方程为: dp[i] = max{dp[j]+1}, 1<=j<i,a[j]<a[i]. 这样简单的复杂度为O(n^2),其实还有更好的方法. 考虑两个数a[x]和a[y],x&

计蒜客课程竞赛入门--最长上升子序列(LIS) 流程记

最长上升子序列 (Longest Increasing Subsequence, 常简称为 LIS) 是动态规划解决的一个经典问题. 我们先讲一下子序列是什么.一个数组的子序列就是从里面选出一些元素,并将他们保持原有的先后顺序排列.比如[1, 2, 3, 4, 5]的子序列有[1, 3, 5].[3, 4],而[1, 5, 3]则不是这个数组的子序列. 这里多介绍一下,还有一个容易与子序列混淆的概念:子串.子串是指从一个数组中选出连续的一个或多个元素,并且保持他们原有的顺序.子串一定是子序列,比