LIS
方法一:O(n2)的dp
状态定义:dp[i] 表示以数 a[i] 结尾的 LIS 值
状态转移方程:dp[i] = max(dp[j] + 1, dp[i]) 1 <= j < i && a[i] > a[j]
代码:
1 #include<iostream> 2 using namespace std; 3 const int maxn = 110; 4 int a[maxn], dp[maxn]; 5 int n, ans = -1; 6 int main() 7 { 8 cin >> n; 9 for(int i = 1; i <= n; i++) 10 { 11 cin >> a[i]; 12 dp[i] = 1; 13 } 14 for(int i = 1; i <= n; i++) 15 for(int j = 1; j < i; j++) 16 if(a[j] < a[i]) 17 dp[i] = max(dp[j] + 1, dp[i]); 18 for(int i = 1; i <= n; i++) 19 ans = max(ans, dp[i]); 20 cout << ans << endl; 21 return 0; 22 }
方法二:O(nlogn)的贪心+二分
引入一个数组,暂且命名为 low[] ,它的含义是:low[i] 表示长度为 i 的 LIS 序列的最小的最后一个元素。所以,low 数组里的元素都是递增的,对于一个递增的序列,要使它“成长的潜力”尽可能大,那么它目前的最后一个元素就要尽可能小。这个思路是正确的,那么就是 low 数组的维护问题了,当目前遍历到的这个数 a[i] 比 low 数组尾部数字大的时候,就把目前这个数加入到 low 数组尾部,否则,在low数组中找到第一个大于等于 a[i] 的元素 low[j] ,用 a[i] 去更新 low[j]。这里就存在着时间的优化,如果你找第一个大于等于 a[i] 的元素时,从头到尾遍历,那么最终的时间复杂度还是O(n2),这里可以用二分来找,二分的复杂度是O(logn),所以总的时间复杂度就是O(nlogn)。
看看代码吧:
1 #include <iostream> 2 using namespace std; 3 const int maxn = 100010; 4 const int INF = 0x3f3f3f3f; 5 int low[maxn], a[maxn]; 6 int n, len; 7 int binary_search(int *a, int r, int x) 8 { 9 int l = 1, mid; 10 while(l <= r) 11 { 12 mid = (l + r) >> 1; 13 if(a[mid] <= x) 14 l = mid + 1; 15 else 16 r = mid - 1; 17 } 18 return l; 19 } 20 int main() 21 { 22 cin >> n; 23 for(int i = 1; i <= n; i++) 24 { 25 cin >> a[i]; 26 low[i] = INF; 27 } 28 low[1] = a[1]; 29 len = 1; 30 for(int i = 2; i <= n; i++) 31 { 32 if(a[i] >= low[len]) 33 low[++len] = a[i]; 34 else 35 low[binary_search(low, len, a[i])] = a[i]; 36 } 37 cout << len << endl; 38 return 0; 39 }
方法三:O(nlogn)的树状数组优化dp
关于这个方法,其实整体思路和第一种没区别,只是在找数 a[i] 前面的LIS最大值的时候使用树状数组结构来优化效率。比如原序列 a[] ,复制一个一样的序列 b[] ,对 b[] 进行排序和去重(不去重的话就不是严格的上升序列),然后遍历 a[] 数组(因为子序列是建立在原序列的基础上的),假设我们遍历到了 a[i] ,我们要得到原序列中在 a[i] 前面的数其中LIS值最大的那个且那个数要比当前 a[i] 小,这两者缺一不可,那么好,b[] 数组刚好是排序后的数,找到数 a[i] 在 b[] 中的位置下标 p ,那么也就是要在 b[] 数组中下标从1到p-1(假设我们的数下标都是从1开始的)中找到LIS值最大的,这里这个操作就可以用树状数组的区间查询来实现,设为 query(p-1) ,则 ans = max(ans,query(p-1)+1) 就能得到最终的LIS。
Reference: