标签:标准 维护 复杂度 现在 最优 turn 答案 mat 两种
很多问题往往会给出一个序列或者一个数表,让你对其进行划分,或者选出其中的某个最优子集。这一类问题往往适合使用线性DP。
线性DP是一种非常常见的DP。它往往以状态内的其中一个维度划分阶段。接下来,我将给出几个非常重要的转移方程。
已知一个序列\(A_i\)。现在我希望从这个序列中从左往右选出若干个元素,使得这些元素组成的子序列元素大小单调递增。求这样序列的最大长度。
我们尝试设计状态表示这个最大高度。不难发现,只要\(A_i < A_j,i<j\),\(A_j\)就可以和\(A_i\)合并。这个过程和\(A_i\)以前的元素没有直接的关系。
于是我们尝试设\(F(i)\)为以\(A_i\)为结尾的,从\(A_1 \sim A_i\)中选出的LIS。不难发现这样一个转移关系:
\[
F(i) = \max_{j < i, A_j < A_i}\{F(j)\} + 1
\]
也就是说,我们以前\(i\)个序列划分阶段,如果有\(A_j < A_i, j < i\),那么答案就可以从\(F(j)\)转移到\(F(i)\)。
初始值\(F(1) = 1\)。
上面这个做法的时间复杂度为\(O(N^2)\),但我们可以通过以下两种方式做到\(O(N \log N)\)。
树状数组维护最大值
树状数组不能维护区间最大值,但可以维护前缀和后缀最大值。一个状态\(F(i)\)的决策集合为\(\{j\mid j < i, A_j < A_i\}\)。我们可以在\(A_i\)的值域上建立树状数组,保存结尾在\([0,A_i]\)范围内的最长上升子序列长度。由于我们每次按输入顺序查询,并更新之,我们自动满足了\(i < j\)的条件。
贪心+二分
并不能算是标准做法,但是非常巧妙。它并没有直接设状态表示最长上升子序列的长度,而是设\(F(i)\)表示当最长上升子序列的长度为\(i\)时,这个子序列末尾的最小值。
有一个比较显然的贪心策略:当两个上升子序列长度一样时,我们应该保留结尾较小者,因为它更有可能接纳更长的上升组序列。
核心代码:
inline int bs(int num)
{
int l = 0, r = maxlen + 1;
while(l < r)
{
int mid = (l + r) >> 1;
if(F[mid] > num)
r = mid;
else
l = mid + 1;
}
return l;
}
int main()
{
N = qr(1);
RP(i, 1, N) A[i] = qr(1);
RP(i, 1, N)
{
int pos = bs(A[i]);
if(pos > maxlen)
{
maxlen = pos;
F[pos] = A[i];
}
else
F[pos] = A[i];
}
printf("%d", maxlen);
return 0;
}
可以看出,尽管DP的速度相较搜索而言相当快了,但也可以通过其他算法进行更深层的优化。
给定两个序列\(A_i\),\(B_i\),现在我们要求找出一个序列\(C\),使得\(C\)既是\(A\)的子序列,又是\(B\)的子序列。请你最大化序列\(C\)长度。
从上一题来看,一个比较直观的想法是设\(F(i,j)\)表示以\(A_i\)结尾,以\(B_j\)结尾的最长公共子序列。当\(A_i \neq B_j\)时,直接令\(F(i,j)=0\)。当\(A_i = B_j\)时,有转移方程 \(F(i,j) = \max_{k < i, l < j, A_k = A_i, B_l = B_j}\{F(k,l)\}+1\)。
这个方程有两个比较明显的问题。首先,\(F(i,j)\)这个状态是不是过度冗余?很大一部分的\(F(i,j)=0\),这样会浪费大量的空间。其次,时间复杂度过高,达到了\(O(N^4)\)。如果用链表也许可以减少时间,但还是减少不了多少。
考虑这样一种做法:设\(F(i,j)\)表示前缀序列\(A_{1\cdots i}\)和\(B_{1\cdots j}\)的最长个公共子序列之长(此时这个子序列不一定包含\(A_i,B_j\)!)。此时的\(i,j\)可以看作是两个扫描数组的指针。
当\(i,j\)想向后扩展时,有以下情况:
综上,当\(A_{i+1}=B_{i+1}\)时,\(F(i,j)\)可以直接转移到\(F(i+1,j+1)\),并增加一个贡献。反之,\(F(i,j)\)应该转移到\(F(i,j+1)\)或者\(F(i+1,j)\),并取其中的最大值。
把上面的每个方程反过来写,写成被转移状态关于转移状态的表达式,把\(i+1,i\)改成\(i,i-1\),就可以得到转移方程:
\[
F(i,j) = \begin{cases}
F(i-1,j-1)+1 & A_i = B_j\\max\{F(i-1,j),F(i,j-1)\} & A_i \neq B_j
\end{cases}
\]
另外,这道题的阶段划分依据叫做“已经处理的前缀长度”。这里并没有直接指明到底是哪一个前缀,因此任意选择一个序列即可。
LIS和LCS的结合。求序列\(A_i,B_i\)最长的,单调递增的LCS。
注意到第一题的状态保证了“取结尾”,而第二题却没有。为了降低时间复杂度,这里我们应该采用“半保留”的设计方法。
设\(F(i,j)\)表示扫描到前缀子序列\(A_{1\cdots i}\),以\(B_j\)结尾的LCIS。
当\(i\)指针向后移一位时,前缀子串会多出来一个新的数\(A_{i+1}\)。对于这个数,我们有两种方案:
把这两种方案写成填表法的形式,把上面的\(j\)分别替换成\(j-1\)和\(k\),就得到:
\[
F(i,j) = \begin{cases}
F(i-1,j) & A_i \neq B_j\\max_{k < j, B_k < B_j}\{F(i-1,k)\} + 1 & A_i = B_j
\end{cases}
\]
这个转移的时间复杂度是\(O(N^3)\)的,其中这里只能以\(A_i\)的前缀子序列长度划分阶段。但它还有优化的空间。
注意到在任意时刻,如果我们需要\(O(N)\)枚举\(F(i,j)\)的决策集合,那么必然有\(A_i = B_j\)。因此,当\(A_i = B_j\)时,转移方程还可以写成这个样子:
\[
F(i,j) = \max_{k < j, B_k < A_i}\{F(i-1,k)\}+1
\]
在每个阶段,\(A_i\)的值都是固定的;在当前阶段,对决策集合的限制条件只有\(k < j\)。因此,当前\(F(i,j)\)计算完之后,\(j < j' = j+\Delta j\),那么\(F(i,j)\)就会立刻被加到\(F(i,j')\)的决策集合中!
这就是时间复杂度过大的根源。我们花费了额外\(O(N^2)\)的时间来重复扫描这个决策集合,而这个集合实际上是“开源”的,可以供当前阶段的所有状态使用。
因此,我们只需要用一个变量\(F_m\)来维护\(\max_{B_j < A_i}\{F(i,j)\}\)。每当计算完一个状态\(F(i,j)\),我们就可以拿它来更新\(F_m\)。这样时间复杂度就降为了\(O(N^2)\)。
标签:标准 维护 复杂度 现在 最优 turn 答案 mat 两种
原文地址:https://www.cnblogs.com/LinearODE/p/11589127.html