关于最长公共子序列
原来学习的关于最长公共子序列的解法,要不是朴素 dp $O(n ^ 2)$ ,要不是基于二分的 $O(n \log n)$ 算法。后者虽然时间复杂度优,但是局限性很大,无法在同时做一些附加的转移。
这里提供一种基于树状数组的 $n \log n$ 解法,实现原理与朴素 dp 相同。
实现原理
树状数组除了维护前缀和外还能维护前缀最大值。
因此我们按元素大小安排每个元素在树状数组中的位置。
这样,每次查询前缀最大值,查到的都是比 a[i]
小的 a[j]
中所对应的 dp[j]
最大值。
参考实现:
dp[1] = 1; modify(a[1], 1); for (int i = 2; i <= n; i++) { int lst = query(a[i] - 1); dp[i] = lst + 1; modify(a[i], dp[i]); mxlen = max(mxlen, dp[i]); }
注:若
a[i]
值域过大应先进行离散化。
一些例题
求最长上升子序列的最大长度及方案数。由于原数组为全排列,所以不考虑去重。 $N \leq 10^5$ 。
朴素的方案数很容易在求长度时顺便转移,只需要将所有能够转移到此状态的前面的状态的方案数求一个总和就行了。
但是由于数据范围,我们需要在 $n \log n$ 下解决问题。
那么,树状数组如何顺便记录最大值的方案数呢?
回归到数据结构本身的原理。每一个 c[i]
都管的是 i - lowbit(i)
这个区间。
对于每个 c[i]
,我们开一个 cnt[i]
表示这段区间内所有取最大值的 dp[i]
所对应的方案数总和。
更新时,我们看当前的 dp
值与原来最大值的关系,分类讨论更新。
query时,当我们算出全局最大值后,再重新扫一遍树状数组,将这段区间最大值等于全局最大值的所有方案数总和加起来就好了。
具体实现见代码(仅提供树状数组部分)。
void modify(int pos, pair<int, int> val) { while (pos <= n) { if (val.first > c[pos]) // 分类讨论:如果大于直接覆盖 { c[pos] = val.first; cnt[pos] = val.second; cnt[pos] %= MOD; } else if (val.first == c[pos]) // 如果等于就加上,保留原来的 { cnt[pos] += val.second; cnt[pos] %= MOD; } pos += lowbit(pos); } } pair<int, int> query(int pos) { int tmp = pos; pair<int, int> res = {0, 0}; while (pos > 0) { res.first = max(res.first, c[pos]); pos -= lowbit(pos); } pos = tmp; while (pos > 0) { if (c[pos] == res.first) { res.second += cnt[pos]; res.second %= MOD; } pos -= lowbit(pos); } return res; }
求最长公共子序列的方案数。序列中包含的数相同算一种。数据保证 $a_i < n$ 。
此时我们需要考虑去重。
易得朴素的转移方程下,我们只需将 dp[i]
加上 dp[j]
满足:
$j < i$ 且 $dp_j$ 为其中最大值且 $a_{j + 1} \to a_{i - 1}$ 中不再出现与 $a_j$ 相同的数(即这个位置对应的的值是在 i 前第一个出现的)。
因此,在每次更新树状数组时,我们需要消除掉原来与其 a 值相同的方案数。
实现见代码。
void modify(int pos, pair<int, int> val, int now) { int tmp = pos; while (pos <= n) { if (c[pos] == maxlen[a[now]]) { cnt[pos] -= maxcnt[a[now]]; } pos += lowbit(pos); } pos = tmp; while (pos <= n) { if (c[pos] < val.first) { c[pos] = val.first; cnt[pos] = val.second; cnt[pos] %= MOD; } else if (c[pos] == val.first) { cnt[pos] += val.second; cnt[pos] %= MOD; } pos += lowbit(pos); } } pair<int, int> query(int pos) { int tmp = pos; pair<int, int> res = {0, 0}; while (pos > 0) { res.first = max(res.first, c[pos]); pos -= lowbit(pos); } pos = tmp; while (pos > 0) { if (c[pos] == res.first) { res.second += cnt[pos]; res.second %= MOD; } pos -= lowbit(pos); } return res; } main() { ios::sync_with_stdio(false); cin >> n; for (int i = 1; i <= n; i++) { cin >> a[i]; } dp[1] = tot[1] = 1; maxlen[a[1]] = maxcnt[a[1]] = 1; modify(a[1], {1, 1}, 1); for (int i = 2; i <= n; i++) { pair<int, int> res = query(a[i] - 1); dp[i] = res.first + 1; tot[i] = res.second % MOD; if (tot[i] == 0) tot[i] = 1; modify(a[i], {dp[i], tot[i]}, i); maxcnt[a[i]] = tot[i]; maxlen[a[i]] = dp[i]; } for (int i = 1; i <= n; i++) { ans1 = max(ans1, dp[i]); } for (int i = n; i > 0; i--) { if (vis[a[i]]) continue; vis[a[i]] = true; if (dp[i] == ans1) { ans2 += tot[i]; ans2 %= MOD; } } cout << ans1 << " " << ans2; return 0; }
此处使用了 maxcnt[]
与 maxlen[]
来记录以每个值结尾的最大长度及其对应方案数。
当更新最大长度时先把原来的 cnt 减回去,以防出现两个位置其值和长度都相同,被算了两次的情况。
还有一些例题。先咕了。
本文作者:aaaaaaqqqqqq
本文链接:https://www.cnblogs.com/aaaaaaqqqqqq/p/17976959
版权声明:本作品采用知识共享署名-非商业性使用-禁止演绎 2.5 中国大陆许可协议进行许可。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步