序列算法(一)
掌握序列算法已经成为面试笔试的标配。基本的序列问题多用动态规划技巧解。动态规划通过把原问题分解为相对简单的子问题的方式求解复杂问题。本文中将要讨论的以下三个问题多用到动态规划:最大连续子序列(Maximum Consecutive Subsequence),最长递增子序列(Longest Increasing Subsequence),最长公共子序列(Longest Common Subsequence)。在用动态规划分析问题时,需要找到分解子问题的方法,此外,还应该判断子问题的最优解是否能决定全局最优解。
最大连续子序列
问题:给定序列$a_{1}, a_{2}, \cdots, a_{n}$,寻找连续子序列$a_{k}, a_{k+1}, \cdots, a_{k+m}$,$k \ge 1$,$k+m \le n$,使得$\sum_{i=k}^{k+m}a_{i}$最大。
动态规划解可以从简单的情况开始构建问题。当序列只有一个元素时,如果该元素不小于0,则它自身构成问题的最优解。此时往序列加入一个元素$a_{i}$,如果$a_{i} \ge 0$,那么可以将其加入以$a_{i-1}$结尾的连续子序列中使其和增大(或不变)。而如果$a_{i} \lt 0$,需要观察当前子序列的和。如果子序列在吸收了该元素后和不小于0,那么有必要记录下当前的和,并将$a_{i}$加入子序列,因为$a_{i}$有可能连接了两段和为正的连续子序列,使得总和大于其中任意一段;如果子序列在吸收了$a_{i}$和小于0,那么应该从$a_{i+1}$开始建立新的连续子序列,因为如果全局最优解存在于之后的元素中,连接以$a_{i}$结尾的子序列不会得到和更大的子序列。
上述分析可以用公式表达。令$S$表示当前子序列的和,$S^{*}$表示最大子序列和,则有
$S_{i} = \max\{S_{i-1} + a_{i}, 0\}$
$S_{i}^{*} = \max\{S_{i}, S_{i-1}^{*}\}$
该方法的朴素解需要考虑所有可能的连续序列。给定长度为$N$的原序列,连续子序列的情况共有$\sum_{i=1}^{N}i$种。考虑到每个子序列长度,需要计算$\sum_{i=1}^{N}i(N-i)$次加法,复杂度为$O(N^{3})$。使用动态规划方法,明显地将复杂度降低为$O(N)$。
// Return maximal sum only.
int max_cons_subseq(const int *array, int len) {
int sum = 0, cnt = 0;
for (int i = 0; i < len; ++i) {
cnt += array[i];
if (cnt > sum) sum = cnt;
else if (cnt < 0) cnt = 0;
}
return (sum);
}
// Retrieve the subsequence.
int max_cons_subseq(const int *array, int len, int &beg, int &end) {
beg = -1, end = -1;
int sum = 0, cnt = 0, pos = -1;
for (int i = 0; i < len; ++i) {
cnt += array[i];
if (cnt > sum) {
sum = cnt;
beg = pos;
end = i + 1;
} else if (cnt < 0) {
cnt = 0;
pos = i + 1;
}
}
return (sum);
}
最长递增子序列
问题:给定序列$a_{1}, a_{2}, \cdots, a_{n}$,寻找最长子序列$a_{k_{1}}, a_{k_{2}}, \cdots, a_{k_{m}}$,$1 \le k_{1} \lt k_{2} \lt \cdots \lt k_{m} \le n$,满足$a_{k_{i}} \le a_{k_{i+1}}$。
还是从简单的情况开始考虑。一个元素自身构成问题的最优解。当新元素$a_{j}$加入时,将它与之前所有元素$a_{i}, i \lt j$作比较。对于之前的每一个元素$a_{i}$,如果新元素$a_{j}$不小于它,那么$a_{j}$可以加长以$a_{i}$结尾的递增子序列。如此,我们可以获得以$a_{j}$结尾的最长的递增子序列长度,它是遍历之前所有元素,并按照上述方法更新长度后得到的最大值。若以$L_{k}$表述以$a_{k}$结尾的最长递增子序列长度,我们有
$L_{k} = \max_{i=1}^{k-1}L_{i} + \left( a_{k} \ge a_{i} \right)$
那么,本文的的最优解就存在于$\max_{i=1}^{n}L_{i}$中。
考虑所有情况的朴素解将处理$2^{N}$种子序列组合,而动态规划法将复杂度降到$O(N^{2})$。
int long_inc_subseq(const int *array, int len) {
int *lis = new int[len];
for (int i = 0; i < len; ++i) lis[i] = 1;
int size = 0;
for (int i = 1; i < len; ++i) {
for (int j = 0; j < i; ++j) {
if (array[i] > array[j]) lis[i] = std::max(lis[i], lis[j] + 1);
}
if (lis[i] > size) size = lis[i];
}
delete [] lis;
return (size);
}
int long_inc_subseq(const int *array, int len, int *index) {
int *lis = new int[len];
int *lip = new int[len];
for (int i = 0; i < len; ++i) lis[i] = 1, lip[i] = 0;
int size = 0, pos = 0;
for (int i = 1; i < len; ++i) {
for (int j = 0; j < i; ++j) {
if (array[i] > array[j]) {
if (lis[j] + 1 > lis[i]) {
lis[i] = lis[j] + 1;
lip[i] = j;
}
}
}
if (lis[i] > size) {
size = lis[i];
pos = i;
}
}
for (int i = size-1; !(i < 0); --i) {
index[i] = pos;
pos = lip[pos];
}
delete [] lis;
delete [] lip;
return (size);
}
最常公共子序列

浙公网安备 33010602011771号