动态规划初步--最长上升子序列(LIS)
一、问题
有一个长为n的数列 a0,a1,a2...,an-1a。请求出这个序列中最长的上升子序列的长度和对应的子序列。上升子序列指的是对任意的i < j都满足ai < aj的子序列。
二、思路
如果i < j且ai < aj则认为ai到aj存在有向边,由于一个数不可能直接或间接的指向自己,所以是一个有向无环图。但是,在这里我们并不需要真正的建立图。我们可以用动态规划来做,对于状态的设定有多种方式。
三、代码实现
1、将dp[i]表示以ai结束的最长上升子序列,当j < i且a[j] < a[i],dp[i] = max(dp[i],dp[j] + 1),初始化dp[i] = 1。由于定义的是结束位置,所以记录的是逆序,用栈倒过来再输出就行。
代码一:
#include<stdio.h>
#include<iostream>
#include<stack>
#include<algorithm>
using namespace std;
const int INF = 0x3f3f3f3f;
const int maxn = 1000 + 10;
int n;
int a[maxn], dp[maxn]; //dp[i]表示以i结束的最长上升子序列
int nextp[maxn]; //记录路径
int res = 0;
int first = 0;
void print_ans()
{
stack<int>s;
for (int i = 0; i < res; i++)
{
s.push(first + 1);
first = nextp[first];
}
for (int i = 0; i < res; i++)
{
int tmp = s.top(); s.pop();
printf("%d ", tmp);
}
printf("\n%d\n", res);
}
void slove()
{
memset(dp, 0, sizeof(dp));
memset(nextp, 0, sizeof(nextp));
res = 0; first = 0; //记得清零
for (int i = 0; i < n; i++)
{
dp[i] = 1;
for (int j = 0; j < i; j++)
{
if (a[j] < a[i])
{
if (dp[j] + 1 > dp[i])
{
dp[i] = dp[j] + 1;
nextp[i] = j;
}
}
}
if (dp[i] > res)
{
res = dp[i];
first = i;
}
}
print_ans();
}
int main()
{
while (scanf("%d", &n) == 1 && n)
{
for (int i = 0; i < n; i++)
scanf("%d", &a[i]);
slove();
}
return 0;
}
代码二:两者只是输出的处理不同,用逆向寻路,不需要额外的空间,但要消耗一些时间。
点击查看代码
#include<stdio.h>
#include<iostream>
#include<stack>
#include<vector>
#include<algorithm>
using namespace std;
const int INF = 0x3f3f3f3f;
const int maxn = 1000 + 10;
int n;
int a[maxn], dp[maxn]; //dp[i]表示以i结束的最长上升子序列
stack<int>sta;
void print_ans(int s)
{
sta.push(s + 1);
if (dp[s] == 1)
{
while (!sta.empty())
{
int tmp = sta.top(); sta.pop();
printf("%d ", tmp);
}
return;
}
for (int i = 0; i < n; i++)
{
if (a[i] < a[s] && dp[s] == dp[i] + 1)
{
print_ans(i);
break;
}
}
}
void slove()
{
memset(dp, 0, sizeof(dp));
int res = 0;
int first = 0;
stack<int>s;
for (int i = 0; i < n; i++)
{
dp[i] = 1;
for (int j = 0; j < i; j++)
{
if (a[j] < a[i])
{
dp[i] = max(dp[i],dp[j] + 1);
}
}
if (dp[i] > res)
{
res = dp[i];
first = i; //first记录最长上升子序列的最后元素的位置
}
}
print_ans(first);
printf("\n%d\n", res);
}
int main()
{
while (scanf("%d", &n) == 1 && n)
{
for (int i = 0; i < n; i++)
scanf("%d", &a[i]);
slove();
}
return 0;
}
2、用dp[i]表示以i开始的最长上升子序列,初始化同样是dp[i] = 0。同时由于定义的是开始位置,可以直接输出最长上升子序列。
点击查看代码
void print_ans()
{
for (int i = 0; i < res; i++)
{
printf("%d ", first + 1);
first = nextp[first];
}
printf("\n%d\n", res);
}
void slove()
{
res = 0; //定义成全局变量时,注意清零
for (int i = n - 1;i >= 0; i--)
{
dp[i] = 1;
for (int j = i + 1; j < n; j++)
{
if (a[i] < a[j])
{
if (dp[j] + 1 > dp[i])
{
dp[i] = dp[j] + 1;
nextp[i] = j;
}
}
}
if (dp[i] > res)
{
res = dp[i];
first = i;
}
}
print_ans();
}
3、把dp[i]表示长度为i + 1的上升子序列中末尾元素的最小值(不存在的话就是INF)。
贪心 + 二分查找,利用贪心思想,对于一个一个上升子序列,显然最后一个元素越小,越有利于添加新的元素,这样序列就越长。
类似的我们也可以定义对称的状态,即把dp[i]表示长度为i + 1的上升子序列中开始元素的最大值
相比前面的状态定义,有两个好处:如果子序列长度相同,可以使最末尾元素的值最小;时间复杂度由O(n^2)降至O(nlogn)。
开始全部初始化为INF,如果i == 0或者dp[i - 1] < aj,dp[i] = min(dp[i],aj),最终dp[i] < INF,最大的i + 1就是结果。如何找到aj呢,由于dp[i]记录的长度为i + 1的最末尾元素的最小值,而最末尾元素是这个长度为i+1中的最大值,所以aj大于dp[i]就能更新。我们还能发现,dp单增,每个aj只需用来更来一次。
完整的dp是,dp[i][j] 表示前i个、长度为j的最长上升子序列的末尾元素的最小值,对于每个i,只会更新某一个j且只需要logn的时间、
n个元素,每次查找logn,总的时间复杂度为O(nlogn).
void slove()
{
fill(dp, dp + n, INF);
for (int i = 0; i < n; i++)
{
*lower_bound(dp, dp + n, a[i]) = a[i];
// *upper_bound(dp, dp + n, a[i]) = a[i]; // 非严格递增用upper_bound
}
int first = lower_bound(dp, dp + n, INF) - dp;
printf("%d\n", dp[first - 1]);
printf("%d\n", first);
}
打印结果路径似乎不太方便,以后再补上吧。
一个例题:leetcode 354. 俄罗斯套娃信封问题
只需要先将第一维升序排序,第二维降序,然后对第二维按照上面的方法处理
因为最终的最长上升子序列肯定是两维都是有序的,将第一维排序不会影响最终答案,并达到降维的效果
而将第二维降序,防止这种情况 {2, 2}, {3,4}, {5, 6}, {5,7}
正确的答案应该是3而不是4
class Solution {
public:
// https://leetcode-cn.com/problems/russian-doll-envelopes/solution/liang-ge-wei-du-de-zui-chang-di-zeng-zi-ctbmd/813562
// 排序完就当一维的做了
int maxEnvelopes(vector<vector<int>>& envelopes) {
sort(envelopes.begin(), envelopes.end(), [](vector<int>& a, vector<int>& b) {
if (a[0] == b[0])
return a[1] > b[1];
return a[0] < b[0];
});
vector<int> dp(envelopes.size(), INT_MAX);
for (int i = 0; i < envelopes.size(); i++) {
*lower_bound(dp.begin(), dp.begin() + i, envelopes[i][1]) = envelopes[i][1];
}
// return max_element(dp.begin(), dp.end())-dp.begin(); // {1,1}
return lower_bound(dp.begin(), dp.end(), INT_MAX)-dp.begin();
}
};