一个数的序列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).
而这里要求的就是 最长上升子序列的长度。
下面介绍几种求解方法:
int length_LIS(int *a, int n)
{
for (int i = 1; i <= n; i++) //初始化 dp[] 数组,初值为 1
dp[i] = 1;
int maxn = 1; // 记录当前最长递增自序列的长度
for (int i = 2; i <= n; i++)
{
for (int j = 1; j < i; j++) //遍历 i 前面的数
{
if (a[j] < a[i])
dp[i] = max(dp[i], dp[j] + 1); //在当前最长值 与 之前最长值加 1 之间选取最大值
}
maxn = max(dp[i], maxn); // 刷新最长值
}
return maxn;
}
算法思路:
dp[i] 表示第 i 个位置处最长的递增子序列长度。所以,dp[i]的初始值都为 1 。对于每一个位置的数,都要去遍历一遍它之前的数,用来更新该位置的 dp[i] 的值。状态转移方程:dp[i] = max(dp[i], dp[j] + 1);
举个例子:设数组为 : 1 7 3 5 9 4 8. 来求它的最长递增子序列的长度。
第一个数,dp[1] = 1,子序列为 1.
第二个数,dp[2] = dp[1] + 1 = 2,子序列为 1,7.
第三个数,dp[3] = dp[1] + 1 = 2,子序列为1, 3.
第四个数,二层循环遍历到 1 时,dp[4] = dp[1] + 1 = 2;当遍历到 3 时,dp[4] = dp[3] + 1 = 3,;取最大值 dp[4] = 3,子序列为 1, 3, 5.
第五个数,二层循环遍历到 1 时,dp[5] = dp[1] + 1 = 2;遍历到 7 时,dp[5] = dp[2] + 1 = 3;遍历到 3 时,dp[5] = dp[3] +1 = 3;遍历到 5 时,dp[5] = dp[4] + 1 = 4;最后取最大值 dp[5] = 4,子序列为 1, 3, 5, 9.
第六个数,类似于前面的遍历,最后得到 dp[6] = dp [3] + 1 = 3,子序列为 1, 3, 4;
第七个数,同理,dp[7] = dp[6] + 1 = 4,子序列为1, 3, 4, 8.
最后,在dp数组中寻找最大值就是解。
由上面的理解可以衍生一种求最大增长子序列和的方法:
//最大增长子序列和
int sum_LIS(int *a, int n)
{
for (int i = 1; i <= n; i++) // 初始化dp[]数组为 a[]中值
dp[i] = a[i];
a[0] = 0;
int sum_maxn = -9999; // 记录当前最大增长序列和
for (int i = 1; i <= n; i++)
{
for (int j = 0; j < i; j++)
{
if (a[j] < a[i])
dp[i] = max(dp[i], dp[j] + a[i]); //这里变成了加上数组值
}
sum_maxn = max(dp[i], sum_maxn);
}
return sum_maxn;
}
简单的就是把 dp 数组初始值改成 a 数组中的对应值,状态转移时加的是a[i](对应总和),而不是 1(对应长度)。
//二分查找,寻找比 key 大的第一个元素的位置
int binary_search(int *a, int left, int right, int key)
{
while (left <= right)
{
int mid = (left + right) >> 1;
if (a[mid] <= key)
left = mid + 1;
else
right = mid - 1;
}
return left;
}
//贪心 + 二分求解最长递增子序列
int greedy_length_LIS(int *a, int n)
{
for (int i = 1; i <= n; i++)
low[i] = INF;
low[1] = a[1];
int ans = 1;
for (int i = 2; i <= n; i++)
{
if (a[i] > low[ans]) //大于low末尾值,则向后接
low[++ans] = a[i];
else {
//否则,找到low中第一个 >=a[i] 的位置low[j],用a[i]更新low[j]
int index = binary_search(low, 1, n, a[i]);
low[index] = a[i];
}
}
return ans;
}
算法思路:定义一个low数组,用来表示长度为i的LIS结尾元素的最小值。对于一个上升子序列,显然其结尾元素越小,越有利于在后面接其他的元素,也就越可能变得更长。因此,只需要维护 low 数组,对于每一个a[ i ],如果a[ i ] > low [当前最长的LIS长度],就把 a [ i ]接到当前最长的LIS后面,即low [++当前最长的LIS长度] = a [ i ]。最后,low数组的长度就是最长递增子序列的长度。
具体操作:对于每一个a [ i ],如果a [ i ]能接到 LIS 后面,就接上去;否则,就用 a [ i ] 取更新 low 数组。在low数组中找到第一个大于等于a [ i ]的元素low [ j ],用a [ i ]去更新 low [ j ]。
对于low 数组,内部一定是单调不降的,所有可以二分 low 数组,找出第一个大于等于a[ i ]的元素。二分一次 low 数组的时间复杂度的O(lgn),所以总的时间复杂度是O(nlogn)。
参考来自:
https://blog.csdn.net/wbin233/article/details/77570070
https://blog.csdn.net/lxt_Lucia/article/details/81206439
(未完待续)