第五章 动态规划(2):序列 / 线性 DP (LIS、LCS模型)

1 最长上升子序列

ACWing 895

Tips: 子序列 ≠ 子串!KMP用于求子串!

集合

f [ i ] f[i] f[i]:所有以第 i i i 个数结尾的上升子序列的长度的最大值

集合划分:
以第 i i i 个数结尾划分

状态计算:

f [ i ] = m a x ( f [ i ] , f [ j ] + 1 ) f[i] = max(f[i], f[j] + 1) f[i]=max(f[i],f[j]+1)

其中,要保证 a j < a i ,   j ∈ ( 0 , i − 1 ) a_j < a_i, \ j \in (0, i - 1) aj<ai, j(0,i1)

#include <iostream>
#include <algorithm>
using namespace std;

const int N = 1010;

int n;
int a[N], f[N];

int main() {
    scanf("%d", &n);
    for (int i = 1; i <= n; i ++ ) scanf("%d", a + i);
    int res = 1;
    for (int i = 1; i <= n; i ++ ) {
        f[i] = 1;
        for (int j = 1; j < i; j ++ )
            if (a[j] < a[i]) {
                f[i] = max(f[i], f[j] + 1);
                res = max(res, f[i]);
            }
    }
    printf("%d\n", res);
    return 0;
}

输出最长上升子序列(倒序):

#include <iostream>
#include <algorithm>
using namespace std;

const int N = 10;

int n;
int a[N], f[N], g[N]; // g保存每个转移的过程

int main() {
    scanf("%d", &n);
    for (int i = 1; i <= n; i++) scanf("%d", a + i);
    for (int i = 1; i <= n; i++) {
        f[i] = 1, g[i] = 0;
        for (int j = 1; j < i; j++)
            if (a[j] < a[i])
                if (f[i] < f[j] + 1)
                    f[i] = f[j] + 1, g[i] = j;
    }
    int k = 1; // 最优解下标
    for (int i = 1; i <= n; i++)
        if (f[k] < f[i]) k = i;
    printf("len: %d\n", f[k]);
    for (int i = 0, len = f[k]; i < len; i++)
        printf("%d\n", a[k]), k = g[k];
    return 0;
}

2 最长上升子序列 II (贪心优化)

ACWing 896

优化思路:

假设有一个序列: 3 , 1 , 2 , 1 , 8 , 5 , 6 3, 1, 2, 1, 8, 5, 6 3,1,2,1,8,5,6

  • 考虑一个性质:
    • 首先仅考虑长度为 1 1 1 的上升子序列,第一个为 3 3 3,第二个是 1 1 1,则在考虑后面所有数的时候,如果它可以添加到 3 3 3 后面构成上升子序列,那么它就一定可以添加到 1 1 1 的后面构成一个上升子序列(因为 1 1 1 3 3 3 更小,比如 8 8 8 可以接到 3 3 3 后面构成 3 , 8 3, 8 3,8,那么它一定可以接到 1 1 1 后面构成 1 , 8 1,8 1,8),所以以 3 3 3 结尾的子序列就没必要存储下来了。总的来说就一句话:考虑一个数放到一个升序的序列后面继续构成升序序列,如果这个数能放到一个结尾更小的序列后面,就一定能放到一个结尾更大的序列后面(基于贪心思想)
    • 由上面的分析,对于每个长度不同的上升子序列,我们只需要存储每个长度下结尾最小的上升子序列即可。因此我们可以用一个数组记录一下每种长度的上升子序列最后一个元素的值的最小值是多少,并且可以证明随着长度的增加,每种长度的结尾的最小值是单调递增的

    证明:假设长度为 6 6 6 的上升序列的结尾数值 Q 6 Q6 Q6,长度为 5 5 5 的上升序列的结尾数值为 Q 5 Q5 Q5。若 Q 6 < = Q 5 Q6 <= Q5 Q6<=Q5,因为它们均是单调递增的子序列,所以长度为 6 6 6 的序列的第 5 5 5 个数 x x x 一定小于 Q 6 Q6 Q6,并且可知 x x x 也一定小于 Q 5 Q5 Q5,与上面的结论(随着长度的增加,每种长度的结尾的最小值是单调递增的 )矛盾,故 Q 6 > Q 5 Q6 > Q5 Q6>Q5,证毕。

  • 由以上性质,对于求任意一个以 a i a_i ai 结尾的最长上升子序列的长度,因为 a i a_i ai 可以放到任意一个结尾值比它小的上升子序列中。但为了求最大长度,应该将 a i a_i ai 放到以第一个比 a i a_i ai小的数结尾的上升序列中。

对于上面最后一点,举个例子详细说明,不然代码中 while 后面的更新很难理解:
在这里插入图片描述

首先, q [ i ] q[i] q[i] 中存储的是长度为 i i i 的单调上升子序列的结尾的最小值。假设有如上图的 q q q,现在我们对任意一个 a i a_i ai,通过二分查找,找到了第一个比 a i a_i ai 小的数 a a a为什么要第一个呢?因为图中的每个长度的单调上升序列的末尾值也是单调的,为了最后得到最长的单调上升子序列,所以应该接到比 a [ i ] a[i] a[i] 值小的最大的数结尾的单调上升序列后面去,这样就可以保证最后得到的序列长度尽可能地长),则 a i a_i ai 可以接到以 a a a 结尾的单调上升子序列的末尾中去。这里注意,有关系 a < a i < = b a < a_i <= b a<ai<=b。如果 a i a_i ai插入了以 a a a 结尾的单调上升序列中,那么其长度就变成了 4 4 4,又因为 a i < = b a_i <= b ai<=b,所以要对长度为 4 4 4 的子序列末尾进行更新。对应到代码中是 q[r + 1] = a[i] (因为 a i < = b a_i <= b ai<=b 就没有必要保留 b b b 了),而最大长度的更新就对应代码中的 len = max(len, r + 1),其中的 r r r 可以理解为 a i a_i ai 可以接到长度为 r r r 的队列的后面。

整体思路:

对于每一个数 a i a_i ai,找到一个以 a i a_i ai 结尾的最长上升子序列,其长度为 l e n len len,故最后的 l e n len len 最长上升子序列的长度。

#include <iostream>
#include <algorithm>
using namespace std;

const int N = 1e5 + 10;

int n;
int a[N], q[N];

int main() {
    scanf("%d", &n);
    for (int i = 0; i < n; i ++ ) scanf("%d", a + i);
    int len = 0;
    q[0] = -2e9;
    for (int i = 0; i < n; i ++ ) {
        int l = 0, r = len;
        while (l < r) {
            int mid = (l + r + 1) >> 1;
            if (q[mid] < a[i]) l = mid;
            else r = mid - 1;
        }
        len = max(len, r + 1), q[r + 1] = a[i];
    }
    printf("%d\n", len);
    return 0;
}

3 最长公共子序列

ACWing 897

集合

f [ i ] [ j ] f[i][j] f[i][j]:所有在第一个序列的前 i i i 个字母出现,且在第二个序列的前 j j j 个字母出现的最长公共子序列。

集合划分

在这里插入图片描述

  • a [ i ] 、 b [ j ] a[i]、b[j] a[i]b[j] 分别表示两个序列中的第 i i i 个元素和第 j j j 个元素;
  • a [ i ] 、 b [ j ] a[i]、b[j] a[i]b[j] 是否包含在子序列中来划分:
    • 00: a [ i ] 、 b [ j ] a[i]、b[j] a[i]b[j] 均不在 f [ i ] [ j ] f[i][j] f[i][j] 中;
    • 01: a [ i ] a[i] a[i] 在但 b [ j ] b[j] b[j] 不在 f [ i ] [ j ] f[i][j] f[i][j] 中;
    • 10: a [ i ] a[i] a[i] 不在但 b [ j ] b[j] b[j] f [ i ] [ j ] f[i][j] f[i][j]中;
    • 11: a [ i ] 、 b [ j ] a[i]、b[j] a[i]b[j] 均在 f [ i ] [ j ] f[i][j] f[i][j] 中。

状态计算:

  • 00: f [ i − 1 ] [ j − 1 ] f[i-1][j-1] f[i1][j1]
  • 11: f [ i − 1 ] [ j − 1 ] + 1 f[i-1][j-1] + 1 f[i1][j1]+1
  • 01: f [ i − 1 ] [ j ] f[i-1][j] f[i1][j]
  • 10: f [ i ] [ j − 1 ] f[i][j-1] f[i][j1]

对于 10 10 10 01 01 01 两种情况的一点解释(解释 01 01 01,对 10 10 10 的分析同理):

  • f [ i − 1 ] [ j ] f[i-1][j] f[i1][j] 表示所有在第一个字符串的前 i − 1 i-1 i1 个字符中出现,并且在第二个字符串的前 j j j 个字符中出现的一个子序列的最大值——注意关键词”出现“;
  • 01 01 01 这种情况是表示 a [ i ] a[i] a[i] 不出现在子序列中, b [ j ] b[j] b[j] 一定出现在子序列中的所有子序列,那么在这种情况下, b [ j ] b[j] b[j] 就一定是公共序列的结尾——注意关键词”结尾“;
  • 但是在 f [ i − 1 ] [ j ] f[i-1][j] f[i1][j] 出现的子序列并不一定是以 b [ j ] b[j] b[j] 结尾的(因为出现不代表是最后一个字母),因而它们的关系应该是: 01 ∈ f [ i − 1 ] [ j ] 01 \in f[i-1][j] 01f[i1][j],是一种包含关系!又因为我们最后求的是最大值,所以我们可以用 f [ i − 1 ] [ j ] f[i-1][j] f[i1][j]来代替 01 01 01,虽然这样求解最大过程中,可能会有重复的步骤(因为 01 、 10 01、10 0110 实际上是包含了 00 00 00 这种情况),但是最后的最大值依然不会改变。如果这里是求的数量,那么这里就不能代替了,求数量的子过程是不能重复的
  • 由以上分析,在实际代码中, 00 00 00 的情况可以不用写;
  • 上面的分析,也说明这 L I S LIS LIS L C S LCS LCS 在状态定义上最大的不同。
#include <iostream>
#include <algorithm>
using namespace std;

const int N = 1010;

int n, m;
char a[N], b[N];
int f[N][N];

int main() {
    scanf("%d%d", &n, &m);
    scanf("%s%s", a + 1, b + 1);  // 涉及i-1和b-1操作,字符串下标最好从1开始
    for (int i = 1; i <= n; i ++ )
        for (int j = 1; j <= m; j ++ ) {
            f[i][j] = max(f[i - 1][j], f[i][j - 1]);  // 01和10两种情况
            // 情况11要成立,且a[i]和b[j]是各自字符串中最后一个,所以它们必须相等
            f[i][j] = max(f[i][j], f[i - 1][j - 1] + (a[i] == b[j]));
        }
    printf("%d\n", f[n][m]);
    return 0;
}

更清晰的思路,更容易理解:按两个序列末尾的字符是不是相等来区分

在这里插入图片描述

#include <iostream>

using namespace std;

const int N = 1010;

int n, m;
char a[N], b[N];
int f[N][N];

int main() {
    scanf("%d%d%s%s", &n, &m, a + 1, b + 1);
    for (int i = 1; i <= n; i++)
        for (int j = 1; j <= m; j++)
            if (a[i] == b[j]) f[i][j] = f[i - 1][j - 1] + 1;
            else f[i][j] = max(f[i - 1][j], f[i][j - 1]);
    printf("%d\n", f[n][m]);
    return 0;
}

4 最短编辑距离

ACWing 902

集合

f [ i ] [ j ] f[i][j] f[i][j]:所有将 a [ 1   ∼   i ] a[1 \ \sim \ i] a[1  i] 变成 b [ 1   ∼   j ] b[1 \ \sim \ j] b[1  j] 的操作次数的最小值

集合分类:

  • 删掉 a [ i ] a[i] a[i] 之后, a a a b b b 匹配,那么要求 a [ 1 ] [ i − 1 ] a[1][ i-1] a[1][i1] 要与 b b b 匹配: f [ i − 1 ] [ j ] + 1 f[i-1][j] + 1 f[i1][j]+1
  • 增加 a [ i + 1 ] a[i + 1] a[i+1] 之后, a a a b b b 匹配,则 a [ i + 1 ] = b [ j ] a[i + 1] = b[j] a[i+1]=b[j],那么要求 a [ 1 ] [ i ] a[1][i] a[1][i] b [ 1 ] [ j − 1 ] b[1][j-1] b[1][j1] 匹配: f [ i ] [ j − 1 ] + 1 f[i][j-1] + 1 f[i][j1]+1
  • 修改 a [ i ] a[i] a[i] 之后, a a a b b b 匹配,那么要求 a [ 1 ] [ i − 1 ] a[1][i-1] a[1][i1] b [ 1 ] [ j − 1 ] b[1][j-1] b[1][j1] 匹配。如果 a [ i ] = = b [ j ] a[i] == b[j] a[i]==b[j],则最后不需要 + 1 +1 +1,否则需要 + 1 +1 +1 f [ i − 1 ] [ j − 1 ] + ( 1   o r   0 ) f[i-1][j-1] + (1\ or\ 0) f[i1][j1]+(1 or 0)

状态计算:

f [ i ] [ j ] = m i n ( f [ i − 1 ] [ j ] + 1 , f [ i − 1 ] [ j − 1 ] + 1 , f [ i − 1 ] [ j − 1 ] + ( 1   o r   0 ) ) f[i][j] = min(f[i-1][j] + 1, f[i-1][j-1] + 1, f[i-1][j-1] + (1\ or\ 0)) f[i][j]=min(f[i1][j]+1,f[i1][j1]+1,f[i1][j1]+(1 or 0))

注意这个题的初始化:

  • 当最初字符串 a a a 中没有字符,要匹配 b b b 中的字符串,只有对 a a a 进行添加操作,操作的次数为 b b b 的长度;
  • 当最初字符串 a a a 中存在字符,而 b b b 中不存在字符,要匹配 b b b 中的字符,只有对 a a a 进行删除操作,操作的次数为 a a a 的长度。
#include <iostream>
#include <algorithm>
using namespace std;

const int N = 1010;

int n, m;
char a[N], b[N];
int f[N][N];

int main() {
    scanf("%d%s%d%s", &n, a + 1, &m, b + 1);
    for (int i = 0; i <= m; i ++ ) f[0][i] = i;  // 因为状态方程中有i-1操作,所以需要从i=0初始化
    for (int i = 0; i <= n; i ++ ) f[i][0] = i;
    for (int i = 1; i <= n; i ++ )
        for (int j = 1; j <= m; j ++ ) {
            f[i][j] = min(f[i - 1][j], f[i][j - 1]) + 1;
            f[i][j] = min(f[i][j], f[i - 1][j - 1] + (a[i] != b[j]));
        }
    printf("%d\n", f[n][m]);
    return 0;
}

5 编辑距离

ACWing 899

#include <iostream>
#include <algorithm>
#include <cstring>
using namespace std;

const int N = 1010, M = 15;

int n, m, k;
char A[N][M], B[M];
int f[M][M];

bool check(char a[], char b[], int k) {
    int lena = strlen(a + 1), lenb = strlen(b + 1);
    for (int i = 0; i <= lena; i ++ ) f[i][0] = i;
    for (int i = 0; i <= lenb; i ++ ) f[0][i] = i;
    for (int i = 1; i <= lena; i ++ )
        for (int j = 1; j <= lenb; j ++ ) {
            f[i][j] = min(f[i - 1][j], f[i][j - 1]) + 1;
            f[i][j] = min(f[i][j], f[i - 1][j - 1] + (a[i] != b[j]));
        }
    return f[lena][lenb] > k ? false : true;
}

int main() {
    scanf("%d%d", &n, &m);
    for (int i = 1; i <= n; i ++ ) scanf("%s", A[i] + 1);
    while (m -- ) {
        int res = 0;
        scanf("%s%d", B + 1, &k);
        for (int i = 1; i <= n; i ++ )
            if (check(A[i], B, k)) res ++ ;
        printf("%d\n", res);
    }
    return 0;
}

6 怪盗基德的滑翔翼

ACwing 1017

算法思路: 两遍LIS,正向和反向,取最大 f [ i ] f[i] f[i]

#include <iostream>
#include <algorithm>
#include <cstring>
using namespace std;

const int N = 110;

int n;
int h[N], f[N];

int main() {
    int T; scanf("%d", &T);
    while (T--) {
        scanf("%d", &n);
        for (int i = 0; i < n; i++) scanf("%d", h + i);
        int res = 0;
        for (int i = 0; i < n; i++) {
            f[i] = 1;
            for (int j = 0; j < i; j++)
                if (h[j] < h[i]) f[i] = max(f[i], f[j] + 1);
            res = max(res, f[i]);
        }
        memset(f, 0, sizeof f);
        for (int i = n - 1; i >= 0; i--) {
            f[i] = 1;
            for (int j = n - 1; j > i; j--)
                if (h[j] < h[i]) f[i] = max(f[i], f[j] + 1);
            res = max(res, f[i]);
        }
        printf("%d\n", res);
    }
    return 0;
}

贪心优化版(快了一半多):

#include <iostream>
#include <algorithm>
#include <cstring>
using namespace std;

const int N = 110;

int n;
int h[N], q[N];

int get() {
    memset(q, 0, sizeof q);
    int len = 0;
    for (int i = 0; i < n; i++) {
        int l = 0, r = len;
        while (l < r) {
            int mid = (l + r + 1) >> 1;
            if (q[mid] < h[i]) l = mid;
            else r = mid - 1;
        }
        len = max(len, r + 1), q[r + 1] = h[i];
    }
    return len;
}

int main() {
    int T; scanf("%d", &T);
    while (T--) {
        scanf("%d", &n);
        for (int i = 0; i < n; i++) scanf("%d", h + i);
        int res = get();
        reverse(h, h + n);
        printf("%d\n", max(res, get()));
    }
    return 0;
}

7 登山

ACwing 1014

  • 条件一:按照编号递增的顺序来浏览 ==> 必须是子序列
  • 条件二:相邻两个景点不能相同
  • 条件三:一旦开始下降,就不能上升了==> 先严格单调上升,再严格单调下降在这里插入图片描述
  • 目标:求最多能浏览多少景点 ==> 求出所有形状是上面这种的子序列长度的最大值

集合划分:

按照上图中的尖点在图中什么位置来划分,则有下图
在这里插入图片描述

不失一般性,我们求第k类的最大长度。由题意可知,图中尖点左右两边的长度是互不相关的,因此我们只需要使左边长度最大和右边长度最大,最后求和即可。

#include <iostream>
#include <algorithm>
using namespace std;

const int N = 1010;

int n;
int h[N], f1[N], f2[N];

int main() {
    scanf("%d", &n);
    for (int i = 0; i < n; i++) scanf("%d", h + i);
    for (int i = 0; i < n; i++) {
        f1[i] = 1;
        for (int j = 0; j < i; j++)
            if (h[j] < h[i])
                f1[i] = max(f1[i], f1[j] + 1);
    }
    for (int i = n - 1; i >= 0; i--) {
        f2[i] = 1;
        for (int j = n - 1; j > i; j--)
            if (h[j] < h[i])
                f2[i] = max(f2[i], f2[j] + 1);
    }
    int res = 0;
    for (int i = 0; i < n; i++) res = max(res, f1[i] + f2[i] - 1);
    printf("%d\n", res);
    return 0;
}

8 合唱队形

ACWing 482

思路跟上面一道题一样,这道题问至少要剔除多少名同学,也就是要到一个最长的上升下降的子序列。

#include <iostream>
#include <algorithm>
using namespace std;

const int N = 1010;

int n;
int h[N], f1[N], f2[N];

int main() {
    scanf("%d", &n);
    for (int i = 0; i < n; i++) scanf("%d", h + i);
    for (int i = 0; i < n; i++) {
        f1[i] = 1;
        for (int j = 0; j < i; j++)
            if (h[j] < h[i])
                f1[i] = max(f1[i], f1[j] + 1);
    }
    for (int i = n - 1; i >= 0; i--) {
        f2[i] = 1;
        for (int j = n - 1; j > i; j--)
            if (h[j] < h[i])
                f2[i] = max(f2[i], f2[j] + 1);
    }
    int res = 0;
    for (int i = 0; i < n; i++) res = max(res, f1[i] + f2[i] - 1);
    printf("%d\n", n - res);
    return 0;
}

9 友好城市

ACWing 1012

算法思路:
在这里插入图片描述

  • 合法的建桥方式:
    • 每个城市上只能建立一座桥;
    • 两桥之间不能相交。
  • 上图两个集合圆中,两个关系是一一对应的。即:每一种合法的建桥方式都对应着一种上升子序列;每一种上升子序列都对应一种合法的建桥方式。
  • 求最多能建多少座桥?也就是先将自变量排序,再按照自变量对应的因变量序列中,寻找最长上升子序即为最多的建桥数量,也就转换成了LIS问题
#include <iostream>
#include <algorithm>
using namespace std;

#define x first
#define y second

typedef pair<int, int> PII;
const int N = 5010;

int n;
PII a[N];
int f[N];

int main() {
    scanf("%d", &n);
    for (int i = 0; i < n; i++) scanf("%d%d", &a[i].x, &a[i].y);
    sort(a, a + n);
    int res = 0;
    for (int i = 0; i < n; i++) {
        f[i] = 1;
        for (int j = 0; j < i; j++)
            if (a[j].y < a[i].y)
                f[i] = max(f[i], f[j] + 1);
        res = max(res, f[i]);
    }
    printf("%d\n", res);
    return 0;
}

9.1 不相交的线

LeetCode 1035

这个题直接就是单纯的LCS问题,与ACwing上的题有点区别。

class Solution {
public:
    int maxUncrossedLines(vector<int>& nums1, vector<int>& nums2) {
        int n = nums1.size(), m = nums2.size();
        int a[n + 1], b[m + 1]; memset(a, 0, sizeof a); memset(b, 0, sizeof b);
        for (int i = 0; i < n; i ++ ) a[i + 1] = nums1[i];
        for (int i = 0; i < m; i ++ ) b[i + 1] = nums2[i];
        int f[n + 1][m + 1]; memset(f, 0, sizeof f);
        for (int i = 1; i <= n; i ++ )
            for (int j = 1; j <= m; j ++ )
                if (a[i] == b[j]) f[i][j] = f[i - 1][j - 1] + 1;
                else f[i][j] = max(f[i - 1][j], f[i][j - 1]);
        return f[n][m];
    }
};

10 最大上升子序列和

ACWing 1016

集合

f[i]:所有以a[i]结尾的上升子序列的和的最大值

集合划分:

在这里插入图片描述

状态计算:

f[i] = max(f[i], f[j] + a[i]);

#include <iostream>
#include <algorithm>
using namespace std;

const int N = 1010;

int n;
int a[N], f[N];

int main() {
    scanf("%d", &n);
    for (int i = 0; i < n; i++) scanf("%d", a + i);
    int res = 0;
    for (int i = 0; i < n; i++) {
        f[i] = a[i];
        for (int j = 0; j < i; j++)
            if (a[j] < a[i]) f[i] = max(f[i], f[j] + a[i]);
        res = max(res, f[i]);
    }
    printf("%d\n", res);
    return 0;
}

11 最长递增子序列的个数

LeetCode 673

需要多使用一个数组g[i]来存储以nums[i]结尾的最长上升子序列的的个数。

  • 每个数都可以独自一个成为子序列,所以起始有g[i] = 1
  • 对区间[0, i)的所有数nums[j],如果满足nums[j] < nums[i],说明nums[i]可以接在nums[j]的后面形成上升子序列,这时候对f[i]f[j] + 1的大小关系进行讨论:
    • 如果f[i] < f[j] + 1,说明最长递增子序列的长度增加了,长度增加,但是数量不变,g[i] = g[j]
    • 如果f[i] = f[j] + 1,说明最长递增子序列的长度并没有增加,但是出现了长度一样的情况,故数量增加g[i] += g[j]

在转移的过程中要记录全局的最长上升子序列的最大长度max,最后结果为所有满足f[i] = maxg[i]的累加值。

class Solution {
public:
    int findNumberOfLIS(vector<int> &nums) {
        int n = nums.size(), maxLen = 0;
        vector<int> f(n), g(n);
        for (int i = 0; i < n; i++) {
            f[i] = g[i] = 1;
            for (int j = 0; j < i; j++)
                if (nums[j] < nums[i]) {
                    if (f[i] < f[j] + 1) f[i] = f[j] + 1, g[i] = g[j];
                    else if (f[i] == f[j] + 1) g[i] += g[j];
                }
            maxLen = max(maxLen, f[i]);
        }
        int res = 0;
        for (int i = 0; i < n; i++)
            if (f[i] == maxLen) res += g[i];
        return res;
    }
};

12 拦截导弹

ACWing 1010

第一问就是经典的LIS问题;

第二问思路:

对于样例:389 207 155 300 299 170 158 65
第一个导弹拦截系统由389开头,然后对于207有两种选择。

  • 接在现有的某个子序列后面;
  • 创建一个新的导弹系统;

不管上面哪一种选择,将导弹207拦截之后,它都是现有的某个子序列的结尾,即可以表示成_____ 207,为了使后面拦截导弹后生成的子序列的结尾值尽可能的大,所以对于207应该添加到一个大于等于207的最小的现有子序列后面,如果现有的序列中所有的子序列结尾都小于207,那么我们就创建一个新的子序列。注意,这里不能使用二分查找位置,因为没有单调性。

总的来说:基于贪心思想,从前往后扫描每个数,对于每个数

  • 情况1:如果现有的子序列的结尾都小于当前数,则创建新的子序列
  • 情况2:否则将当前数放到结尾大于等于它的最小的子序列的后面。

这里结论的证明的参考链接

Tips:
由于本题没有告诉导弹的个数,所以在输入的时候要计数。这里可以使用两种方法:
方法一: while (cin >> q[n]) n ++ ;
方法二:使用stringstream

代码中数组 g [ N ] g[N] g[N] 存储所有现有的子序列的最后一个数

#include <iostream>
#include <algorithm>
using namespace std;

const int N = 1010;

int n;
int q[N], f[N], g[N];

int main() {
    while (scanf("%d", q + n) != EOF) n++;
    int res = 0;
    for (int i = 0; i < n; i++) { // 求最长不上升的子序列
        f[i] = 1;
        for (int j = 0; j < i; j++)
            if (q[j] >= q[i]) f[i] = max(f[i], f[j] + 1);
        res = max(res, f[i]);
    }
    printf("%d\n", res);
    int cnt = 0; // 当前子序列的个数
    for (int i = 0; i < n; i++) {
        int j = 0;
        while (j < cnt && g[j] < q[i]) j++;
        g[j] = q[i];
        if (j >= cnt) cnt++;
    }
    printf("%d\n", cnt);
    return 0;
}

13 导弹防御系统

ACWing 187

跟上一个题的第二问思路一样,不过要分情况讨论是放到上升子序列还是下降子序列,使用DFS遍历所有情况,ans来记录全局最小值。

#include <iostream>
#include <algorithm>
using namespace std;

const int N = 55;

int n;
int q[N];
int up[N], down[N]; // 上升子序列和下降子序列的结尾值
int ans;

// u枚举到的第几个数 su上升子序列的个数 sd下降子序列的个数
inline void dfs(int u, int su, int sd) {
    if (su + sd >= ans) return;
    if (u == n) {
        ans = su + sd; return;
    }
    int k = 0;
    while (k < su && up[k] > q[u]) k++;
    int t = up[k];
    up[k] = q[u];
    if (k < su) dfs(u + 1, su, sd); else dfs(u + 1, su + 1, sd);
    up[k] = t;

    k = 0;
    while (k < sd && down[k] < q[u]) k++;
    t = down[k];
    down[k] = q[u];
    if (k < sd) dfs(u + 1, su, sd); else dfs(u + 1, su, sd + 1);
    down[k] = t;
}

int main() {
    while (scanf("%d", &n), n) {
        for (int i = 0; i < n; i++) scanf("%d", q + i);
        ans = n;
        dfs(0, 0, 0);
        printf("%d\n", ans);
    }
    return 0;
}

14 最长公共上升子序列(LIS && LCS)

ACWing 272

集合
f[i, j]:所有由第一个序列的前i个字母,第二个序列的前j个字母构成的,且以a[i] or b[j]结尾的公共上升子序列

注:这个题目使用的集合是“所有由第一个序列的前i个字母,第二个序列的前j个字母构成的,且以b[j]结尾的公共上升子序列”。

集合划分:
在这里插入图片描述

  • 右边:所有不包含a[i]的公共上升子序列,即f[i-1, j]
  • 左边:所有包含a[i]的公共上升子序列;

对于左边的分析:
在这里插入图片描述

按照LIS的划分方法,左边的子序列表示为:在这里插入图片描述
由于左半部分表示的是所有包含a[i]的,且由第一个序列的前i个字母和第二个序列的前j个字母构成的公共上升子序列,现在进一步划分后加上一个限制,即以b[1]结尾,也就包含b序列的前一个字母。所以黄色方框里面所表示的含义为:由第一个序列的前 i 个字母和第二个序列的前 1 个字母构成的,且以b[1]结尾的所有的公共上升子序列。所以左半边的状态表示为f[i, 1] + 1,同理对于以b[k]结尾的状态表示为:f[i, k] + 1,这里的k ∈ [0, j-1]

朴素版本的代码:

#include <iostream>
#include <algorithm>
using namespace std;

const int N = 3010;

int n;
int a[N], b[N];
int f[N][N];

int main() {
    scanf("%d", &n);
    for (int i = 1; i <= n; i++) scanf("%d", &a[i]);
    for (int i = 1; i <= n; i++) scanf("%d", &b[i]);
    for (int i = 1; i <= n; i++)
        for (int j = 1; j <= n; j++) {
            f[i][j] = f[i - 1][j]; // 考虑右半边
            if (a[i] == b[j]) { // 左半边存在的前提条件
                f[i][j] = max(f[i][j], 1); // 更新左边只有一个数的时候
                for (int k = 1; k < j; k++)
                    if (b[k] < b[j]) // 左半边每种情况存在的前提:满足单调上升
                        f[i][j] = max(f[i][j], f[i - 1][k] + 1);
            }
        }
    int res = 0;
    for (int i = 1; i <= n; i++) res = max(res, f[n][i]);
    printf("%d\n", res);
    return 0;
}

优化代码:对代码做等价变形

  1. 代码第23行,a[i] == b[j],所以23行后的代码b[j]都可以换成a[i]
  2. 第一步之后,这个循环的含义发生了变化,
    for (int k = 1; k < j; k++)
    	if (b[k] < b[j]) 
        	f[i][j] = max(f[i][j], f[i - 1][k] + 1);
    
    含义是:找到从1j-1里面小于a[i]f[i][k]的最大值。因此这个条件与j无关,所以这个循环的含义也就是:在满足某个和j没有关系的条件的前提下,求出其某个前缀[1 ~ j )的最大值。所以可以使用一个变量maxv来记录前缀的最大值,这样可以省掉一层循环。于是第18行开始的代码可以修改为
    for (int i = 1; i <= n; i ++ ) { 
        int maxv = 1;
        for (int j = 1; j <= n; j ++ ) {
            f[i][j] = f[i - 1][j]; // 考虑右半边
            if (a[i] == b[j]) f[i][j] = max(f[i][j], maxv);
            if (b[j] < a[i]) maxv = max(maxv, f[i - 1][j] + 1);
        }
    } 
    

最后的代码:

#include <iostream>
#include <algorithm>
using namespace std;

const int N = 3010;

int n;
int a[N], b[N], f[N][N];

int main() {
    scanf("%d", &n);
    for (int i = 1; i <= n; i++) scanf("%d", a + i);
    for (int i = 1; i <= n; i++) scanf("%d", b + i);
    for (int i = 1; i <= n; i++) {
        int maxv = 1;
        for (int j = 1; j <= n; j++) {
            f[i][j] = f[i - 1][j];
            if (a[i] == b[j]) f[i][j] = max(f[i][j], maxv);
            if (b[j] < a[i]) maxv = max(maxv, f[i - 1][j] + 1);
        }
    }
    int res = 0;
    for (int i = 1; i <= n; i++) res = max(res, f[n][i]);
    printf("%d\n", res);
    return 0;
}

15 最大整除子集

LeetCode 368

f [ i ] f[i] f[i] 表示以第 i i i 个数字结尾的整除子集的大小
g [ i ] g[i] g[i] 记录转移状态

class Solution {
public:
    vector<int> largestDivisibleSubset(vector<int> &nums) {
        sort(nums.begin(), nums.end());
        int n = nums.size();
        vector<int> f(n, 0), g(n, 0);
        for (int i = 0; i < n; i++) {
            f[i] = g[i] = 1;
            for (int j = 0; j < i; j++)
                if (!(nums[i] % nums[j]))
                    if (f[j] + 1 > f[i]) f[i] = f[j] + 1, g[i] = j;
        }
        int idx = 0, maxn = -2e9;
        for (int i = 0; i < n; i++)
            if (f[i] > maxn) maxn = f[i], idx = i;
        maxn = f[idx];
        vector<int> res;
        while (res.size() != maxn) res.push_back(nums[idx]), idx = g[idx];
        return res;
    }
};

16 选元素

ACwing 4418

集合
f [ i ] [ j ] f[i][j] f[i][j]:从前 i i i个数中选择 j j j个数,且选择了第 i i i 个数的所有方案的集合中其集合的和的最大值 。

集合划分
f [ i ] [ j ] f[i][j] f[i][j] :当前已经第 j j j 个点在第 i i i 个位置,那么第 j − 1 j-1 j1 个点与第 k k k 个点之间的点的个数不能大于 k k k,假设倒数第二个点即第 j − 1 j-1 j1 个点的下标为 t t t,则有 i − k ≤ t < i i-k \le t < i ikt<i

状态转移
f [ i ] [ j ] = m a x ( f [ i ] [ j ] ,   f [ t ] [ j − 1 ] + a [ i ] ) f[i][j] = max(f[i][j],\, f[t][j - 1] + a[i]) f[i][j]=max(f[i][j],f[t][j1]+a[i]),其中 i − k ≤ t < i i-k \le t < i ikt<i

最终结果是在 n − k + 1 ∼ n n - k + 1 \sim n nk+1n 之间选择一个最后一个点取最大值即可。

#include <iostream>
#include <algorithm>
#include <cstring>
using namespace std;

typedef long long LL;
const int N = 210;

int n, k, m;
LL f[N][N];

int main() {
    scanf("%d%d%d", &n, &k, &m);
    memset(f, -0x3f, sizeof f);
    f[0][0] = 0;
    for (int i = 1; i <= n; i++) {
        int v; scanf("%d", &v);
        for (int j = 1; j <= m; j++) // 因为最后一个数必须需选择,所以至少有一个数
            for (int t = max(i - k, 0); t < i; t++)
                f[i][j] = max(f[i][j], f[t][j - 1] + v);
    }
    LL res = -1;
    for (int i = n - k + 1; i <= n; i++) res = max(res, f[i][m]);
    printf("%lld\n", res);
    return 0;
}

17 正则表达式

LeetCode 10

算法思路:DP

集合: f ( i , j ) f(i,j) f(i,j)表示是否存在 s 1 ∼ s i s_1\sim s_i s1si p 1 ∼ p j p_1 \sim p_j p1pj匹配的合法方案。

状态计算:

  • p j ≠ ∗ p_j \ne * pj= 时,判断 (si == pj || pj = '.') && f(i - 1, j - 1) 是否满足;
  • p j = ∗ p_j = * pj= 时,就需要枚举 ∗ * 能表示多少个字符,最后对所有情况取或运算||即可:
    • ∗ * 表示 0 0 0个字符,则需要满足 f(i, j - 2)
    • ∗ * 表示 1 1 1 个字符的时候,则需要满足 f(i-1, j-2) && si == pj
    • ∗ * 表示 2 2 2 个字符的时候,则需要满足 f(i - 2, j - 2) && si == pj && s(i - 1) == p(j - 1)
    • 后面依次递推…

时间复杂度:状态数量 n 2 n^2 n2,转移数量为 n n n,整个算法时间复杂度为 O ( n 3 ) O(n^3) O(n3)

优化

对于当 p j = ∗ p_j = * pj= 时所枚举的状态为: f ( i , j ) = f ( i , j − 2 ) ∣ ∣ f ( i − 1 , j − 2 ) & & s i = = p j − 1 ∣ ∣ f ( i − 2 , j − 2 ) & & s i = = p j − 1 & & s i − 1 = = p j − 2 . . . f ( i − 1 , j ) = f ( i − 1 , j − 2 ) ∣ ∣ f ( i − 2 , j − 2 ) & & s i − 1 = = p j − 2 ∣ ∣ f ( i − 3 , j − 2 ) & & s i − 1 = = p j − 2 & & s i − 2 = = p j − 3 . . . ⇒ f ( i , j ) = f ( i , j − 2 ) ∣ ∣ f ( i − 1 , j ) & & s i = = p j − 1 \begin{aligned} &f(i, j) = f(i, j - 2) || f(i - 1, j - 2) \&\& s_i == p_{j-1} || f(i-2,j-2) \&\&s_i == p_{j-1} \&\&s_{i-1} == p_{j-2}... \\ &f(i-1, j) = f(i-1, j - 2) || f(i - 2, j - 2) \&\& s_{i-1} == p_{j-2} || f(i-3,j-2) \&\&s_{i-1} == p_{j-2} \&\&s_{i-2} == p_{j-3}... \\ \Rightarrow &f(i,j) = f(i, j -2) || f(i-1, j) \&\& s_i == p_{j-1}\\ \end{aligned} f(i,j)=f(i,j2)∣∣f(i1,j2)&&si==pj1∣∣f(i2,j2)&&si==pj1&&si1==pj2...f(i1,j)=f(i1,j2)∣∣f(i2,j2)&&si1==pj2∣∣f(i3,j2)&&si1==pj2&&si2==pj3...f(i,j)=f(i,j2)∣∣f(i1,j)&&si==pj1

class Solution {
public:
    bool isMatch(string s, string p) {
        int n = s.size(), m = p.size();
        s = ' ' + s, p = ' ' + p; // 使其从下标1开始
        vector<vector<bool>> f(n + 1, vector<bool>(m + 1));
        f[0][0] = true;
        for (int i = 0; i <= n; i++)
            for (int j = 1; j <= m; j++) { // j 从0开始,f[i][0]是不可能匹配的,同时下面代码中i>0,因为i=0同样没意义
                if (j + 1 <= m && p[j + 1] == '*') continue; // 如果 p[j+1]='*',需要p[j]和p[j+1]看成一个整体
                if (i && p[j] != '*') f[i][j] = f[i - 1][j - 1] && (s[i] == p[j] || p[j] == '.');
                else if (p[j] == '*') f[i][j] = f[i][j - 2] || i && f[i - 1][j] && (s[i] == p[j - 1] || p[j - 1] == '.');
            }
        return f[n][m];
    }
};

18 不同骰子序列的数目

LeetCode 2318

序列计数问题,先往 DP 上想

两个条件等价于:

  • 相邻两个数之间互质
  • 相同数值的两个数之间,为了方便分析,假定相同数值的两个数中要求第二个数值之前的两个数与其不同

最长上升子序列只与前一个数有关,这个题与它前两个数相关。

三维DP

集合: f ( i , j , k ) f(i, j, k) f(i,j,k),长度为 i i i,且倒数第二个元素为 j j j,倒数第一个元素为 k k k 的所有合法方案数

集合划分:按照倒数第三个元素划分

假设倒数第三个元素为 p p p,那么需要满足两个条件

  1. g c d ( p , j ) = 1 gcd(p, j) = 1 gcd(p,j)=1
  2. p ≠ k p \ne k p=k p ≠ j p \ne j p=j

那么就有状态转移方程 f ( i , j , k ) = f ( i − 1 , p , j ) f(i, j, k) = f(i - 1, p, j) f(i,j,k)=f(i1,p,j)

static const int N = 10010, MOD = 1e9 + 7;
int f[N][10][10];

class Solution {
public:
    int gcd(int a, int b) {
        return b ? gcd(b, a % b) : a;
    }

    int distinctSequences(int n) {
        memset(f, 0, sizeof f);
        if (n == 1) return 6;
        for (int i = 1; i <= 6; i++) // 初始化长度为2的方案
            for (int j = 1; j <= 6; j++)
                if (gcd(i, j) == 1 && i != j)
                    f[2][i][j] = 1;
        for (int i = 3; i <= n; i++)
            for (int j = 1; j <= 6; j++)
                for (int k = 1; k <= 6; k++)
                    if (j != k && gcd(j, k) == 1)
                        for (int p = 1; p <= 6; p++) // 枚举倒数第三个元素
                            if (p != j && p != k && gcd(p, j) == 1)
                                f[i][j][k] = (f[i][j][k] + f[i - 1][p][j]) % MOD;
        int res = 0;
        for (int i = 1; i <= 6; i++)
            for (int j = 1; j <= 6; j++)
                res = (res + f[n][i][j]) % MOD;
        return res;
    }
};

二维DP参考题解

集合: f [ i ] [ j ] f[i][j] f[i][j] 表示序列长度为 i i i,最后一个元素为 j j j 时的序列个数

类似于三维情况,枚举倒数第二个元素 k k k,其必须满足

  • k ≠ j k \ne j k=j
  • g c d ( k , j ) = 1 gcd(k, j) = 1 gcd(k,j)=1

f [ i ] [ 4 ] f[i][4] f[i][4] 为例,这时 k k k 只能取 1 、 3 1、3 13 5 5 5。如果直接将 f [ i − 1 ] [ 1 / 3 / 5 ] f[i - 1][1/3/5] f[i1][1/3/5] 转移到 f [ i ] [ 4 ] f[i][4] f[i][4] 上,会得到一个错误的转移方程

f [ i ] [ 4 ] = ∑ { f [ i − 1 ] [ 1 ] f [ i − 1 ] [ 3 ] f [ i − 1 ] [ 5 ] f[i][4] = \sum \begin{cases} &f[i - 1][1]\\ &f[i - 1][3]\\ &f[i - 1][5]\\ \end{cases} f[i][4]= f[i1][1]f[i1][3]f[i1][5]

原因是第 i − 2 i-2 i2 个元素不能为 4 4 4,但是在计算 f [ i − 1 ] [ 1 / 3 / 5 ] f[i - 1][1/3/5] f[i1][1/3/5] 的时候,第 i − 2 i-2 i2 个元素是可以为 4 4 4 的。

倘若将 f [ i − 2 ] [ 4 ] f[i-2][4] f[i2][4] 都减掉,这时仍然会得到一个错误的转移方程

f [ i ] [ 4 ] = ∑ { f [ i − 1 ] [ 1 ] − f [ i − 2 ] [ 4 ] f [ i − 1 ] [ 3 ] − f [ i − 2 ] [ 4 ] f [ i − 1 ] [ 5 ] − f [ i − 2 ] [ 4 ] f[i][4] = \sum \begin{cases} &f[i - 1][1] - f[i-2][4]\\ &f[i - 1][3] - f[i-2][4]\\ &f[i - 1][5] - f[i-2][4]\\ \end{cases} f[i][4]= f[i1][1]f[i2][4]f[i1][3]f[i2][4]f[i1][5]f[i2][4]

这是因为:

  • f [ i − 1 ] [ 1 ] f[i-1][1] f[i1][1] 对应的序列,不包含第 i − 3 i-3 i3 个数为 1 1 1 的情况;
  • f [ i − 1 ] [ 3 ] f[i-1][3] f[i1][3] 对应的序列,不包含第 i − 3 i-3 i3 个数为 3 3 3 的情况;
  • f [ i − 1 ] [ 5 ] f[i-1][5] f[i1][5] 对应的序列,不包含第 i − 3 i-3 i3 个数为 5 5 5 的情况;
  • f [ i − 2 ] [ 4 ] f[i-2][4] f[i2][4] 对应的序列,包含第 i − 3 i-3 i3 个数为 1 / 3 / 5 1/3/5 1/3/5 的情况

注意到上面粗体字所对应的序列,又恰好组成了一个 f [ i − 2 ] [ 4 ] f[i-2][4] f[i2][4],所以对于前三项缺失的情况,只需要添加一个 f [ i − 2 ] [ 4 ] f[i-2][4] f[i2][4] 就能得到正确的转移方程

f [ i ] [ 4 ] = ∑ { f [ i − 1 ] [ 1 ] − f [ i − 2 ] [ 4 ] f [ i − 1 ] [ 3 ] − f [ i − 2 ] [ 4 ] f [ i − 1 ] [ 5 ] − f [ i − 2 ] [ 4 ] + f [ i − 2 ] [ 4 ] f[i][4] = \sum \begin{cases} &f[i - 1][1] - f[i-2][4]\\ &f[i - 1][3] - f[i-2][4]\\ &f[i - 1][5] - f[i-2][4]\\ \end{cases} +f[i-2][4]\\ f[i][4]= f[i1][1]f[i2][4]f[i1][3]f[i2][4]f[i1][5]f[i2][4]+f[i2][4]

一般化后,转移方程为

f [ i ] [ j ] = ( ∑ k ≠ j g c d ( k , j ) = 1 ( f [ i − 1 ] [ k ] − f [ i − 2 ] [ j ] ) ) + f [ i − 2 ] [ j ] f[i][j]=\left(\sum_{\substack{k \neq j \\gcd(k,\,j) = 1}}(f[i-1][k]-f[i-2][j])\right)+f[i-2][j] f[i][j]= k=jgcd(k,j)=1(f[i1][k]f[i2][j]) +f[i2][j]

一个特殊的边界是当 i = 3 i=3 i=3 的时候, i − 3 i-3 i3 不存在,故不需要多加一个 f [ i − 2 ] [ j ] f[i-2][j] f[i2][j]

typedef long long LL;
static const int MOD = 1e9 + 7, N = 1e4 + 10;
int f[N][6];

class Solution {
public:
    int distinctSequences(int n) {
        memset(f, 0, sizeof f);
        for (int i = 0; i < 6; i++) f[1][i] = 1;
        for (int i = 2; i <= n; i++)
            for (int j = 0; j < 6; j++) {
                LL s = 0;
                for (int k = 0; k < 6; k++)
                    if (k != j && gcd(k + 1, j + 1) == 1)
                        s += f[i - 1][k] - f[i - 2][j];
                if (i > 3) s += f[i - 2][j];
                f[i][j] = s % MOD;
            }
        LL res = 0;
        for (int v: f[n]) res += v;
        return (res % MOD + MOD) % MOD;
    }
};

19 最长ZigZag子序列

ACwing 3505

f [ i ] f[i] f[i]:表示 x i − x i + 1 > 0 x_i - x_{i + 1} > 0 xixi+1>0 的最长子序列长度;
g [ i ] g[i] g[i]:表示 x i − x i + 1 < 0 x_i - x_{i + 1} < 0 xixi+1<0 的最长子序列长度。

#include <iostream>
#include <algorithm>
using namespace std;

const int N = 55;

int n;
int w[N], f[N], g[N];

int main() {
    scanf("%d", &n);
    for (int i = 0; i < n; i++) scanf("%d", w + i);
    int res = 0;
    for (int i = 0; i < n; i++) {
        f[i] = g[i] = 1;
        for (int j = 0; j < i; j++)
            if (w[j] > w[i]) f[i] = max(f[i], g[j] + 1);
            else if (w[j] < w[i]) g[i] = max(g[i], f[j] + 1);
        res = max(res, max(f[i], g[i]));
    }
    printf("%d\n", res);
    return 0;
}

20 校门外的树

ACwing 3414

题目大意:有一个数轴,数轴上已经存在一些点,现在在除两个端点之外选择一些点,使得两个端点、以及选择的点之间构成区间。然后在这些区间之间再选择一些以前没有选择的点(至少要选择一个点),使得每个区间各自分别被等分。问这种划分方案一共有多少关系?

等分 意味着 整除,即约数

可见,第一步选择一些点与后面再选择一些点等分区间是乘法关系。

选择问题往 DP 上想(背包问题:有限制的选择问题)。

状态表示
集合 f [ i ] f[i] f[i]:区间 a 0 ∼ a i a_0 \sim a_i a0ai 上所有选法的集合 ( a i a_i ai 是指第 i i i 个障碍物的坐标)
属性:数量

集合划分
根据最后一个区间左端点 j   ( j ∈ [ 0 , i − 1 ] ) j\ (j \in [0, i - 1]) j (j[0,i1]) 的位置,将集合划分为 i ( i = i − 1 − 0 + 1 ) i (i= i - 1 - 0 + 1) i(i=i10+1)

这种集合划分方案保证不重不漏每一种情况的证明。
不漏:易知这种划分方案不会漏掉任何一种情况。
不重:假设存在区间 [ a j ′ ,   a i ] 、 [ a j ,   a i ] [a_{j'}, \, a_i]、[a_j,\, a_i] [aj,ai][aj,ai],其中 a j ′ < a j a_{j'} < a_j aj<aj,由于要求每次选择等分点的时候都不能选择以前选择过的点,即对于区间 [ a j ′ ,   a i ] [a_{j'}, \, a_i] [aj,ai] 选择等分点的时候一定不可以选择 a j a_j aj。假设区间 [ a j ′ ,   a i ] [a_{j'}, \, a_i] [aj,ai] 每一段等分大小为 d ′ d' d,区间 [ a j ,   a i ] [a_{j}, \, a_i] [aj,ai] 每一段等分大小为 d d d,并且 a j ′ 、 a j a_{j'}、a_j ajaj 都会存在于各自的等差数列里面,而等差数列的 d ≠ d ′ d \ne d' d=d (数组 s t st st 判重),所以在区间 [ a j ′ ,   a i ] [a_{j'}, \, a_i] [aj,ai] 选择等分点的时候一定不会选择到 a j a_j aj

状态计算
记区间 [ a j , a i ] [a_j, a_i] [aj,ai] 之间的距离为 d d d,将区间等分,其每段距离记为 k k k。由于必须至少要划分为两段,则 k k k 应该取除 d d d 本身之外的所有 d d d 的约数,且选定的划分点不能与前面已经选择的点重合(使用数组 s t st st 判重),则在区间 [ a j , a i ] [a_j, a_i] [aj,ai] 上的所有方案为 ∑ k \sum k k
f [ i ] = f [ i ] + f [ j ] × ∑ k f[i] = f[i] + f[j] \times \sum k f[i]=f[i]+f[j]×k
注:每个区间之间的划分方案互不相关,数量是乘积关系

#include <iostream>
#include <algorithm>
#include <cstring>
#include <vector>
using namespace std;

typedef long long LL;
const int N = 1010, M = 100010, MOD = 1e9 + 7;

int n;
int a[N], f[N];
vector<int> q[M]; // 存储每个数的所有约数
bool st[M];

int main() {
    for (int i = 1; i < M; i++) // 每个数的所有约数
        for (int j = i * 2; j < M; j += i) // 不能取本身
            q[j].push_back(i);
    scanf("%d", &n);
    for (int i = 0; i < n; i++) scanf("%d", a + i);
    f[0] = 1;
    for (int i = 1; i < n; i++) {
        memset(st, 0, sizeof st);
        for (int j = i - 1; j >= 0; j--) {
            int d = a[i] - a[j], cnt = 0;
            for (int k: q[d]) if (!st[k]) cnt++, st[k] = true;
            st[d] = true; // 表示该距离上的点不可取
            f[i] = (f[i] + (LL) f[j] * cnt) % MOD;
        }
    }
    printf("%d\n", f[n - 1]);
    return 0;
}
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值