LIS 算法:计算最长不下降子序列
常见的LIS 算法有两种解法一种是类动态规划,另一种则是二分法维系一个单调队列。
假设给定数组 d, 求 d 的最长不下降子序列
vector<int> d = {5,6,7,1,2,8}; // 总长度为 6
法一:类动态规划
最优子结构:dp[i] 即为 d[i] 的最长子序列
初始化: dp[i] = 1; // 每个序列至少有其本身数字
转移方程:d[i] > d[j] 则 dp[i] = max(dp[i],dp[j] + 1); // j 为 i + 1 ... d.size() - 1 的遍历值;
一:先将dp数组中每个值初始为1
vector<int> dp(d.size(),1); // 按照d的大小初始化并全部赋值为一
二:从末尾倒数第二个元素反向遍历数组 d (因为最后一个元素的最长序列只可能为1)
for (int i = d.size() - 2; i >= 0; i--) {
}
三:j 从 i + 1 遍历到 d.size() - 1, 状态转移
for (int j = i + 1; j < d.size(); j++) {
if (d[i] < d[j]) dp[i] = max(dp[i],dp[j] + 1);
}
四:遍历每一个dp[i] 获取最长不下降子序列长度
int maxn = 1;
for (int i = d.size() - 2; i >= 0; i--) maxn = max(maxn,dp[i]);
总代码:
int LIS(vector<int> d) {
int maxn = 1;
vector<int> dp(d.size(),1);
for (int i = d.size() - 2; i >= 0; i--) {
for (int j = i + 1; j < d.size(); j++) {
if (d[i] < d[j]) dp[i] = max(dp[i],dp[j] + 1);
}
maxn = max(dp[i],maxn);
}
return maxn;
}
法二:二分法维系单调队列
引言:根据贪心思路,为让一个单调队列越长,在一个单调队列ans中ans[i]肯定是越小越好,这样这个单调队列后面能插入元素的机会更大
从引言推出,对于一个上升子序列,显然其结尾元素越小,越有利于在后面接其他的元素,也就越可能变得更长。
首先我们构造一个单调递增队列 ans = {d[0]};
vector<int> ans;
ans.push_back(d[0]);
注意:ans[i] 为 长度为 i 的 LIS 元素结尾最小值,从引言推出 ans.size() 即为 LIS 的值
然后 i 从第二个元素开始遍历整个数组
for (int i = 1; i < d.size(); i++) {
}
如果 d[i] > ans[ans.size() - 1] // 如果比队列中最大的元素还大,ans 就添加这个元素 ans.push_back(d[i]);
if (ans[ans.size() - 1] < d[i]) ans.push_back(d[i]);
反之,就在单调队列中的找到第一个比 d[i] 大的元素并替换他,实现 ans[i] 为 长度为 i 的 LIS 元素结尾最小值
else {
// lower_bound() 二分找到ans数组中大于等于d[i]的第一个位置,注意是大于等于
int pos = lower_bound(ans.begin(),ans.end(),d[i]) - ans.begin();
ans[pos] = d[i];
}
最后ans.size() 就是 LIS 的解
总代码:
int LIS(vector<int> d) {
vector<int> ans;
ans.push_back(d[0]);
for (int i = 1; i < d.size(); i++) {
if (ans[ans.size() - 1] < d[i]) ans.push_back(d[i]);
else {
int pos = lower_bound(ans.begin(),ans.end(),d[i]) - ans.begin();
ans[pos] = d[i];
}
}
return ans.size();
}
题型练习:[蓝桥杯 2020 国 B] 游园安排 - 洛谷
题目背景:典型的LIS 变种问题
题目描述:
给定一个长字符串,其中每个大写字母搭配0到多个小写字母就是一个人名,列如: "SgadKHsdfa" 其中有三个人名,分别为: Sgad,K,Hsdfa。请输出人名的最长的不下降子序列。
输入:WoAiLanQiaobei
输出:AiLanQiaobei
题目解析:
1. 分割长字符串改为多个人名,形成人名字符串数组 v
string s;
cin >> s;
int f = 0;
vector<string> v;
for (int i = 1; i < s.size(); i++) {
if (s[i] <= 'Z' && s[i] >= 'A') {
v.push_back(s.substr(f,i - f));
f = i;
}
}
v.push_back(s.substr(f,s.size() - f)); // 注意最后一个人名特殊处理
2. 对该字符串数组通过二分维系单调队列。
3. 注意输出的答案并非是单调队列,单调队列上的每一个值只是长度为 i 的 LIS 元素结尾最小值,所以要记得将前缀值补充到最后一个字符串上去。(可能有点难懂这句话,详见代码)
int len = 1;
dp[len] = v[0]; ans[len] = v[0];
for (int i = 1; i < v.size(); i++) {
int l = 1,r = len;
int pos = 0;
if (dp[r] < v[i]) pos = ++len;
else {
// 注意一定是用lower_bound() 题目描述是可能重名的
pos = lower_bound(dp + 1,dp + len,v[i]) - dp;
}
dp[pos] = v[i];
ans[pos] = ans[pos - 1] + v[i]; // 记录前缀值,用于最后输出
}
cout << ans[len];
整体代码:
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
string ans[1000005],dp[1000005];
void solve() {
string s;
cin >> s;
int e,f = 0;
vector<string> v;
for (int i = 1; i < s.size(); i++) {
if (s[i] <= 'Z' && s[i] >= 'A') {
v.push_back(s.substr(f,i - f));
f = i;
}
}
v.push_back(s.substr(f,s.size() - f));
int len = 1;
dp[len] = v[0]; ans[len] = v[0];
for (int i = 1; i < v.size(); i++) {
int l = 1,r = len;
int pos = 0;
if (dp[r] < v[i]) pos = ++len;
else {
pos = lower_bound(dp + 1,dp + len,v[i]) - dp;
}
dp[pos] = v[i];
ans[pos] = ans[pos - 1] + v[i];
}
cout << ans[len];
}
int main() {
ios::sync_with_stdio(false);
int T = 1;
while (T--) {
solve();
}
}