LIS(最长上升子序列)题目汇总
kuangbin带你飞专题十二 基础DP1 题解+总结 一个大佬整理的
萌新又来写记录了
LIS是一个很重要的DP应用~在很多题里都可以用上,很多问题可以直接或间接的转化为LIS问题的求解。
尽量都用nlogn的算法(单调队列)写~但是不代表不使用n2算法啦 (数据非常小,不只是要求LIS的长度,还有其他要求的(如输出路径,求和等,用n2会简单),考虑n2算法)
放一个模板nlogn:
#include<cstdio>
#include<algorithm>
using namespace std;
int t,n,len,book[40005],ans[40005];
int binary_search(int x){
int left = 1, right = len, mid;
while(left <= right){ //如果这里带着等号,下面right = mid - 1,自己画图看
mid = (right + left) / 2;
if(ans[mid] >= x) right = mid - 1;
else left = mid + 1;
}
return left;
}
int main(){
scanf("%d",&t);
while(t--){
scanf("%d",&n);
for(int i = 1; i <= n; ++i)
scanf("%d",&book[i]);
ans[1] = book[1], len = 1;
for(int i = 2; i <= n; ++i){
if(book[i] > ans[len]) ans[++len] = book[i];
else{
// int pos = binary_search(book[i]); 容易乱,调用库函数
*lower_bound(ans + 1, ans + 1 + len, book[i]) = book[i];
//ans[pos] = book[i];
}
}
printf("%d\n",len);
}
return 0;
}
二分的函数可以自己写(如上面),也可以调用库函数lower_bound(),upper_bound();具体用哪个要看是严格单调增还是不严格单调增(即:可不可以有等号)。同理也可以求最长下降子序列/不递增子序列,可以选择重载函数内的比较器(greater<…>),也可以选择倒着算。
再放一个n2算法的模板8!
#include<cstdio>
#include<algorithm>
using namespace std;
const int maxn = 1005;
int n,book[maxn],dp[maxn];
int main(){
scanf("%d",&n);
for(int i = 1; i <= n; ++i)
scanf("%d",&book[i]);
int res = 0;
for(int i = 1; i <= n; ++i){
dp[i] = 1;
for(int j = 1; j < i; ++j){
if(book[j] < book[i])
dp[i] = max(dp[i], dp[j] + 1);
}
res = max(res, dp[i]);
}
printf("%d",res);
return 0;
}
再放一个nlogn输出路径的模板8!(G题)
#include<cstdio>
#include<algorithm>
using namespace std;
const int maxn = 1005;
int cnt,po,n,ans[maxn],pos[maxn],list[maxn],book[maxn];
int main(){
scanf("%d",&n);
for(int i = 1;i <= n;i++) scanf("%d",&book[i]);
ans[++cnt] = book[1], pos[++po] = 1;
for(int i = 2;i <= n;i++){
if(ans[cnt] < book[i]){
ans[++cnt] = book[i];
pos[++po] = cnt; //用来记录从a[i]中1到n每个位置的在dp中的位置
}else{
int tmp = lower_bound(ans + 1, ans + 1 + cnt,book[i]) - ans;
ans[tmp] = book[i];
pos[++po] = tmp;
}
}
int t = cnt;
for(int i = n;i >= 1;i--){
if(pos[i] == t)
list[t--] = book[i];
if (t<1) break;
}
for(int i = 1;i <= cnt;i++)
printf("%d\n",list[i]);
return 0;
}
A.POJ2533(Longest Ordered Subsequence)
LIS的模板题。严格单调递增。
#include<cstdio>
#include<algorithm>
using namespace std;
int n,book[1005],ans[1005],cnt;
int main(){
while(~scanf("%d",&n)){
for(int i = 1; i <= n; ++i)
scanf("%d",&book[i]);
ans[1] = book[1], cnt = 1;
for(int i = 2; i <= n; ++i){
if(book[i] > ans[cnt]) ans[++cnt] = book[i];
else *lower_bound(ans + 1, ans + 1 + cnt, book[i]) = book[i];
}
printf("%d\n",cnt);
}
return 0;
}
B.POJ 1836 Alignment / 洛谷P1091 合唱队形
tips:求删除最少的数,使得从序列中任取一个数h[i],有h[1] ~ h[i]严格单增,或h[i] ~ h[n]严格单减。(一个山峰的形状)。
思路:正向/反向求LIS
nlogn的单调队列里面存的是长度为len的LIS的末尾是几,而不是LIS本身,也不是选到该位置时LIS的最大长度。所以需要用一个数组d来记录选到当前位置时,LIS的最大值。
处理完后,在两层for循环枚举d数组。
P.S.这两个题几乎完全一样,就是数据类型不同。
POJ1836:
#include<cstdio>
#include<algorithm>
using namespace std;
int n,cnt1,cnt2,d1[1005],d2[1005];
double ans1[1005],ans2[1005],book[1005];
int main(){
scanf("%d",&n);
for(int i = 1; i <= n; ++i)
scanf("%lf",&book[i]);
ans1[1] = book[1], cnt1 = 1, d1[1] = 1;
for(int i = 2; i <= n; ++i){
if(book[i] > ans1[cnt1]) ans1[++cnt1] = book[i], d1[i] = cnt1;
else *lower_bound(ans1 + 1, ans1 + 1 + cnt1, book[i]) = book[i], d1[i] = cnt1;
}
ans2[1] = book[n], cnt2 = 1, d2[n] = 1;
for(int i = n - 1; i >= 1; --i){
if(book[i] > ans2[cnt2]) ans2[++cnt2] = book[i], d2[i] = cnt2;
else *lower_bound(ans2 + 1, ans2 + 1 + cnt2, book[i]) = book[i], d2[i] = cnt2;
}
int cnt = 0;
for(int i = 1; i <= n; ++i)
for(int j = i + 1; j <= n; ++j)
cnt = max(cnt, d1[i] + d2[j]);
printf("%d",n - cnt);
return 0;
}
合唱队形:
#include<cstdio>
#include<algorithm>
using namespace std;
int n,cnt1,cnt2,d1[1005],d2[1005];
int ans1[1005],ans2[1005],book[1005];
int main(){
scanf("%d",&n);
for(int i = 1; i <= n; ++i)
scanf("%d",&book[i]);
ans1[1] = book[1], cnt1 = 1, d1[1] = 1;
for(int i = 2; i <= n; ++i){
if(book[i] > ans1[cnt1]) ans1[++cnt1] = book[i], d1[i] = cnt1;
else *lower_bound(ans1 + 1, ans1 + 1 + cnt1, book[i]) = book[i], d1[i] = cnt1;
}
ans2[1] = book[n], cnt2 = 1, d2[n] = 1;
for(int i = n - 1; i >= 1; --i){
if(book[i] > ans2[cnt2]) ans2[++cnt2] = book[i], d2[i] = cnt2;
else *lower_bound(ans2 + 1, ans2 + 1 + cnt2, book[i]) = book[i], d2[i] = cnt2;
}
int cnt = 0;
for(int i = 1; i <= n; ++i)
for(int j = i + 1; j <= n; ++j)
cnt = max(cnt, d1[i] + d2[j]);
printf("%d",n - cnt);
return 0;
}
C.Wooden Sticks POJ - 1065 / 洛谷P1233 木棍加工
两个题几乎完全一样~关键在于如何转化问题。
木棍的长度、宽度有两个属性,实际就是定义了一个偏序关系。(这个题也可以转化成矩形的完全覆盖问题)~
显然 ,先根据长度从高到低排序,如果长度相同,再根据宽度从高到低排序。如果是在同一次准备周期里面,前面的木棍一定在后面木棍之前被加工。
这样,这个问题就转化成了在n个数中,求不下降子序列最少个数。
根据dilworth定理,不上升子序列最小个数等于最长上升子序列的长度。
补充一个离散数学的定理:Dilworth(bu shuo ren hua 迪尔沃斯)定理:偏序集的最少反链划分数等于最长链的长度(??). 就是说: **不上升子序列的个数等于最长上升子序列的长度。反之亦然。**可以看下面D题。
问题又简化成求n个数的最大上升子序列。
#include<bits/stdc++.h>
using namespace std;
struct wood{
int a,b;
}stick[10005];
int cmp(wood w1, wood w2){
return (w1.a != w2.a) ? w1.a > w2.a : w1.b > w2.b ;
}
int n,ans[10005],cnt,t;
int main(){
scanf("%d",&t);
while(t--){
scanf("%d",&n);
for(int i = 1; i <= n; ++i)
scanf("%d%d",&stick[i].a,&stick[i].b);
sort(stick + 1, stick + 1 + n, cmp);
ans[1] = stick[1].b, cnt = 1;
for(int i = 1; i <= n; ++i){
if(stick[i].b > ans[cnt]) ans[++cnt] = stick[i].b;
else *lower_bound(ans + 1, ans + 1 + cnt, stick[i].b) = stick[i].b;
}
printf("%d\n",cnt);
}
return 0;
}
D.洛谷P1020 导弹拦截/最少拦截系统 HDU - 1257
tips:依然是d-w定理的应用。经典题。
“第二个数字表示如果要拦截所有导弹最少要配备多少套这种导弹拦截系统”就是求一共的不下降子序列的个数->转化为求最长上升子序列的长度。
要注意upper/lower的选择!!!!
导弹拦截:
#include<bits/stdc++.h>
using namespace std;
int n,book[100005], ans[100005],cnt,num,cnt2;
int main(){
while(~scanf("%d",&book[++num])); num--;
ans[++cnt] = book[1];
for(int i = 2; i <= num; ++i){
if(ans[cnt] >= book[i]) ans[++cnt] = book[i];
else *upper_bound(ans + 1, ans + 1 + cnt, book[i], greater<int>()) = book[i];
}
ans[++cnt2] = book[1];
for(int i = 2; i <= num; ++i){
if(ans[cnt2] < book[i]) ans[++cnt2] = book[i];
else *lower_bound(ans + 1, ans + 1 + cnt2, book[i]) = book[i];
}
printf("%d\n%d",cnt,cnt2);
return 0;
}
最少拦截系统
#include<cstdio>
#include<algorithm>
using namespace std;
int book[10005],n,cnt,ans[10005];
int main(){
while(~scanf("%d",&n)){
for(int i = 1; i <= n; ++i)
scanf("%d",&book[i]);
cnt = 0, ans[++cnt] = book[1];
for(int i = 2; i <= n; ++i){
if(book[i] > ans[cnt]) ans[++cnt] = book[i];
else *lower_bound(ans + 1, ans + 1 + cnt, book[i]) = book[i];
}
printf("%d\n",cnt);
}
return 0;
}
一个数列划分成最少的最长不升子序列的数目就等于这个数列的最长上升子序列的长度。
为什么呢?有一种简单的理解方式:
对于最长上升子序列中的每个元素,两两不能分为一组,因此该序列的长度(length)为多少,就至少应划分多少不升子序列。
反证法,若划分length个不升子序列无法满足题意。则必又存在一个该序列之外的元素与该序列的元素两两不能分为一组,而两两不能分为一组的条件是这个元素与该序列的元素构成上升子序列。所以,这个“最长上升子序列”的长度并非最长,矛盾,即知一个数列划分成最少的最长不升子序列的数目就等于这个数列的最长上升子序列的长度。
E.Dining Cows POJ - 3671
tips:给你一串只有1,2的数字,让你改变最少的次数,让这个序列变成非递减的。思路就是求最长不下降子序列长度,剩下的全改就是了~
#include<cstdio>
#include<algorithm>
using namespace std;
int n,book[30005],ans[30005],cnt;
int main(){
scanf("%d",&n);
for(int i = 1; i <= n; ++i)
scanf("%d",&book[i]);
ans[++cnt] = book[1];
for(int i = 2; i <= n; ++i){
if(ans[cnt] <= book[i]) ans[++cnt] = book[i];
else *upper_bound(ans + 1, ans + 1 + cnt, book[i]) = book[i];
}
printf("%d",n - cnt);
return 0;
}
F.Super Jumping! Jumping! Jumping! HDU - 1087
tips:要求最长 上升子序列的最大和(注意不一定是最长上升子序列! 如这组数据: 1 2 9 3 4 答案是12, 而不是10)
这种题还是用n2算法8!更改一下dp数组的含义。(原来dp[i]表示从前到目前最长的LIS的长度,现在变成到目前上升子序列的最大和~)
数组名是girl是因为原题是要攻略小姐姐
#include<cstdio>
#include<algorithm>
using namespace std;
int n,girl[1005],dp[1005],ans;
int main(){
while(~scanf("%d",&n) && n){
for(int i = 1; i <= n; ++i){
scanf("%d",&girl[i]);
dp[i] = girl[i];
}
ans = 0;
for(int i = 1; i <= n; ++i){
for(int j = 1; j < i; ++j){
if(girl[i] > girl[j])
dp[i] = max(dp[i], dp[j] + girl[i]);
}
ans = max(ans, dp[i]);
}
printf("%d\n",ans);
}
return 0;
}
G.FatMouse’s Speed HDU - 1160(很重要的一个题!输出路径)
tips:找出最长的序列,使得:老鼠的体重严格递增,且速度严格递减~
排序后用LIS做。但是!!这个题需要 输出路径!
学习一下单调队列nlogn输出路径的方法:放一个大佬的解释8
LIS nlogn优化以及路径输出
路径输出——倒序输出(ans的长度就是最长子序列的长度),从cnt到1,t–,找到第一个pos[i]=t的地方输出。(这里必须是第一个,否则有可能不是LIS了(倒着遍历,第一个pos[i]=t的位置一定是最小的(这也是ans数组的含义)))
用pos记录位置,用list记录路径~ 这里也只能输出一条路径(一般这种题会special judge)
模板见上面~
本题只需要先按体重递增排序,体重相同再速度递减排序即可转化为关于速度的最长下降子序列(重载lower_bound函数)。
注意体重严格递增是前提,体重相同但是速度递减是不可以放在一起构成LIS的!
#include<cstdio>
#include<algorithm>
#include<functional>
using namespace std;
struct mouse{
int wei,speed, id;
}book[1005];
int n,speed,wei,cnt,ans[1005],list[1005],pos[1005],po,lastwei;
int cmp(mouse m1, mouse m2){
return (m1.wei != m2.wei) ? m1.wei < m2.wei : m1.speed > m2.speed;
}
int main(){
while(~scanf("%d%d",&wei,&speed)) book[++n].speed = speed, book[n].wei = wei, book[n].id = n;
sort(book + 1, book + n + 1, cmp);
/* for(int i = 1; i <= n; ++i)
printf("%d %d %d\n",book[i].wei,book[i].speed,book[i].id);*/
ans[++cnt] = book[1].speed, pos[++po] = 1, lastwei = book[1].wei;
for(int i = 2; i <= n; ++i){
//if(lastwei == book[i].wei) continue; 不可以这样写哦
if(ans[cnt] > book[i].speed && book[i].wei > lastwei) ans[++cnt] = book[i].speed, pos[++po] = cnt;
else {
int temp = lower_bound(ans + 1, ans + 1 + cnt, book[i].speed, greater<int>()) - ans;
ans[temp] = book[i].speed;
pos[++po] = temp;
}
lastwei = book[i].wei;
}
int t = cnt;
for(int i = n; i >= 1;i--){
if(pos[i] == t){
list[t--] = book[i].id;
}
if (t < 1) break;
}
printf("%d\n",cnt);
for(int i = 1;i <= cnt;i++) printf("%d\n",list[i]);
return 0;
}
不过就这个题,还是n2算法求路径更方便一些~
n2错误代码:
#include<cstdio>
#include<algorithm>
#include<functional>
using namespace std;
struct mouse{
int wei,speed, id;
}book[1005];
int n,speed,wei,ans,dp[1005];
int cmp(mouse m1, mouse m2){
return (m1.wei != m2.wei) ? m1.wei < m2.wei : m1.speed > m2.speed;
}
int main(){
while(~scanf("%d%d",&wei,&speed)) book[++n].speed = speed, book[n].wei = wei, book[n].id = n;
sort(book + 1, book + 1 + n, cmp);
for(int i = 1; i <= n; ++i){
dp[i] = 1;
for(int j = 1; j < i; ++j)
if(book[j].wei < book[i].wei && book[j].speed > book[i].speed) dp[i] = max(dp[i], dp[j] + 1);
ans = max(ans, dp[i]);
}
printf("%d\n",ans);
int idx = 1;
for(int i = 1; i <= n; ++i){
if(dp[i] == idx++)
printf("%d\n",book[i].id);
}
return 0;
}
n2正确代码:
#include<cstdio>
#include<algorithm>
#include<functional>
using namespace std;
struct mouse{
int wei,speed, id;
}book[1005];
int n,speed,wei,ans,dp[1005];
int cmp(mouse m1, mouse m2){
return (m1.wei != m2.wei) ? m1.wei < m2.wei : m1.speed > m2.speed;
}
int main(){
while(~scanf("%d%d",&wei,&speed)) book[++n].speed = speed, book[n].wei = wei, book[n].id = n;
sort(book + 1, book + 1 + n, cmp);
for(int i = n; i >= 1; --i){
dp[i] = 1;
for(int j = i + 1; j <= n; ++j)
if(book[j].wei > book[i].wei && book[j].speed < book[i].speed) dp[i] = max(dp[i], dp[j] + 1);
ans = max(ans, dp[i]);
}
printf("%d\n",ans);
for(int i = 1; i <= n; ++i){
if(dp[i] == ans){
ans--;
printf("%d\n",book[i].id);
}
}
return 0;
}
想要正着输出路径,就必然要倒着dp~(这也是第一个代码错误的原因)
举个栗子:1 2 6 3 4
正向DP : 1 2 3 3 4
正向DP+正着输出的路径是1 2 6 4,显然不是LIS。
倒着DP: 4 3 1 2 1
反向DP+正着输出的路径是1 2 3 4,是想要的答案
所以采取的策略是:反向DP+正向输出
未完待续ing……