动态规划初步--最长上升子序列(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。由于定义的是结束位置,所以记录的是逆序,用栈倒过来再输出就行。

代码一:

 1 #include<stdio.h>
 2 #include<iostream>
 3 #include<stack>
 4 #include<algorithm>
 5 using namespace std;
 6 
 7 const int INF = 0x3f3f3f3f;
 8 const int maxn = 1000 + 10;
 9 int n;
10 int a[maxn], dp[maxn];    //dp[i]表示以i结束的最长上升子序列
11 int nextp[maxn];        //记录路径
12 int res = 0;
13 int first = 0;
14 
15 void print_ans()
16 {
17     stack<int>s;
18     for (int i = 0; i < res; i++)
19     {
20         s.push(first + 1);
21         first = nextp[first];
22     }
23     for (int i = 0; i < res; i++)
24     {
25         int tmp = s.top(); s.pop();
26         printf("%d ", tmp);
27     }
28     printf("\n%d\n", res);
29 }
30 
31 void slove()
32 {
33     memset(dp, 0, sizeof(dp));
34     memset(nextp, 0, sizeof(nextp));
35     res = 0; first = 0;                //记得清零
36     for (int i = 0; i < n; i++)
37     {
38         dp[i] = 1;
39         for (int j = 0; j < i; j++)
40         {
41             if (a[j] < a[i])
42             {
43                 if (dp[j] + 1 > dp[i])
44                 {
45                     dp[i] = dp[j] + 1;
46                     nextp[i] = j;
47                 }
48             }
49         }
50         if (dp[i] > res)
51         {
52             res = dp[i];
53             first = i;
54         }
55     }
56     print_ans();
57 }
58 
59 int main()
60 {
61     while (scanf("%d", &n) == 1 && n)
62     {
63         for (int i = 0; i < n; i++)
64             scanf("%d", &a[i]);
65         slove();
66     }
67     return 0;
68 }

代码二:两者只是输出的处理不同,用逆向寻路,不需要额外的空间,但要消耗一些时间。

 1 #include<stdio.h>
 2 #include<iostream>
 3 #include<stack>
 4 #include<vector>
 5 #include<algorithm>
 6 using namespace std;
 7 
 8 const int INF = 0x3f3f3f3f;
 9 const int maxn = 1000 + 10;
10 int n;
11 int a[maxn], dp[maxn];    //dp[i]表示以i结束的最长上升子序列
12 stack<int>sta;
13 
14 void print_ans(int s)
15 {
16     sta.push(s + 1);
17     if (dp[s] == 1)
18     {
19         while (!sta.empty())
20         {
21             int tmp = sta.top(); sta.pop();
22             printf("%d ", tmp);
23         }
24         return;
25     }
26     for (int i = 0; i < n; i++)
27     {
28         if (a[i] < a[s] && dp[s] == dp[i] + 1)
29         {
30             print_ans(i);
31             break;
32         }
33     }
34 }
35 void slove()
36 {
37     memset(dp, 0, sizeof(dp));
38     int res = 0;
39     int first = 0;
40     stack<int>s;
41     for (int i = 0; i < n; i++)
42     {
43         dp[i] = 1;
44         for (int j = 0; j < i; j++)
45         {
46             if (a[j] < a[i])
47             {
48                 dp[i] = max(dp[i],dp[j] + 1);
49             }
50         }
51         if (dp[i] > res)
52         {
53             res = dp[i];
54             first = i;            //first记录最长上升子序列的最后元素的位置
55         }
56     }
57 
58     print_ans(first);
59     printf("\n%d\n", res);
60 }
61 
62 int main()
63 {
64     while (scanf("%d", &n) == 1 && n)
65     {
66         for (int i = 0; i < n; i++)
67             scanf("%d", &a[i]);
68         slove();
69     }
70     return 0;
71 }

2、用dp[i]表示以i开始的最长上升子序列,初始化同样是dp[i] = 0。同时由于定义的是开始位置,可以直接输出最长上升子序列。

 1 void print_ans()
 2 {
 3     for (int i = 0; i < res; i++)
 4     {
 5         printf("%d ", first + 1);
 6         first = nextp[first];
 7     }
 8     printf("\n%d\n", res);
 9 }
10 
11 void slove()
12 {
13     res = 0;        //定义成全局变量时,注意清零
14     for (int i = n - 1;i >= 0; i--)
15     {
16         dp[i] = 1;
17         for (int j = i + 1; j < n; j++)
18         {
19             if (a[i] < a[j])
20             {
21                 if (dp[j] + 1 > dp[i])
22                 {
23                     dp[i] = dp[j] + 1;
24                     nextp[i] = j;
25                 }
26             }
27         }
28         if (dp[i] > res)
29         {
30             res = dp[i];
31             first = i;
32         }
33     }
34     print_ans();
35 }

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只需用来更来一次。

n个元素,每次查找logn,总的时间复杂度为O(nlogn).

 1 void slove()
 2 {
 3     fill(dp, dp + n, INF);
 4     for (int i = 0; i < n; i++)
 5     {
 6         *upper_bound(dp, dp + n, a[i]) = a[i];
 7     }
 8     int first = lower_bound(dp, dp + n, INF) - dp;
 9     printf("%d\n", dp[first - 1]);
10     printf("%d\n", first);
11 }

打印结果路径似乎不太方便,以后再补上吧。

 

转载于:https://www.cnblogs.com/lfri/p/9443120.html

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值