====== 最长上升子序列 (LIS) ===== == 最长不下降及下降子序列请自行考虑关系运算符变化 == ===== 问题描述 ===== 给定(非负)整数序列{a1, a2, ..., an},求其一个子序列,使得元素呈升序,且子序列长度最长,输出长度(输出序列) 例:{1, 6, 4, 2, 3, 9, 8} 的LIS为{1, 2, 3, 9} 或 {1, 2, 3, 8} ===== 解题思路 ===== ==== Solution1 : DP ==== 设 $dp[i]$ 表示以 $a[i]$ 为结尾的 $LIS$ 长度, 初始化为 $1$ (子序列至少包含自己) 为保证升序,状态转移需从 $a[i]$ **之前** 比 $a[i]$ **小** 的元素转移而来。你说,是不是啊 则状态转移方程 $dp[i] = max(dp[i], dp[j] + 1)$ ,其中 $(j < i) a[j] < a[i]$, 复杂度$O(n^2)$。你说,是不是啊*2 如需输出任意一解,可设置一追踪标记 $tr$,与 $ans$ 同步更新为当前位置 $i$ 设 $ans$ 在 $dp[k]$ 处取到,则对于 $i < k$ 都有 $dp[i] < dp[k]$ (废话),且**至少有一个序列满足 $dp$ 数组以 1 为单位递增**。你说,是不是啊*3 如{1, 6, 4, 2, 3, 9, 8}的 $dp$ 数组元素为{1, 2, 2, 2, 3, 4, 4} 那么只需从 $tr$ 遍历到 $1$,遇到 $a[tr] == ans$ 则 $a[tr]$ 压栈,同时 $ans -= 1$,最后弹栈输出。你说,是不是啊*4 for(int i = 1; i <= n; i++) dp[i] = 1; int ans = -1, tr; for(int i = 1; i <= n; ++i){ for(int j = 1; j < i; ++j){ if(a[j] < a[i]) dp[i] = max(dp[i], dp[j] + 1); ans = max(ans, dp[i]); if(dp[i] >= ans) tr = i, ans = dp[i];/*也可不加等号*/ } } printf("%d\n", ans); while(tr){ if(dp[tr] == ans) z.push(a[tr]), --ans; --tr; } while(!z.empty()) printf("%d ", z.top()), z.pop(); ==== Solution 2 : 二分+贪心 ==== 因为贪心所以某些条件下会受限制。你说,是... 草,不说了 设 $f[i]$ 表示长度为 $i$ 的 $LIS$ 末尾元素的**最小值**,这个数组一定是**单调不下降**的。 置 $f[1] = a[1]$ ,取 $cnt = 1$ 表示 $LIS$ 的长度,遍历数组 $a$ : $a[i] > f[cnt]$ 则 $f[++cnt] = a[i]$ $a[i] < f[cnt]$ ,找到 $f$ 中**第一个**大于 $a[i]$ 的位置并替换为 $a[i]$ 这样做的替换并不会影响当前 $LIS$ 的长度变化,只改变了数组 $f$ 中某个位置的值并让其尽可能小,以便于不断获得更优解,所以不影响 $LIS$ 长度的正确性,但不能用来追踪解,可想而知。 因为数组 $f$ 有序,所以可用二分,复杂度 $O(nlogn)$ f[1] = a[1]; int cnt = 1; for(int i = 2; i <= n; ++i){ if(a[i] > f[cnt]) f[++cnt] = a[i]; else if(a[i] < f[cnt]){ int j = upper_bound(f + 1, f + cnt + 1, a[i]) - f;/*得到数组f中第一个大于a[i]的位置,也可手写二分*/ f[j] = a[i]; } } 最终 $f[cnt]$ 即为答案 ==== Solution 3 : 转LCS ==== 顾名思义,将数组 $a$ 排序得到新数组 $b$,问题转化为求 $a,b$ 的**最长公共子序列**,长度分别为 $n,m$ 此处只给出其 $DP$ 状态转移方程,以 $dp[i][j]$ 表示 $a[i]$ 和 $b[j]$ 的LCS长度,状态转移方程 $dp[i][j] =$ 0 ;(i == 0 || j == 0) dp[i-1][j-1] + 1 ;(a[i] == b[j]) max{dp[i][j-1], dp[i-1][j]} ;(a[i] != b[j]) 解的追踪(不予详解):设一标记数组 $tr[n][m]$ ,标记依据 $dp$ 方程设立,最后从 $tr[n][m]$ 回溯寻找 $a[i] == b[j]$的标记,压栈即可 伪码 for(int i = 1; i <= n; ++i){ for(int j = 1; j <= m; ++j){ if(a[i] == b[j]){ dp[i][j] = dp[i-1][j-1] + 1; tr[i][j] = 3; } else{ dp[i][j] = max(dp[i-1][j], dp[i][j-1]); tr[i][j] = dp[i-1][j] > dp[i][j-1] ? 1 : 2; } } }。 int x = n, y = m, t; int dx[4] = {INF, -1, 0, -1}; int dy[4] = {INF, 0, -1, -1}; stack z; while(tr[x][y]){ if(tr[x][y] == 3) z.push(a[x]); t = tr[x][y]; x += dx[t]; y += dy[t]; } while(!z.empty()){ printf("%d ", z.top()); z.pop(); } 答案即为 $dp[n][m]$ ==== Solution 4 : 据说可以用树状数组? ==== 树状数组可参考本队上一专题→[[树状数组]] 但是到考期了你懂的 这里不讲了,也是 $O(nlogn)$ 的复杂度,有需要自行查找吧