浅谈关于 LIS 和 一般线性 DP 的思考

\(\texttt{浅谈关于 LIS 和 一般线性 DP 的思考}\)

写在前

关于 \(\texttt{LIS}\) 这类的经典问题,已经普遍在 \(\texttt{OI界}\) 了。这里笔者在复习时,对一些二级知识点有些新的心得,所以浅谈关于 \(\texttt{nlog 做法和 DP 优化问题}\)

关于裸板透露的信息

首先,可以肯定的一点是,对于第一个版本的 \(\texttt{LIS}\) 问题,也就是最裸的板子题,数据范围是 \(1e4\)\(n^2\) 板子。思想见下:

int n;
int a[maxn], f[maxn]; // f[i] 是以 i 结尾的 LIS 长度
rd(n);
for (int i = 1; i <= n; i++)
rd(a[i]), f[i] = 1;
for (int i = 1; i <= n; i++)
for (int j = 1; j < i; j++) {
    if (a[j] < a[i] && f[i] < f[j] + 1)
	f[i] = f[j] + 1; // 如果当前状态,由于之前枚举的状态,那就转移
}

由此观之,\(\texttt{DP 具有 判断性继承思想}\),这也是关于 \(\texttt{DP}\) 的主要思想之一,也是不变的线索。

\(\texttt{nlogn}\) 板子的思考

众所周知,当数据范围达到 \(1e5\) 以上(关于这个大小的具体值,不做讨论,以测评机为准)时,我们需要以一种更优秀的复杂度解决,那么答案就是如下:

// 这是一种类贪心做法,考虑已经做好了一个长度为 i 的 LIS,此时,我们显然是期望这个 LIS 的结尾值越小越好,因为这样对后面的新元素的加入,产生的贡献更大。
int n, len, l, r, mid;
int a[maxn], f[maxn]; // f[i] 是长度为 i 的 LIS 的结尾的最小值
rd(n);
for (int i = 1; i <= n; i++)
rd(a[i]), f[i] = 0x3f3f3f3f;
f[1] = a[1]; len = 1;
for (int i = 2; i <= n; i++) {
    l = 0; r = len;
    if (a[i] > f[len]) f[++len] = a[i];
    else while (l < r) {
        mid = (l + r) >> 1;
        if (f[mid] > a[i]) r = mid;
        else l = mid + 1;
    } f[l] = min(f[l], a[i]);
} wr(len);

那么,由此观之,我们不再有枚举 \(i\) 之前的 \(j\) 个元素的过程,而是通过转变状态记录策略,只用关注每个长度意义下的状态转移问题,这样就可以用类似二分、单调队列等带 \(\texttt{log}\) 的工具,优化其中一个维度的枚举,以达到优化目的。所以我们可以总结,关于 \(\texttt{DP 的优化}\) 可以是由状态记录策略的转移变化而改变的,最显然的例子是一些维度优化、滚动数组等。

\(\texttt{关于统计方案数}\)

对于 \(n^2\) 的统计其实很好想,至于 \(\texttt{nlogn}\) 可能比较复杂,等我以后再想:

// 即,对第一次的 dp 数组,进行一次新的 dp
// f1 是第一次的 dp 数组,f2 是第二次的 dp 
for (int i = 1; i <= n; i++) {
    if (f1[i] == 1) f2[i] = 1;
    for (int j = 1; j <= n; j++)
    if (a[i] > a[j] && f1[j] == f1[i] - 1) f2[i] += f2[j];
    else if (a[i] == a[j] && f1[j] == f1[i]) f2[i] = 0;
    if (f2[i] == ans) res += f[i];
}

相关的思考是:复杂度越高的算法往往拥有更强的功能听君一席话,如听一席话。

这里想要表达的是,我们应该掌握算法核心思想,而不是关注板子。

关于路径输出

显然,可以只记录前驱,然后输出(可以用栈)。那么 \(n^2\) 就很好想了,至于 \(\texttt{nlogn}\) 我还是以后再想:

int n, a[maxn];
int f[maxn], from[maxn];
void output(int x) {
    if (!x) return;
    output(from[x]);
    wr(a[x], ' '); return;
} signed main() {
    rd(n);
    for (int i = 1; i <= n; i++)
	rd(a[i]);
    for (int i = 1; i <= n; i++) {
		f[i] = 1; from[i] = 0;
        for (int j = 1; j < i; j++) {
            if (a[j] < a[i] && f[i] < f[j] + 1)
            f[i] = f[j] + 1, from[i] = j;
		}
	} int ans = f[1], pos = 1; // 注意到,我们可以记录每个元素的前驱,然后找到最优状态下的末尾数字,然后再输出。
    for (int i = 1; i <= n; i++) {
    	if (ans < f[i])
        ans = f[i], pos = i;
    } wr(ans, '\n'); output(pos); return 0;
}

关于公共 \(\texttt{LIS}\)

一下两种做法,即 \(n^2\)\(nlogn\)

\(n^2\) 做法

考虑设计 \(dp[i][j]\) 表示序列 \(a\) 的第 \(i\) 位和序列 \(b\) 的第 \(j\) 位的公共 \(LIS\)

转移分为两种情况:

  • 一般情况直接转移 \(dp[i][j] = \max(dp[i - 1][j], dp[i][j - 1])\)
  • \(a[i] == b[j]\) 时,考虑更新 \(dp[i][j] = \max(dp[i][j], dp[i - 1][j - 1] + 1)\)

伪代码如下:

int n, m, a[maxn], b[maxn], dp[maxn][maxn];
rd(n, m);
for (int i = 1; i <= n; i++) rd(a[i]);
for (int i = 1; i <= m; i++) rd(b[i]);
for (int i = 1; i <= n; i++)
for (int j = 1; j <= m; j++) {
    dp[i][j] = max(dp[i - 1][j], dp[i][j - 1]);
    if (a[i] == b[j])
	dp[i][j] = max(dp[i][j], dp[i - 1][j - 1] + 1);
} wr(dp[n][m]); return 0;

\(nlogn\) 做法

考虑给定的两个序列都是 \(1 \sim n\) 的全排列,也就是说两个序列除了元素的位置不一样,其他的都一样,是互异且相同的。

那么考虑,维护一个 \(pos\) 数组,记录序列 \(a\) 的元素在序列 \(b\) 中的位置。

因为我们知道,\(LIS\) 是按照位向后对比的,所以如果 \(a\) 中的每个元素在 \(b\) 的位置是递增的话,说明 \(b\) 中的这个数字在 \(a\) 中的这个数整体位置偏后。然后纳入 \(LIS\) 实现 \(nlogn\) 的复杂度。

下面是伪代码:

int n, f[maxn], pos[maxn], a[maxn], b[amxn];
rd(n);
for (int i = 1; i <= n; i++) rd(a[i]), pos[a[i]] = i;
for (int i = 1; i <= n; i++) rd(b[i]), f[i] = 0x3f3f3f3f;
int len = 0; f[0] = 0;
for (int i = 1; i <= n; i++) {
    int l = 0, r = len, mid;
    if (pos[b[i]] > f[len]) f[++len] = pos[b[i]];
    else while (l < r) {
        mid = (l + r) >> 1;
        if (f[mid] > pos[b[i]]) r = mid;
        else l = mid + 1;
    } f[l] = min(f[l], pos[b[i]]);
    wr(len); return 0;
}

写在后

这篇写完,一是给自己温故知新,二是给学弟了解新知。

posted @ 2023-09-11 22:23  Furthe77oad  阅读(66)  评论(1编辑  收藏  举报