一、问题
有一个长为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 }
打印结果路径似乎不太方便,以后再补上吧。