dp-LIS LCS 模型

最长上升子序列问题:

https://www.cnblogs.com/sxq-study/p/12303589.html

一:两遍LIS问题

1:题目:

怪盗基德是一个充满传奇色彩的怪盗,专门以珠宝为目标的超级盗窃犯。

而他最为突出的地方,就是他每次都能逃脱中村警部的重重围堵,而这也很大程度上是多亏了他随身携带的便于操作的滑翔翼。

有一天,怪盗基德像往常一样偷走了一颗珍贵的钻石,不料却被柯南小朋友识破了伪装,而他的滑翔翼的动力装置也被柯南踢出的足球破坏了。

不得已,怪盗基德只能操作受损的滑翔翼逃脱。

假设城市中一共有N幢建筑排成一条线,每幢建筑的高度各不相同。

初始时,怪盗基德可以在任何一幢建筑的顶端。

他可以选择一个方向逃跑,但是不能中途改变方向(因为中森警部会在后面追击)。

因为滑翔翼动力装置受损,他只能往下滑行(即:只能从较高的建筑滑翔到较低的建筑)。

他希望尽可能多地经过不同建筑的顶部,这样可以减缓下降时的冲击力,减少受伤的可能性。

请问,他最多可以经过多少幢不同建筑的顶部(包含初始时的建筑)?

输入格式

输入数据第一行是一个整数K,代表有K组测试数据。

每组测试数据包含两行:第一行是一个整数N,代表有N幢建筑。第二行包含N个不同的整数,每一个对应一幢建筑的高度h,按照建筑的排列顺序给出。

输出格式

对于每一组测试数据,输出一行,包含一个整数,代表怪盗基德最多可以经过的建筑数量。

数据范围

1K1001≤K≤100,
1N1001≤N≤100,
0<h<100000<h<10000

输入样例:

3
8
300 207 155 299 298 170 158 65
8
65 158 170 298 299 155 207 300
10
2 1 3 4 5 6 7 8 9 10

输出样例:

6
6
9

代码:

 1 #include <iostream>
 2 #include <algorithm>
 3 
 4 using namespace std;
 5 
 6 const int N = 110;
 7 
 8 int dp[N];
 9 int arr[N];
10 int n;
11 
12 int main(){
13     int t;cin >> t;
14     while(t --){
15         cin >> n;
16         for(int i = 1;i <= n;++i)cin >> arr[i];
17         int ans = 0;
18         //最长上升子序列(LIS)
19         for(int i = 1;i <= n;++i){
20             dp[i] = 1;
21             for(int j = 1;j < i;++j)
22                 if(arr[j] < arr[i])
23                     dp[i] = max(dp[i], dp[j] + 1);
24             ans = max(ans, dp[i]);
25         }
26         //最长下降子序列
27         for(int i = n;i >= 1;--i){
28             dp[i] = 1;
29             for(int j = i+1;j <= n;++j)
30                 if(arr[i] > arr[j])
31                     dp[i] = max(dp[i], dp[j] + 1);
32             ans = max(ans, dp[i]);
33         }
34         cout << ans << endl;
35     }
36     return 0;
37 }
View Code

2:题目:

五一到了,ACM队组织大家去登山观光,队员们发现山上一个有N个景点,并且决定按照顺序来浏览这些景点,即每次所浏览景点的编号都要大于前一个浏览景点的编号。

同时队员们还有另一个登山习惯,就是不连续浏览海拔相同的两个景点,并且一旦开始下山,就不再向上走了。

队员们希望在满足上面条件的同时,尽可能多的浏览景点,你能帮他们找出最多可能浏览的景点数么?

输入格式

第一行包含整数N,表示景点数量。

第二行包含N个整数,表示每个景点的海拔。

输出格式

输出一个整数,表示最多能浏览的景点数。

数据范围

2N10002≤N≤1000

输入样例:

8
186 186 150 200 160 130 197 220

输出样例:

4

代码:

 1 #include <iostream>
 2 #include <algorithm>
 3 
 4 using namespace std;
 5 
 6 const int N = 1010;
 7 
 8 int arr[N];
 9 int dp_up[N], dp_down[N];
10 int n;
11 
12 int main(){
13     cin >> n;
14     for(int i = 1;i <= n;++i)cin>>arr[i];
15     for(int i = 1;i <= n;++i){
16         dp_up[i] = 1;
17         for(int j = 1;j < i;++j)
18             if(arr[i] > arr[j])
19                 dp_up[i] = max(dp_up[i], dp_up[j] + 1);
20     }
21     for(int i = n;i >= 1;--i){
22         dp_down[i] = 1;
23         for(int j = i+1;j <= n;++j)
24             if(arr[i] > arr[j])
25                 dp_down[i] = max(dp_down[i], dp_down[j] + 1);
26     }
27     //最长上升和最长下降子序列的和的最大值就是答案
28     int ans = 0;
29     for(int i = 1;i <= n;++i)
30         ans = max(ans, dp_down[i] + dp_up[i] - 1);
31     cout << ans << endl;
32     return 0;
33 }
View Code

二:一端排序另一端再LIS的问题

 1 题目:
 2 Palmia国有一条横贯东西的大河,河有笔直的南北两岸,岸上各有位置各不相同的N个城市。
 3 
 4 北岸的每个城市有且仅有一个友好城市在南岸,而且不同城市的友好城市不相同。
 5 
 6 每对友好城市都向政府申请在河上开辟一条直线航道连接两个城市,但是由于河上雾太大,政府决定避免任意两条航道交叉,以避免事故。
 7 
 8 编程帮助政府做出一些批准和拒绝申请的决定,使得在保证任意两条航线不相交的情况下,被批准的申请尽量多。
 9 
10 输入格式
11 第1行,一个整数N,表示城市数。
12 
13 第2行到第n+1行,每行两个整数,中间用1个空格隔开,分别表示南岸和北岸的一对友好城市的坐标。
14 
15 输出格式
16 仅一行,输出一个整数,表示政府所能批准的最多申请数。
17 
18 数据范围
19 1≤N≤5000,
20 0≤xi≤10000
21 输入样例:
22 7
23 22 4
24 2 6
25 10 3
26 15 12
27 9 8
28 17 17
29 4 2
30 输出样例:
31 4
32 
33 代码:
34 
35 #include <iostream>
36 #include <algorithm>
37 using namespace std;
38 
39 const int N = 5010;
40 
41 int dp[N];
42 pair<int, int> arr[N];
43 int n;
44 
45 int main(){
46     cin >> n;
47     for(int i = 1;i <= n;++i) cin >> arr[i].first >> arr[i].second;
48     //按照其中一面进行排序
49     sort(arr+1, arr+1+n);
50     int ans = 0;
51     //另一面求个最长上升子序列
52     for(int i = 1;i <= n;++i){
53         dp[i] = 1;
54         for(int j = 1;j < i;++j)
55             if(arr[i].second > arr[j].second)
56                 dp[i] = max(dp[i], dp[j] + 1);
57         ans = max(ans, dp[i]);
58     }
59     cout << ans << endl;
60     return 0;
61 }
View Code

三:最大上升字序和

题目:

一个数的序列 bibi,当 b1<b2<<bSb1<b2<…<bS 的时候,我们称这个序列是上升的。

对于给定的一个序列(a1,a2,,aNa1,a2,…,aN),我们可以得到一些上升的子序列(ai1,ai2,,aiKai1,ai2,…,aiK),这里1i1<i2<<iKN1≤i1<i2<…<iK≤N。

比如,对于序列(1,7,3,5,9,4,8),有它的一些上升子序列,如(1,7),(3,4,8)等等。

这些子序列中和最大为18,为子序列(1,3,5,9)的和。

你的任务,就是对于给定的序列,求出最大上升子序列和。

注意,最长的上升子序列的和不一定是最大的,比如序列(100,1,2,3)的最大上升子序列和为100,而最长上升子序列为(1,2,3)。

输入格式

输入的第一行是序列的长度N。

第二行给出序列中的N个整数,这些整数的取值范围都在0到10000(可能重复)。

输出格式

输出一个整数,表示最大上升子序列和。

数据范围

1N10001≤N≤1000

输入样例:

7
1 7 3 5 9 4 8

输出样例:

18

代码:

 1 #include <iostream>
 2 #include <algorithm>
 3 using namespace std;
 4 
 5 const int N = 1010;
 6 int n;
 7 int dp[N];
 8 int arr[N];
 9 
10 int main(){
11     cin >> n;
12     for(int i = 1;i <= n;++i)cin >> arr[i];
13     int ans = 0;
14     for(int i = 1;i <= n;++i){
15         dp[i] = arr[i];
16         for(int j = 1;j < i;++j)
17             if(arr[i] > arr[j])
18                 dp[i] = max(dp[i], dp[j] + arr[i]);
19         ans = max(ans, dp[i]);
20     }
21     cout << ans;
22     return 0;
23 }
View Code

相关习题:

https://leetcode-cn.com/problems/maximum-length-of-pair-chain/

代码:

 1 class Solution {
 2 public:
 3     int findLongestChain(vector<vector<int>>& pairs) {
 4         //方法一,动态规划(最长上升子序列思路)
 5         /*
 6         sort(pairs.begin(), pairs.end());//这里排序是因为保证以当前数对为结尾的最长数对只会产生在当前数对的左面
 7         vector<int> dp(pairs.size()+10, 1);//从起点到当前数对的最长上升个数
 8         int ans = 0;
 9         for(int i = 1;i < pairs.size();++i){
10             for(int j = 0;j < i;++j){ 
11                 if(pairs[j][1] < pairs[i][0])
12                     dp[i] = max(dp[i], dp[j] + 1);
13             }
14             ans = max(ans, dp[i]);
15         }
16         return ans;
17         */
18         //方法二,贪心(区间贪心问题)
19         //排序为了找到小于当前数对左端点的第一个数
20         sort(pairs.begin(), pairs.end(), [](const auto& a, const auto& b){
21             return a[1] < b[1];
22         });
23         int ans = 0;
24         int cur = INT_MIN;//cur是当前上升数对的右端点
25         for(auto t : pairs){
26             if(t[0] > cur){
27                 ++ans;
28                 cur = t[1];
29             }
30         }
31         return ans;
32     }
33 };
View Code

四:求出最少的上升或者下降的子序列的数目

题目:

某国为了防御敌国的导弹袭击,发展出一种导弹拦截系统。

但是这种导弹拦截系统有一个缺陷:虽然它的第一发炮弹能够到达任意的高度,但是以后每一发炮弹都不能高于前一发的高度。

某天,雷达捕捉到敌国的导弹来袭。

由于该系统还在试用阶段,所以只有一套系统,因此有可能不能拦截所有的导弹。

输入导弹依次飞来的高度(雷达给出的高度数据是不大于30000的正整数,导弹数不超过1000),计算这套系统最多能拦截多少导弹,如果要拦截所有导弹最少要配备多少套这种导弹拦截系统。

输入格式

共一行,输入导弹依次飞来的高度。

输出格式

第一行包含一个整数,表示最多能拦截的导弹数。

第二行包含一个整数,表示要拦截所有导弹最少要配备的系统数。

输入样例:

389 207 155 300 299 170 158 65

输出样例:

6
2

思路分析


1:结论:最长上升子序列的数目等于最少的下降子序列数目(本文皆指严格上升和下降),同理,最长下降子序列的数目等于最少的上升子序列的数目
2:如何求出最少的上升或者下降子序列的数目,我们就可以采用贪心的方法了
(1):最少的上升子序列的数目:我们可以构造一个数组假设已经存下所有上升子序列的每一个末尾的值(比如 1 2 3,存的就是3),这时,当我们新遍历一个元素时,如果说当前元素值小于等于数组末尾值,那么这个元素就是一个单独的上升子序列,所以我们在数组末尾新增加以这个元素为结尾的子序列,否则当前元素值大于数组末尾值,那么我们可以通过二分的方法找到第一个比当前元素小的值(这种贪心的方法需要自己仔细分析一下,很容易得出),并用当前元素更新找到的那个值,也就是相当于把当前元素放在了找到的那个子序列的后面,最后数组的大小就是结果。
(2):最少的下降子序列数目:同上,具体可以看下面的代码,有详细的注释


代码:

 1 #include <iostream>
 2 #include <vector>
 3 #include <algorithm>
 4 using namespace std;
 5 
 6 vector<int> arr;
 7 vector<int> tail;
 8 
 9 int main(){
10     int x;
11     while(cin >> x) arr.push_back(x);
12     //第一个答案是最长下降子序列 等价于 二分的方法就是找到的最少上升子序列的数量
13     tail.push_back(-0x3f3f3f3f);
14     for(int i = 0;i < arr.size();++i){
15         //此处的tail是一个单调下降的数组
16         if(arr[i] <= tail.back())//此处有等号,要严格满足上升子序列,除非题意可以取得等号
17             tail.push_back(arr[i]);
18         else{
19             //找到第一个比a[i] 小的
20             int l = 0, r = tail.size()-1;
21             while(l < r){
22                 int mid = l + r>> 1;
23                 if(tail[mid] < arr[i]) r = mid;
24                 else l = mid + 1;
25             }
26             tail[l] = arr[i];
27         }
28     }
29     cout << tail.size() << endl;
30 
31     //第二个答案是最少的下降子序列的数目 等价于 最长上升子序列的数目
32     tail = vector<int>();
33     tail.push_back(0x3f3f3f3f);
34     for(int i = 0;i < arr.size();++i){
35         //此处的tail是一个单调上升的数组
36         if(arr[i] > tail.back())//此处没有等号因为题意
37             tail.push_back(arr[i]);
38         else{
39             //找到第一个比a[i] 大的
40             int l = 0, r = tail.size()-1;
41             while(l < r){
42                 int mid = l + r >> 1;
43                 if(tail[mid] >= arr[i]) r = mid;
44                 else l = mid + 1;
45             }
46             tail[l] = arr[i];
47         }
48     }
49     cout << tail.size() << endl;
50     return 0;
51 }
View Code

五:最少的上升和下降的子序列的和

题目:

为了对抗附近恶意国家的威胁,R国更新了他们的导弹防御系统。

一套防御系统的导弹拦截高度要么一直 严格单调 上升要么一直 严格单调 下降。

例如,一套系统先后拦截了高度为3和高度为4的两发导弹,那么接下来该系统就只能拦截高度大于4的导弹。

给定即将袭来的一系列导弹的高度,请你求出至少需要多少套防御系统,就可以将它们全部击落。

输入格式

输入包含多组测试用例。

对于每个测试用例,第一行包含整数n,表示来袭导弹数量。

第二行包含n个不同的整数,表示每个导弹的高度。

当输入测试用例n=0时,表示输入终止,且该用例无需处理。

输出格式

对于每个测试用例,输出一个占据一行的整数,表示所需的防御系统数量。

数据范围

1n501≤n≤50

输入样例:

5
3 5 2 4 1
0 

输出样例:

2

样例解释

对于给出样例,最少需要两套防御系统。

一套击落高度为3,4的导弹,另一套击落高度为5,2,1的导弹。

思路分析

本题和上一题的区别是,上一题求出最少的上升或者下降的子序列即可,而本题要求出最少的上升和下降的子序列的和,故本题才用的方法和上题不同,本题比较如果能想出序列中任意一个元素要么属于上升序列,要么属于下降序列,那么就可以很容易的想到暴力的方法(枚举所有方案)得出最优解。

但是如何优化呢,首先每个元素要么属于上升子序列要么属于下降子序列,这点是没法优化的,优化的地方在于属于上升或者下降的子序列时,如何快速找出属于哪个子序列,这点就可以用上一题的思想了,就是用贪心的方法, 以上升子序列为例,插入到小于当前元素的所有上升子序列中末尾元素最大的那个就是最优的,值得我们注意的地方就是up数组(所有上升子序列的末尾元素组成的)是单调下降的,这点自己列举几个数分析一下就可以看出来

代码:

#include <iostream>
#include <algorithm>
#include <vector>

using namespace std;

const int N = 60;

int arr[N];
int up[N], down[N];
int n;
int ans;

void dfs(int index, int u, int d){
    if(index == n + 1){
        ans = min(ans, u + d);
        return;
    }
    if(u + d >= ans) return;//提前回溯
    //放在up里, up是单调下降的
    if(arr[index] < up[u]) {
        up[++u] = arr[index];
        dfs(index+1, u, d);
        --u;
    }
    else{
        for(int i = 1;i <= u;++i){
            if(arr[index] > up[i]){
                int t = up[i];
                up[i] = arr[index];
                dfs(index+1, u, d);
                up[i] = t;
                break;
            }
        }
    }
    //放在down里,down是单调上升的
    if(arr[index] > down[d]){
        down[++d] = arr[index];
        dfs(index+1, u, d);
        --d;
    }else{
        for(int i = 1;i <= d;++i){
            if(arr[index] < down[i]){
                int t = down[i];
                down[i] = arr[index];
                dfs(index+1, u, d);
                down[i] = t;
                break;
            }
        }
    }
    return;
}

int main(){
    while(cin >> n, n){
        for(int i = 1;i <= n;++i) cin >> arr[i];
        up[0] = 0x3f3f3f3f, down[0] = -0x3f3f3f3f;
        ans = 100;
        dfs(1, 0, 0);        
        cout << ans << endl;
    }    
}
View Code

六:LICS问题(最长的公共上升子序列)

题目:

熊大妈的奶牛在小沐沐的熏陶下开始研究信息题目。

小沐沐先让奶牛研究了最长上升子序列,再让他们研究了最长公共子序列,现在又让他们研究最长公共上升子序列了。

小沐沐说,对于两个数列A和B,如果它们都包含一段位置不一定连续的数,且数值是严格递增的,那么称这一段数是两个数列的公共上升子序列,而所有的公共上升子序列中最长的就是最长公共上升子序列了。

奶牛半懂不懂,小沐沐要你来告诉奶牛什么是最长公共上升子序列。

不过,只要告诉奶牛它的长度就可以了。

数列A和B的长度均不超过3000。

输入格式

第一行包含一个整数N,表示数列A,B的长度。

第二行包含N个整数,表示数列A。

第三行包含N个整数,表示数列B。

输出格式

输出一个整数,表示最长公共子序列的长度。

数据范围

1N30001≤N≤3000,序列中的数字均不超过2311231−1

输入样例:

4
2 2 1 3
2 1 2 3

输出样例:

2

思路分析:

这道题目是AcWing 895. 最长上升子序列和AcWing 897. 最长公共子序列的结合版,在状态表示和状态计算上都是融合了这两道题目的方法。

状态表示:

f[i][j]代表所有a[1 ~ i]和b[i ~ j]中以b[j]结尾的公共上升子序列的集合;

f[i][j]的值等于该集合的子序列中长度的最大值;

状态计算(对应集合划分):

首先依据公共子序列中是否包含a[i],将f[i][j]所代表的集合划分成两个不重不漏的子集:

不包含a[i]的子集,最大值是f[i - 1[[j];

包含a[i]的子集,将这个子集继续划分,依据是子序列的倒数第二个元素在b[]中是哪个数:

子序列只包含b[j]一个数,长度是1;

子序列的倒数第二个数是b[1]的集合,最大长度是f[i - 1][1] + 1;

子序列的倒数第二个数是b[j - 1]的集合,最大长度是f[i - 1][j - 1] + 1;

参考文章

https://www.geek-share.com/detail/2678442635.html

代码:

 1 #include <iostream>
 2 #include <algorithm>
 3 
 4 using namespace std;
 5 
 6 const int N = 3010;
 7 
 8 int a[N], b[N];
 9 int dp[N][N];//从起点到i,从起点到j并且以b[j]结尾的最大LICS
10 int n;
11 
12 int main(){
13     cin >> n;
14     for(int i = 1;i <= n;++i) cin >> a[i];
15     for(int i = 1;i <= n;++i) cin >> b[i];
16     int ans = 0;
17     //下面是n^3的代码,帮助理解
18     /*
19     for(int i = 1;i <= n;++i){
20         for(int j = 1;j <= n;++j){
21             //不包含 a[i]
22             dp[i][j] = dp[i-1][j];
23             //包含
24             if(a[i] == b[j]){
25                 //此时至少有一个LICS
26                 dp[i][j] = max(1, dp[i][j]);
27                 for(int k = 1;k < j;++k)
28                     if(b[j] > b[k])
29                         dp[i][j] = max(dp[i][j], dp[i-1][k] + 1);
30             }
31             ans = max(ans, dp[i][j]);
32         }
33     }
34     */
35     //接下来我们优化成n^2
36     for(int i = 1;i <= n;++i){
37         int maxv = 0;//从 dp[i-1][1] 到 dp[i-1][j - 1] 的前缀最大值
38         for(int j = 1;j <= n;++j){
39             if(a[i] == b[j])
40                 dp[i][j] = maxv + 1;
41             else
42                 dp[i][j] = dp[i-1][j];
43             if(a[i] > b[j])
44                 maxv = max(maxv, dp[i-1][j]);//这步理解不了
45             ans = max(ans, dp[i][j]);
46         }
47     }
48     cout << ans << endl;
49     return 0;
50 }
View Code

七:最长递增子序列的个数

原题链接:

https://leetcode-cn.com/problems/number-of-longest-increasing-subsequence/

题解链接:

https://leetcode-cn.com/problems/longest-consecutive-sequence/solution/tao-lu-jie-jue-zui-chang-zi-xu-lie-deng-yi-lei-wen/

代码:

 1 class Solution {
 2 public:
 3     int findNumberOfLIS(vector<int>& nums) {
 4         vector<int> dp(nums.size()+10, 0);
 5         vector<int> count(nums.size()+10, 0);
 6         int maxv = 0;
 7         for(int i = 0;i < nums.size();++i){
 8             dp[i] = 1;
 9             count[i] = 1;
10             for(int j = 0;j < i;++j){
11                 if(nums[j] < nums[i]){
12                     if(dp[j] + 1 > dp[i]){
13                         dp[i] = dp[j] + 1;
14                         count[i] = count[j];
15                     }else if(dp[j] + 1 == dp[i])
16                         count[i] += count[j];
17                 }
18             }
19             maxv = max(maxv, dp[i]);
20         }
21         int ans = 0;
22         for(int i = 0;i < nums.size();++i)
23             if(dp[i] == maxv)
24                 ans += count[i];
25         return ans;
26     }
27 };
View Code
原文地址:https://www.cnblogs.com/sxq-study/p/12384721.html