树状数组优化LIS问题

树状数组优化LIS问题

LIS即为最长上升子序列问题。学习动态规划问题(DP问题)中,其中有一个知识点叫最长上升子序列(longest increasing subsequence),也可以叫最长非降序子序列。

总所周知,LIS问题有贪心解法和DP解法。
贪心时间复杂度\(O(n)\),DP时间复杂度\(O(n^2)\)
本文将不讨论贪心的解法,因为一般是想不到怎么去做贪心的
实际上,在面试和比赛时候更常见的是使用DP做法(更加直观和具有一般性。

『题目传送门』:洛谷P1020
题目简述:给出一个长度不超过100000的数列,其中的数每个是不大于50000的正整数,求这个数列的最长不降子序列(问一)以及将这个数列划分为n个不降子序列时,n的最小值(问二)。
(推导过程见Dilworth定理:偏序集的最少反链划分数等于最长链的长度

DP平方复杂度

『题目传送门』:洛谷P1020
因为空间是1e5,如果不优化空间开二维数组肯定空间爆炸(\(({1e5})^2\))。
使用滚动数组思想优化空间后,对于最长上升子序列的状态转移方程是\(f[i]=\text{max}(f[j])\)其中 \(j< i\)并且\(a[j]< a[i]\),值得注意的是需要把每一次状态的初始值初始化为1(因为一个数的上升子序列就是1)
相同的,对于最长不上升子序列,只需要将这个过程反过来做即可,即从后面向前面做转移,方程是\(f[i]=\max (f[j])\)其中 \(i< j\)并且\(a[j]<= a[i]\),值得注意的是i的初始状态是从n开始的(从后往前做转移)。

// Author: oceanlvr
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
const int inf = 0x3f3f3f3f;
const int maxn = 1e5 + 10;
int n;
int res1 = -inf, res2 = -inf;
int a[maxn];
int f[maxn];
int main() {
  while (cin >> a[++n])
    ;
  n -= 1;
  //最长不上升子序列
  for (int i = n; i >= 1; i--) {
    f[i] = 1;
    for (int j = n; j > i; j--) {
      // i <-j
      if (a[i] >= a[j]) {
        f[i] = max(f[i], f[j] + 1);
      }
    }
  }
  for (int i = 1; i <= n; i++) res1 = max(res1, f[i]);
  //最长上升子序列
  for (int i = 1; i <= n; i++) {
    f[i] = 1;
    for (int j = 1; j < i; j++) {
      if (a[j] < a[i]) {  // j->i
        f[i] = max(f[i], f[j] + 1);
      }
    }
  }
  for (int i = 1; i <= n; i++) res2 = max(res2, f[i]);
  cout << res1 << endl << res2 << endl;
  return 0;
}

树状数组维护查询nlgn复杂度

『题目传送门』:洛谷P1020

不出意外上面的写法只能过10个测试点。题目给出提示:nlgn的写法能够给出200分
那么如何达到nlgn呢?我们来分析下:
我们知道DP的时间复杂度是转移的次数x各个状态转移成本,其中转移方程的次数又叫做阶段数。

  • 首先是转移方程的次数是\(n\),这个没有办法再优化了(至少要完成每一个转移)。
  • 然后是转移的成本,分析转移方程可知,DP的LIS做法每一次都需要查找前面元素中可转移的最大值即a[j]<=a[i]中a[j]最大。这一部分可以转为有条件的在区间内搜索最大值,是一个变种的RMQ问题,即可使用ST表或者线段树或者树状数组来优化。

更加具体一点,树状数组\(f\)(区间范围是从\(1->max(a[i])\))。
维护的是:树状数组f[i]当前以i结尾的LIS长度的最大值。(关键是理解维护的是什么)
因此每一次转移时,我们即查询以\([1,a[i]-1]\)中的最大值。(区间查询操作query(a[i] - 1)
然后这个最大值+1即为当前的以\(a[i]\)结尾的最大值,并且把这个最大值加入到树状数组中(插入单点操作add(a[i], q + 1);

手动过一遍样例。

//#define judge
// Author: oceanlvr
#include <bits/stdc++.h>
using namespace std;
const int inf = 0x3f3f3f3f;
const int ninf = 0xcfcfcfcf;
static int faster_iostream = []() {
  std::ios::sync_with_stdio(false);
  std::cin.tie(NULL);
  return 0;
}();
const int N = 1e5 + 10;
int maxn = ninf;
int n;
int res1 = -inf, res2 = -inf;
int a[N];
int f[N];
int lowbit(int x) { return x & -x; }

void add(int x, int c) {
  for (int i = x; i <= maxn; i += lowbit(i)) f[i] = max(f[i], c);
}

int query(int x) {
  int res = 0;
  //求以小于等于x的数为结尾的最长不上升子序列的长度的最大值
  for (int i = x; i; i -= lowbit(i)) res = max(res, f[i]);
  return res;
}
int main() {
  /*使用树状数组f来维护信息
    维护的是:当前以i结尾的最大的LIS长度
    每次查询的时间复杂度是log(max(a[i])) 即a[i]的值域的对数
  */
  while (cin >> a[++n]) maxn = max(a[n], maxn);
  n -= 1;
  //最长不上升子序列
  /*
  求以a[i]结尾的最大的不上升子序列的长度
  等效于从后向前->求a[i]结尾的最长不上升子序列
              1 3 4 4 5
  最长上升    1 3 4 5
  最长不上升  4 4       (从后向前看,求最长不上升)
  所以对于这个问题只需要维护一个树状数组即可
  一次从后向前做(最长不上升子序列)
  一次从前往后做(最长上升子序列)
  每次查询都是问 $以a[i]结尾目前的最长子序列的长度$
  即树状数组维护的是长度,并且其定义域是a[i]的值域
  */
  for (int i = n; i >= 1; i--) {
    int q = query(a[i]);  
    add(a[i], q + 1);
    res1 = max(res1, q + 1);
  }
  memset(f, 0, sizeof f);  // memset一下

  //最长上升子序列
  for (int i = 1; i <= n; i++) {
    int q = query(a[i] - 1);  // 找到[1,a[i]-1]中的最大值
    add(a[i], q + 1);  //这个最大值即是有效的转移 加入到树状数组中去
    res2 = max(res2, q + 1);
  }
  cout << res1 << endl << res2 << endl;
  return 0;
}
posted @ 2020-03-12 15:07  AdaMeta730  阅读(901)  评论(0编辑  收藏  举报