线性dp

目录
  • 线性dp
    • 1.算法分析
    • 2. 算法模板
    • 3. 典型例题
      • 3.1 LIS模型
        • 3.1.1 母题:最长上升子序列
        • 3.1.2 扩展1:最长上升子序列打印路径
        • 3.1.3 LIS的NlogN做法:贪心
        • 3.1.4 求^型序列(既要求最长上升,也要求最长下降)
        • 3.1.5 满足LIS性质的应用
        • 3.1.6 魔改最长上升子序列状态表示:最长上升子序列和
        • 3.1.7 LIS的区间覆盖问题
          • 3.1.7.1 用n个最长不下降子序列或者k个最长不上升子序列来覆盖区间:dilworth定理
          • 3.1.7.2 用n1个最长上升子序列和n2个最长下降子序列来覆盖区间:dfs
      • 3.2 最长公共子序列模型
        • 3.2.1 母题:LCS问题
        • 3.2.2 编辑距离问题
        • 3.2.3 最长公共上升子序列问题
      • 3.3 数字三角形模型
        • 3.3.1 母题:数字三角形
        • 3.3.2 摘花生问题
        • 3.3.3 方格取数问题
      • 3.4 扩展习题

线性dp

1.算法分析

    与数学的线性空间类似,如果一个动态规划算法的状态包括多个维度,但在每个维度上都具有线性变换的的阶段,那么该动态规划算法同样被称为线性dp。这类题目的特点是:DP的阶段沿着各个维度线性增长,从一个或多个边界点开始有方向地向整个状态空间转移、扩展,最后每个状态上都保留了以自身为目标的子问题的最优解。

    线性dp可以很典型地分为:

  • 最长上升子序列: 强调在一维情况下,当前状态与前面所有状态的关系
  • 最长公共子序列模型: 在二维情况下,当前状态与前面所有状态的关系
  • 数字三角形模型: 强调当前状态与前面几个状态的关系
- LIS问题 LCS问题 数字三角形问题
问题描述 给定一个长度为N的数列A,求数值单调递增的子序列的长度最长是多少。 给定两个长度分别为N和M的字符串A和B,求既是A的子序列又是B的子序列的字符串的最长长度是多少 给定一个共有N行的三角矩阵A,其中第i行有j列,从左上角出发,每次可以向下走或者右下方走一步,最终达到底部,求把经过的所有位置上的数加起来,和最大是多少。
状态表示 F[i]表示以A[i]为结尾的LIS的长度 F[i,j]表示前缀子串A[1~i]和B[1~j]的LCS长度 F[i][j]表示位置为(i, j)时的最大和
阶段划分 子序列的结尾位置 已经处理的前缀长度 上一步的位置
转移方程 F[i] = max{F[j] + 1} F[i, j] = max{F[i- 1, j], F[i, j - 1], F[i - 1, j - 1] + 1(A[i] == B[j])} F[i, j] = max(F[i - 1, j], F[i, j - 1]) + A[i, j]
边界 f[i]= 1 F[i, 0] = 0, F[0, j] = 0 F[1, 1] = A[1][1]
目标 max{F[i]} F[N][M] max{F[N, j]}

2. 算法模板

3. 典型例题

3.1 LIS模型

3.1.1 母题:最长上升子序列

acwing895. 最长上升子序列

给定一个长度为N的数列,求数值严格单调递增的子序列的长度最长是多少。

输入格式

第一行包含整数N。

第二行包含N个整数,表示完整序列。

输出格式

输出一个整数,表示最大长度。

数据范围

1≤N≤1000,
−109≤数列中的数≤109

输入样例:

7
3 1 2 1 8 5 6

输出样例:

4

#include 

using namespace std;

int const N = 1e3 + 10;
int f[N], a[N];
int n;

int main()
{
    cin >> n;
    for (int i = 1; i <= n; ++i) scanf("%d", &a[i]);
    
    memset(f, 0, sizeof f);
    
    int res = 0;  // 记录答案
    for (int i = 1; i <= n; ++i)
    {
        f[i] = 1;  // 处理边界
        for (int j = 1; j <= i - 1; ++j)  // 以子状态划分
            if (a[i] > a[j])  // 满足递增条件
                f[i] = max(f[i], f[j] + 1);
                
        // 已经更新完f[i]了, 更新答案
        res = max(res, f[i]); 
    }
    
    cout << res << endl;
    
    return 0;
}

3.1.2 扩展1:最长上升子序列打印路径

/*
如果想要记录路径,那么需要一个数组g[]来记录当前状态是由哪个状态转移过来的,
g[i]=j表示i状态是由j状态转移过来的
在计算g数组的时候从前往后,那么打印的时候就必须从后往前,必须和更新的顺序相反,这样才能满足dag的性质
*/

#include 

using namespace std;

int const N = 1e3 + 10;
int f[N], a[N], g[N];  // g[i]=j记录i是从j的位置转移过来的
int n;

int main()
{
    cin >> n;
    for (int i = 1; i <= n; ++i) scanf("%d", &a[i]);
    
    memset(f, 0, sizeof f);
    
    int res = 0;  // 记录答案
    for (int i = 1; i <= n; ++i)
    {
        f[i] = 1;  // 处理边界
        for (int j = 1; j <= i - 1; ++j)  // 以子状态划分
            if (a[i] > a[j])  // 满足递增条件
            {  
                if (f[j] + 1 > f[i])  // 能够更新
                {
                    f[i] = f[j] + 1;
                    g[i] = j;  // 记录转移
                    res = max(res, f[i]);
                }
            }
                
        // 已经更新完f[i]了, 更新答案
        res = max(res, f[i]); 
    }
    
    // 找到最大值
    vector path;
    int pos = -1, maxi = -1;
    for (int i = 1; i <= n; ++i) 
        if (f[i] > maxi)
        {
            maxi = f[i];
            pos = i;
        }
    
    // 得到路径
    while(pos)
    {
        path.push_back(pos);
        pos = g[pos];
    }
    reverse(path.begin(), path.end());
    
    // 打印
    cout << res << endl;
    for (auto p: path) cout << p << " ";
    
    return 0;
}

3.1.3 LIS的NlogN做法:贪心

/*
贪心的算法时间是O(NlogN),缺点在于只能求出LIS的长度,求不出路径和距离的LIS内的值
算法思想在于要让LIS尽可能长,那么要让最后的值尽可能小
*/
#include 

using namespace std;

int const N = 1e3 + 10;
vector low;
int a[N], n;

int main()
{
    cin >> n;
    for (int i = 1; i <= n; ++i) scanf("%d", &a[i]);

    for (int i = 1; i <= n; ++i)
    {
        if (low.empty() || low.back() < a[i]) low.push_back(a[i]);  // 空的或者最大的也比a[i]小
        else   // 寻找第一个大于等于a[i]的值,替换
        {
            int pos = lower_bound(low.begin(), low.end(), a[i]) - low.begin();
            low[pos] = a[i];
        }
    }

    cout << low.size() << endl;  // 大于长度
    return 0;
}

3.1.4 求^型序列(既要求最长上升,也要求最长下降)

acwing1017怪盗基德的滑翔翼

怪盗基德是一个充满传奇色彩的怪盗,专门以珠宝为目标的超级盗窃犯。
而他最为突出的地方,就是他每次都能逃脱中村警部的重重围堵,而这也很大程度上是多亏了他随身携带的便于操作的滑翔翼。
有一天,怪盗基德像往常一样偷走了一颗珍贵的钻石,不料却被柯南小朋友识破了伪装,而他的滑翔翼的动力装置也被柯南踢出的足球破坏了。
不得已,怪盗基德只能操作受损的滑翔翼逃脱。
假设城市中一共有N幢建筑排成一条线,每幢建筑的高度各不相同。
初始时,怪盗基德可以在任何一幢建筑的顶端。
他可以选择一个方向逃跑,但是不能中途改变方向(因为中森警部会在后面追击)。
因为滑翔翼动力装置受损,他只能往下滑行(即:只能从较高的建筑滑翔到较低的建筑)。
他希望尽可能多地经过不同建筑的顶部,这样可以减缓下降时的冲击力,减少受伤的可能性。
请问,他最多可以经过多少幢不同建筑的顶部(包含初始时的建筑)?

输入格式

输入数据第一行是一个整数K,代表有K组测试数据。

每组测试数据包含两行:第一行是一个整数N,代表有N幢建筑。第二行包含N个不同的整数,每一个对应一幢建筑的高度h,按照建筑的排列顺序给出。

输出格式

对于每一组测试数据,输出一行,包含一个整数,代表怪盗基德最多可以经过的建筑数量。

数据范围

1≤K≤100,
1≤N≤100,
0

输入样例:

3

8

300 207 155 299 298 170 158 65

8

65 158 170 298 299 155 207 300

10

2 1 3 4 5 6 7 8 9 10

输出样例:

6

6

9

/*
求最长上升子序列和最长下降子序列(直接把a数组reverse一下即可)
*/
#include 

using namespace std;

int const N = 1e2 + 10;
vector low;
int a[N], n;

// 得到最长上升(下降)子序列
int get_lis(int flg)
{
    if (flg) reverse(a + 1, a + n + 1);
    low.clear();
    for (int i = 1; i <= n; ++i)
    {
        if (low.empty() || low.back() < a[i]) low.push_back(a[i]);  // 空的或者最大的也比a[i]小
        else   // 寻找第一个大于等于a[i]的值,替换
        {
            int pos = lower_bound(low.begin(), low.end(), a[i]) - low.begin();
            low[pos] = a[i];
        }
    }

    return low.size();
}

int main()
{
    freopen("in.txt", "r", stdin);
    freopen("out.txt", "w", stdout);
    int t;
    cin >> t;
    while (t--)
    {
        low.clear();
        cin >> n;
        for (int i = 1; i <= n; ++i) scanf("%d", &a[i]);
        int res1 = get_lis(0), res2 = get_lis(1);
        cout << max(res1, res2) << endl;
    }
    
    return 0;
}

acwing1014.登山

五一到了,ACM队组织大家去登山观光,队员们发现山上一个有N个景点,并且决定按照顺序来浏览这些景点,即每次所浏览景点的编号都要大于前一个浏览景点的编号。
同时队员们还有另一个登山习惯,就是不连续浏览海拔相同的两个景点,并且一旦开始下山,就不再向上走了。
队员们希望在满足上面条件的同时,尽可能多的浏览景点,你能帮他们找出最多可能浏览的景点数么?

输入格式

第一行包含整数N,表示景点数量。
第二行包含N个整数,表示每个景点的海拔。

输出格式

输出一个整数,表示最多能浏览的景点数。

数据范围

2≤N≤1000

输入样例:

8

186 186 150 200 160 130 197 220

输出样例:
4

/*
f[i]表示正向的、以i结尾的最长上升子序列的长度,g[i]表示反向的、以i结尾的最长上升子序列(正向的、以i开头的最长下降子序列)
*/
#include 

using namespace std;

int const N =1e3 + 10;
int f[N], g[N], a[N], n;

int main()
{
    // freopen("in.txt", "r", stdin);
    // freopen("out.txt", "w", stdout);
    cin >> n;
    for (int i = 1; i <= n; ++i) scanf("%d", &a[i]);

    // 计算f[i]
    for (int i = 1; i <= n; ++i)
    {
        f[i] = 1;
        for (int j = 1; j < i; ++j)
            if (a[i] > a[j])
                f[i] = max(f[i], f[j] + 1);
    }

    // 计算g[i]
    for (int i = n; i >= 1; --i)
    {
        g[i] = 1;
        for (int j = n; j > i; --j)
            if (a[i] > a[j])
                g[i] = max(g[i], g[j] + 1);
    }

    // 找出最长的^型序列
    int res = -1;
    for (int i = 1; i <= n; ++i) res = max(res, f[i] + g[i] - 1);
    cout << res << endl;
    return 0;
}

3.1.5 满足LIS性质的应用

acwing1012. 友好城市

Palmia国有一条横贯东西的大河,河有笔直的南北两岸,岸上各有位置各不相同的N个城市。
北岸的每个城市有且仅有一个友好城市在南岸,而且不同城市的友好城市不相同。
每对友好城市都向政府申请在河上开辟一条直线航道连接两个城市,但是由于河上雾太大,政府决定避免任意两条航道交叉,以避免事故。
编程帮助政府做出一些批准和拒绝申请的决定,使得在保证任意两条航线不相交的情况下,被批准的申请尽量多。

输入格式

第1行,一个整数N,表示城市数。
第2行到第n+1行,每行两个整数,中间用1个空格隔开,分别表示南岸和北岸的一对友好城市的坐标。

输出格式

仅一行,输出一个整数,表示政府所能批准的最多申请数。

数据范围

1≤N≤5000,
0≤xi≤10000

输入样例:

7

22 4

2 6

10 3

15 12

9 8

17 17

4 2

输出样例:

4

/*
假设固定北岸的点为[1, 2, 3, 4],南岸和北岸配对的点为[3, 1, 2, 4],
那么要想不交叉的尽可能多,那么南岸的点需要满足lis性质,因为南岸的点如果不单调递增,必然存在交叉的情况
*/
#include 

using namespace std;

int n;
int const N = 1e4 + 10;
vector > v;
int a[N], f[N];

int main()
{
    freopen("in.txt", "r", stdin);
    freopen("out.txt", "w", stdout);
    cin >> n;
    for (int i = 1; i <= n; ++i)
    {
        int a, b;
        scanf("%d %d", &a, &b);
        v.push_back({a, b});
    }
    sort(v.begin(), v.end());
    for (int i = 0; i < n; ++i) a[i] = v[i].second;
    for (int i = 0; i < n; ++i)
    {
        f[i] = 1;
        for (int j = 0; j < i; ++j)
        {
            if (a[i] > a[j]) f[i] = max(f[i], f[j] + 1);
        }
    }
    int ans = 0;
    for (int i = 0; i < n; ++i) ans = max(ans, f[i]);
    cout << ans;
    return 0;
}

3.1.6 魔改最长上升子序列状态表示:最长上升子序列和

acwing1016. 最大上升子序列和

一个数的序列 bi,当 b1 对于给定的一个序列(a1,a2,…,aN),我们可以得到一些上升的子序列(ai1,ai2,…,aiK),这里1≤i1 比如,对于序列(1,7,3,5,9,4,8),有它的一些上升子序列,如(1,7),(3,4,8)等等。
这些子序列中和最大为18,为子序列(1,3,5,9)的和。
你的任务,就是对于给定的序列,求出最大上升子序列和。
注意,最长的上升子序列的和不一定是最大的,比如序列(100,1,2,3)的最大上升子序列和为100,而最长上升子序列为(1,2,3)。

输入格式

输入的第一行是序列的长度N。

第二行给出序列中的N个整数,这些整数的取值范围都在0到10000(可能重复)。

输出格式

输出一个整数,表示最大上升子序列和。

数据范围

1≤N≤1000

输入样例:

7

1 7 3 5 9 4 8

输出样例:

18

/*
求最长上升子序列和可以魔改最长上升子序列的状态定义,
f[i]表示以i结尾的最大最长上升子序列和
f[i] = max(f[j] + a[i])
*/
#include 

using namespace std;

int const N = 1e3 + 10;
int f[N], a[N], n;

int main()
{
    // freopen("in.txt", "r", stdin);
    // freopen("out.txt", "w", stdout);
    cin >> n;
    for (int i = 1; i <= n; ++i) scanf("%d", &a[i]);

    int res = 0;
    for (int i = 1; i <= n; ++i)
    {
        f[i] = a[i];
        for (int j = 1; j < i; ++j)
            if (a[i] > a[j])
                f[i] = max(f[i], f[j] + a[i]);
        res = max(res, f[i]);
    }

    cout << res << endl;
    return 0;
}

3.1.7 LIS的区间覆盖问题

3.1.7.1 用n个最长不下降子序列或者k个最长不上升子序列来覆盖区间:dilworth定理

dilworth定理:

  • 能覆盖整个序列的最少的不上升子序列的个数”等价于“该序列的最长上升子序列长度
  • 能覆盖整个序列的最少的不下降子序列的个数”等价于“该序列的最长下降子序列长度

acwing1010. 拦截导弹

某国为了防御敌国的导弹袭击,发展出一种导弹拦截系统。
但是这种导弹拦截系统有一个缺陷:虽然它的第一发炮弹能够到达任意的高度,但是以后每一发炮弹都不能高于前一发的高度。
某天,雷达捕捉到敌国的导弹来袭。
由于该系统还在试用阶段,所以只有一套系统,因此有可能不能拦截所有的导弹。
输入导弹依次飞来的高度(雷达给出的高度数据是不大于30000的正整数,导弹数不超过1000),计算这套系统最多能拦截多少导弹,如果要拦截所有导弹最少要配备多少套这种导弹拦截系统。

输入格式

共一行,输入导弹依次飞来的高度。

输出格式

第一行包含一个整数,表示最多能拦截的导弹数。

第二行包含一个整数,表示要拦截所有导弹最少要配备的系统数。

输入样例:

389 207 155 300 299 170 158 65

输出样例:

6

2

/*
第一问:求最长单调不上升子序列
第二问:求最少几个单调不上升子序列能够覆盖全部的序列

对于第一问,直接做lis即可;
对于第二问,依据dilworth定理,
能覆盖整个序列的最少的不上升子序列的个数”等价于“该序列的最长上升子序列长度,
能覆盖整个序列的最少的不下降子序列的个数”等价于“该序列的最长下降子序列长度
那么直接求最长上升子序列即可
*/
#include 

using namespace std;

int const N = 1e3 + 10;
int f[N], a[N], n, g[N];  // g[]求最长单调上升子序列,f[]求最长单调不上升子序列

int main()
{
    // freopen("in.txt", "r", stdin);
    // freopen("out.txt", "w", stdout);
    string s;
    getline(cin, s);
    stringstream ss(s);
    while (ss >> a[++n]);
    int res1 = 0, res2 = 0;
    for (int i = 1; i < n; ++i)
    {
        f[i] = 1;
        g[i] = 1;
        for (int j = 1; j < i; ++j)
        {
            if (a[i] > a[j])
                g[i] = max(g[i], g[j] + 1);
            if (a[i] <= a[j])
                f[i] = max(f[i], f[j] + 1);
        }
        res1 = max(res1, f[i]);
        res2 = max(res2, g[i]);
    }
    cout << res1 << endl << res2 << endl;
    return 0;
}
3.1.7.2 用n1个最长上升子序列和n2个最长下降子序列来覆盖区间:dfs

    这种情况下,没有什么好的方法,只能使用 dfs+剪枝 来枚举每个数是属于最长上升子序列还是最长下降子序列

acwing187. 导弹防御系统

为了对抗附近恶意国家的威胁,R国更新了他们的导弹防御系统。
一套防御系统的导弹拦截高度要么一直 严格单调 上升要么一直 严格单调 下降。
例如,一套系统先后拦截了高度为3和高度为4的两发导弹,那么接下来该系统就只能拦截高度大于4的导弹。
给定即将袭来的一系列导弹的高度,请你求出至少需要多少套防御系统,就可以将它们全部击落。

输入格式

输入包含多组测试用例。
对于每个测试用例,第一行包含整数n,表示来袭导弹数量。
第二行包含n个不同的整数,表示每个导弹的高度。
当输入测试用例n=0时,表示输入终止,且该用例无需处理。

输出格式

对于每个测试用例,输出一个占据一行的整数,表示所需的防御系统数量。

数据范围

1≤n≤50

输入样例:

5

3 5 2 4 1

0

输出样例:

2

样例解释

对于给出样例,最少需要两套防御系统。

一套击落高度为3,4的导弹,另一套击落高度为5,2,1的导弹。

/*
每个数字要不然属于上升序列,要不然属于下降序列,可以枚举每个数字属于上升还是下降序列
时间为O(2^n),剪枝后可以优化时间
*/
#include 

using namespace std;

int n;
int const N = 55;
int q[N], up[N], down[N];
int ans;

// u:当前枚举的数字下标,su:上升序列的个数,sd:下降序列的个数
void dfs(int u, int su, int sd)
{
    if (ans <= su + sd) return;  // 如果上升序列和下降序列的个数和大于答案,那么该分支不可能找到比答案小的了
    if (u == n) // 如果把所有数字都遍历完了
    {
        ans = su + sd;  // 更新答案
        return ;
    }
    
    int k = 0;
    // 情况1:插入上升队列
    while (k < su && q[u] <= up[k]) 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;  // 恢复

    // 情况2:插入下降队列
    k = 0;
    while (k < sd && q[u] >= down[k]) 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()
{
    // freopen("in.txt", "r", stdin);
    // freopen("out.txt", "w", stdout);
    while (scanf("%d", &n) && n != 0)
    {
        ans = n;  // 初始n为最大值
        for (int i = 0; i < n; ++i) scanf("%d", &q[i]);
        dfs(0, 0, 0);  // 从第0个数字开始枚举,当前0个上升序列,0个下降序列
        printf("%d\n", ans);
    }
    return 0;
}

3.2 最长公共子序列模型

3.2.1 母题:LCS问题

acwing897. 最长公共子序列

给定两个长度分别为N和M的字符串A和B,求既是A的子序列又是B的子序列的字符串长度最长是多少。

输入格式

第一行包含两个整数N和M。
第二行包含一个长度为N的字符串,表示字符串A。
第三行包含一个长度为M的字符串,表示字符串B。
字符串均由小写字母构成。

输出格式

输出一个整数,表示最大长度。

数据范围

1≤N≤1000,

输入样例:

4 5

acbd

abedc

输出样例:

3

/*

*/
#include 

using namespace std;

int const N = 1e3 + 10;
int f[N][N];
char a[N], b[N];
int n, m;

int main()
{
    freopen("in.txt", "r", stdin);
    freopen("out.txt", "w", stdout);
    cin >> n >> m;
    cin >> a + 1 >> b + 1;

    memset(f, 0, sizeof f);
    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]);
            if (a[i] == b[j]) f[i][j] = max(f[i][j], f[i - 1][j - 1] + 1);
        }
    }
    
    cout << f[n][m];
    return 0;
}

3.2.2 编辑距离问题

acwing902. 最短编辑距离

给定两个字符串A和B,现在要将A经过若干操作变为B,可进行的操作有:

  1. 删除–将字符串A中的某个字符删除。
  2. 插入–在字符串A的某个位置插入某个字符。
  3. 替换–将字符串A中的某个字符替换为另一个字符。
    现在请你求出,将A变为B至少需要进行多少次操作。

输入格式

第一行包含整数n,表示字符串A的长度。
第二行包含一个长度为n的字符串A。
第三行包含整数m,表示字符串B的长度。
第四行包含一个长度为m的字符串B。
字符串中均只包含大写字母。

输出格式

输出一个整数,表示最少操作次数。

数据范围

1≤n,m≤1000

输入样例:

10

AGTCTGACGC

11

AGTAAGTAGGC

输出样例:

4

/*
编辑距离问题:f[i][j]表示从a的前i个变成b的前j个需要的变换次数
状态划分为前缀的所有状态
因此,f[i][j] = min(min(f[i - 1][j] + 1, f[i][j - 1] + 1), f[i - 1][j - 1] + (a[i] != b[j]));
边界为:f[i][0] = i, f[0][j] = j, f[0][0] = 0
目标为:f[n][m]
*/
#include 

using namespace std;

int const N = 1e3 + 10;
char a[N], b[N];
int f[N][N];
int n, m;

int main()
{
    // freopen("in.txt", "r", stdin);
    // freopen("out.txt", "w", stdout);
    scanf("%d%s%d%s", &n, a + 1, &m, b + 1);
    
    memset(f, 0x3f, sizeof f);
    
    f[0][0] = 0;
    for (int i = 1; i <= n; ++i)
    {
        f[i][0] = i;
        for (int j = 1; j <= m; ++j)
        {
            f[0][j] = j;
            f[i][j] = min(min(f[i - 1][j] + 1, f[i][j - 1] + 1), f[i - 1][j - 1] + (a[i] != b[j]));
        }
    }

    cout << f[n][m] << endl;
    return 0;
}

3.2.3 最长公共上升子序列问题

acwing272. 最长公共上升子序列

熊大妈的奶牛在小沐沐的熏陶下开始研究信息题目。
小沐沐先让奶牛研究了最长上升子序列,再让他们研究了最长公共子序列,现在又让他们研究最长公共上升子序列了。
小沐沐说,对于两个数列A和B,如果它们都包含一段位置不一定连续的数,且数值是严格递增的,那么称这一段数是两个数列的公共上升子序列,而所有的公共上升子序列中最长的就是最长公共上升子序列了。
奶牛半懂不懂,小沐沐要你来告诉奶牛什么是最长公共上升子序列。
不过,只要告诉奶牛它的长度就可以了。
数列A和B的长度均不超过3000。

输入格式

第一行包含一个整数N,表示数列A,B的长度。
第二行包含N个整数,表示数列A。
第三行包含N个整数,表示数列B。

输出格式

输出一个整数,表示最长公共子序列的长度。

数据范围

1≤N≤3000,序列中的数字均不超过231−1

输入样例:

4

2 2 1 3

2 1 2 3

输出样例:

2

线性dp_第1张图片

/*
f[i][j]维护a的前i个字符,b的前j个字符,且b[j]必选的最长公共上升子序列的长度
那么划分子状态可以根据a[i]是否选来划分
如果不选a[i], 那么f[i][j] = f[i - 1][j]
如果选a[i],必须满足条件b[j] == a[i], f[i][j] = max(f[i][j], max{f[i - 1][k] + 1}) (1 <= k < j)
基于这个,我们可以维护一个maxi记录max{f[i - 1][k] + 1},初始时为1,然后每次更新完当前j时,把maxi更新一下
*/
#include 

using namespace std;

int const N = 3e3 + 10;
int f[N][N], a[N], b[N], n;  //f[i][j]维护a的前i个字符,b的前j个字符,且b[j]必选的最长公共上升子序列的长度

int main()
{
    cin >> n;
    for (int i = 1; i <= n; ++i) scanf("%d", &a[i]);
    for (int i = 1; i <= n; ++i) scanf("%d", &b[i]);
    
    memset(f, 0, sizeof f);
    for (int i = 1; i <= n; ++i)
    {
        int maxi = 1;  // maxi记录max{f[i - 1][k] + 1}
        for (int j = 1; j <= n; ++j)
        {
            f[i][j] = f[i - 1][j];  // 不选a[i];
            if (a[i] == b[j]) f[i][j] = max(f[i][j], maxi);  // 选a[i]
            if (a[i] > b[j]) maxi = max(maxi, f[i - 1][j] + 1);  // 枚举完当前j后更新maxi
        }
    }
    
    // 寻找答案
    int res = 0;
    for (int i = 1; i <= n; ++i) res = max(res, f[n][i]);
    cout << res << endl;
    return 0;
}

3.3 数字三角形模型

3.3.1 母题:数字三角形

acwing898. 数字三角形

给定一个如下图所示的数字三角形,从顶部出发,在每一结点可以选择移动至其左下方的结点或移动至其右下方的结点,一直走到底层,要求找出一条路径,使路径上的数字的和最大。

        7
      3   8
    8   1   0
  2   7   4   4
4   5   2   6   5

输入格式

第一行包含整数n,表示数字三角形的层数。
接下来n行,每行包含若干整数,其中第 i 行表示数字三角形第 i 层包含的整数。

输出格式

输出一个整数,表示最大的路径数字和。

数据范围

1≤n≤500,

−10000≤三角形中的整数≤10000

输入样例:

5

7

3 8

8 1 0

2 7 4 4

4 5 2 6 5

输出样例:

30

/*
*/
#include 

using namespace std;

int const N = 5e2 + 10;
int a[N][N], f[N][N], n;

int main()
{
    cin >> n;
    for (int i = 1; i <= n; ++i) 
        for (int j = 1; j <= i; ++j)
            scanf("%d", &a[i][j]);
    
    for (int i = 1; i <= n; ++i) f[n][i] = a[n][i];
    for (int i = n - 1; i >= 1; --i)
        for (int j = 1; j <= i; ++j)
            f[i][j] = max(f[i + 1][j], f[i + 1][j + 1]) + a[i][j];
        
    cout << f[1][1] << endl;
    return 0;
}

3.3.2 摘花生问题

acwing1015摘花生

Hello Kitty想摘点花生送给她喜欢的米老鼠。

她来到一片有网格状道路的矩形花生地(如下图),从西北角进去,东南角出来。

地里每个道路的交叉点上都有种着一株花生苗,上面有若干颗花生,经过一株花生苗就能摘走该它上面所有的花生。

Hello Kitty只能向东或向南走,不能向西或向北走。

问Hello Kitty最多能够摘到多少颗花生。

线性dp_第2张图片

输入格式

第一行是一个整数T,代表一共有多少组数据。

接下来是T组数据。

每组数据的第一行是两个整数,分别代表花生苗的行数R和列数 C。

每组数据的接下来R行数据,从北向南依次描述每行花生苗的情况。每行数据有C个整数,按从西向东的顺序描述了该行每株花生苗上的花生数目M。

输出格式

对每组输入数据,输出一行,内容为Hello Kitty能摘到得最多的花生颗数。

数据范围

1≤T≤100,

1≤R,C≤100,

0≤M≤1000

输入样例:

2

2 2

1 1

3 4

2 3

2 3 4

1 6 5

输出样例:

8

16

#include 

using namespace std;

int const N = 1e3 + 10;
int t;
int w[N][N], f[N][N];
int r, c;

int main()
{
    // freopen("in.txt", "r", stdin);
    // freopen("out.txt", "w", stdout);
    cin >> t;
    while (t--)
    {
        /* code */
        memset(f, 0, sizeof f);
        cin >> r >> c;
        for (int i = 1; i <= r; ++i)
            for (int j = 1; j <= c; ++j)
                scanf("%d", &w[i][j]);
        
        for (int i = 1; i <= r; ++i)
            for (int j = 1; j <= c; ++j)
                f[i][j] = max(f[i - 1][j], f[i][j - 1]) + w[i][j];
        
        printf("%d\n", f[r][c]);
    }
    return 0;
}

3.3.3 方格取数问题

acwing1027方格取数

设有 N×N 的方格图,我们在其中的某些方格中填入正整数,而其它的方格中则放入数字0。如下图所示:

线性dp_第3张图片

某人从图中的左上角 A 出发,可以向下行走,也可以向右行走,直到到达右下角的 B 点。

在走过的路上,他可以取走方格中的数(取走后的方格中将变为数字0)。

此人从 A 点到 B 点共走了两次,试找出两条这样的路径,使得取得的数字和为最大。

输入格式

第一行为一个整数N,表示 N×N 的方格图。

接下来的每行有三个整数,第一个为行号数,第二个为列号数,第三个为在该行、该列上所放的数。

一行“0 0 0”表示结束。

输出格式

输出一个整数,表示两条路径上取得的最大的和。

数据范围

N≤10

输入样例:

8

2 3 13

2 6 6

3 5 7

4 4 14

5 2 21

5 6 4

6 3 15

7 2 14

0 0 0

输出样例:

67

/*
可以假设两条路同时从(0, 0)开始,走的路径是k,那么一旦确定了一条路径的尾点的横坐标i1,就可以得到它的纵坐标k-i1
那么当前状态就可以使用f[k][i1][i2]来表示,当前的点的坐标为(i1, k - i1), (i2, k - i2)
只需特判两个点是否会重合即可
*/
#include 

using namespace std;

int const N = 20;
int n;
int w[N][N], f[N][N][N];

int main()
{
    // freopen("in.txt", "r", stdin);
    // freopen("out.txt", "w", stdout);
    cin >> n;
    int a, b, c;
    while (scanf("%d %d %d", &a, &b, &c) && (a != 0 && b != 0 && c != 0))
    {
        /* code */
        w[a][b] = c;
    }
    
    for (int k = 2; k <= n + n; ++k)  // k = i1 + j1 = i2 + j2,只有当k确定了,那么第一条路和第二条路才可能在某个点重合
        for (int i1 = 1; i1 <= n ; ++i1)  
            for (int i2 = 1; i2 <= n; ++i2)
            {
                int j1 = k - i1, j2 = k - i2;
                int t = w[i1][j1];  
                if (i1 != i2) t += w[i2][j2];  // 如果两条路径不重合,那么需要加两次
                if (j1 >= 1 && j1 <= n && j2 >= 1 && j2 <= n)
                {
                    // 枚举4个方向,1.i1向下,i2向下;2.i1向下,i2向右;3.i1向右,i2向下;4.i1向右,i2向右
                    int &x = f[k][i1][i2];
                    x = max (x, f[k - 1][i1 - 1][i2 - 1] + t);
                    x = max (x, f[k - 1][i1 - 1][i2] + t);
                    x = max (x, f[k - 1][i1][i2] + t);
                    x = max (x, f[k - 1][i1][i2 - 1] + t);
                }
            }
    cout << f[n + n][n][n] << endl;
    return 0;
}

3.4 扩展习题

acwing271. 杨老师的照相排列

有 N 个学生合影,站成左端对齐的 k 排,每排分别有 N1,N2,…,Nk 个人。 (N1≥N2≥…≥Nk)
第1排站在最后边,第 k 排站在最前边。
学生的身高互不相同,把他们从高到底依次标记为 1,2,…,N。
在合影时要求每一排从左到右身高递减,每一列从后到前身高也递减。
问一共有多少种安排合影位置的方案?
下面的一排三角矩阵给出了当 N=6,k=3,N1=3,N2=2,N3=1 时的全部16种合影方案。注意身高最高的是1,最低的是6。

123 123 124 124 125 125 126 126 134 134 135 135 136 136 145 146
45 46 35 36 34 36 34 35 25 26 24 26 24 25 26 25
6 5 6 5 6 4 5 4 6 5 6 4 5 4 3 3

输入格式

输入包含多组测试数据。

每组数据两行,第一行包含一个整数k表示总排数。

第二行包含k个整数,表示从后向前每排的具体人数。

当输入k=0的数据时,表示输入终止,且该数据无需处理。

输出格式

每组测试数据输出一个答案,表示不同安排的数量。

每个答案占一行。

数据范围

1≤k≤5,学生总人数不超过30人。

输入样例:

1

30

5

1 1 1 1 1

3

3 2 1

4

5 3 3 1

5

6 5 4 3 2

2

15 15

0

输出样例:

1

1

16

4158

141892608

9694845

/*
f[a1][a2][a3][a4][a5]代表第一排为a1人,第二排为a2人,第三排为a3人,第四排为a4人,第五排为a5人的方案数
那么以最好一个人的位置来划分
最后一个人可以在第1排,可以在第2排,。。。,可以在第5排
因此转移方程为:f[a1][a2][a3][a4][a5] = f[a1 - 1][a2][a3][a4][a5] + f[a1][a2 - 1][a3][a4][a5] + f[a1][a2][a3 - 1][a4][a5] + f[a1][a2][a3][a4 - 1][a5] + f[a1][a2][a3][a4][a5 - 1]
同时要满足一个性质,前一排的人数比后以前的多
*/
#include 

using namespace std;

typedef long long LL;

const int N = 31;

int n;
LL f[N][N][N][N][N];

int main()
{
    while (cin >> n, n)
    {
        int s[5] = {0};
        for (int i = 0; i < n; i ++ ) cin >> s[i];

        // 初始化
        memset(f, 0, sizeof f);
        f[0][0][0][0][0] = 1;

        // 状态转移
        for (int a = 0; a <= s[0]; a ++ )
            for (int b = 0; b <= min(a, s[1]); b ++ )
                for (int c = 0; c <= min(b, s[2]); c ++ )
                    for (int d = 0; d <= min(c, s[3]); d ++ )
                        for (int e = 0; e <= min(d, s[4]); e ++ )
                        {
                            LL &x = f[a][b][c][d][e];
                            if (a && a - 1 >= b) x += f[a - 1][b][c][d][e];
                            if (b && b - 1 >= c) x += f[a][b - 1][c][d][e];
                            if (c && c - 1 >= d) x += f[a][b][c - 1][d][e];
                            if (d && d - 1 >= e) x += f[a][b][c][d - 1][e];
                            if (e) x += f[a][b][c][d][e - 1];
                        }
        cout << f[s[0]][s[1]][s[2]][s[3]][s[4]] << endl;
    }

    return 0;
}

acwing273分级

给定长度为N的序列A,构造一个长度为N的序列B,满足:

1、B非严格单调,即B1≤B2≤…≤BN或B1≥B2≥…≥BN。
2、最小化 S=∑Ni=1|Ai−Bi|。

只需要求出这个最小值S。

输入格式

第一行包含一个整数N。

接下来N行,每行包含一个整数Ai。

输出格式

输出一个整数,表示最小S值。

数据范围

1≤N≤2000,
0≤Ai≤109

输入样例:

7

1

3

2

4

5

3

9

输出样例:

3

/*

本题可以证明:bi一定属于a数组

状态表示: 
f[i][j]: 已经选择了i项,第i项选择a[j]的最小值
状态划分:第i-1项的选择
状态转移:f[i,j] = min{f[i-1, k]} + abs(a[i] - a[j])  (a[j] >= a[k])
入口: f[0][0] = 0;
出口: min{f[n][i]}

如何计算出升序的bi和降序的bi:
升序的就正常做dp即可,因为从头开始那么就是升序;降序直接从尾开始到投,那么得到的答案就是降序,实现上就是做个reverse

编程时因为f[i-1,k]递增,所以可以维护一个minv来记录当前f[i-1,k]的最小值,这样子就能把O(N^3)->O(N^2)了

*/
#include

using namespace std;

int const N = 2e3 + 10;
int f[N][N], a[N], n, b[N], INF = 0x3f3f3f3f;

// 求B升序时的最小值
int dp()
{
    for (int i = 1; i <= n; ++i) b[i] = a[i];
    sort(b + 1, b + 1 + n);  // b[i]一定属于a数组,所以选的时候需要排序是为了保证b[j]>=b[k]

    for (int i = 1; i <= n; ++i)  // 枚举选择物品的个数
    {
        int minv = INF;  // 记录min{f[i-1, k]}
        for (int j = 1; j <= n; ++j)  // 枚举第i个的选择
        {
            minv = min(minv, f[i - 1][j]);  // 更新minv
            f[i][j] = minv + abs(a[i] - b[j]);  // 更新f[i, j]
        }
    }

    // 计算答案
    int res = INF;
    for (int i = 1; i <= n; ++i) res = min(res, f[n][i]);
    return res;
}

int main()
{
    freopen("in.txt", "r", stdin);
    freopen("out.txt", "w", stdout);

    cin >> n;
    for (int i = 1; i <= n; ++i) scanf("%d", &a[i]);
    
    int res = INF;
    res = min(res, dp());  // 升序
    reverse(a + 1, a + 1 + n);  // 反转之后再做一次升序就是降序
    res = min(res, dp());  // 降序

    cout << res << endl;
    return 0;
}

acwing274移动服务

一个公司有三个移动服务员,最初分别在位置1,2,3处。

如果某个位置(用一个整数表示)有一个请求,那么公司必须指派某名员工赶到那个地方去。

某一时刻只有一个员工能移动,且不允许在同样的位置出现两个员工。

从 p 到 q 移动一个员工,需要花费 c(p,q)。

这个函数不一定对称,但保证 c(p,p)=0。

给出N个请求,请求发生的位置分别为 p1~pN。

公司必须按顺序依次满足所有请求,目标是最小化公司花费,请你帮忙计算这个最小花费。

输入格式

第1行有两个整数L,N,其中L是位置数量,N是请求数量,每个位置从1到L编号。

第2至L+1行每行包含L个非负整数,第i+1行的第j个数表示c(i,j) ,并且它小于2000。

最后一行包含N个整数,是请求列表。

一开始三个服务员分别在位置1,2,3。

输出格式

输出一个整数M,表示最小花费。

数据范围

3≤L≤200,

1≤N≤1000

输入样例:

5 9

0 1 1 1 1

1 0 2 3 2

1 1 0 4 1

2 1 5 0 1

4 2 3 4 0

4 2 4 1 5 4 3 2 1

输出样例:

5

/*
本题可以很容易想到使用f[i][x][y][z]表示处理第i个问题时,3个服务员分别处于x,y,z时的最小花费,但是这样子就会爆内存
但是因为处理完第i个问题后,某个服务员就会处于p[i]的位置,所以我们可以节约1维的费用
使用f[i][x][y]表示当处理完第i个问题时,3个服务员分别处于x,y,p[i]的位置的最小花费
dp的状态转移:1.什么状态能够更新当前状态 2.当前状态能够更新什么状态 
由于能够更新当前状态的状态特别多,而当前状态能够更新的状态只有3个,所以本题使用一种特别的更新方式:使用当前状态更新其他状态
f[i][x][y]能够更新的状态有:
z = p[i]
1. 话务员x去处理问题i+1, f(i + 1, z, y) = f(i, x, y) + w(x, p(i + 1))
2. 话务员y去处理问题i+1, f(i + 1, x, z) = f(i, x, y) + w(y, p(i + 1))
3. 话务员z去处理问题i+1, f(i + 1, x, y) = f(i, x, y) + w(z, p(i + 1))

入口为f(0, 1, 2) = 0, z = p[0] = 3;
f(other) = 0x3f

出口为:min{f(n, x, y)}
*/
#include

using namespace std;

int const N = 2e2 + 10, M = 1e3 + 10, inf = 0x3f3f3f3f;
int g[N][N], p[M], l, n, f[M][N][N];

int main()
{
    // freopen("in.txt", "r", stdin);
    // freopen("out.txt", "w", stdout);

    // 读入
    cin >> l >> n;
    for (int i = 1; i <= l; ++i)
        for (int j = 1; j <= l; ++j)
            cin >> g[i][j];
    for (int i = 1; i <= n; ++i) scanf("%d", &p[i]);

    // 初始化
    memset(f, 0x3f, sizeof f);
    f[0][1][2] = 0;
    p[0] = 3;

    // 状态转移
    for (int i = 0; i <= n - 1; ++i)
    {
        for (int x = 1; x <= l; ++x)
        {
            for (int y = 1; y <= l; ++y)
            {
                int z = p[i], u = p[i + 1];
                
                if (z == x || z == y || x == y) continue;  // 不能出现在一起
                
                f[i + 1][z][y] = min(f[i + 1][z][y], f[i][x][y] + g[x][u]);
                f[i + 1][x][z] = min(f[i + 1][x][z], f[i][x][y] + g[y][u]);
                f[i + 1][x][y] = min(f[i + 1][x][y], f[i][x][y] + g[z][u]);
            }
        }
    }

    // 找答案
    int res = inf;
    for (int x = 1; x <= l; ++x)
        for (int y = 1; y <= l; ++y)
        {
            int z = p[n];
            
            if (z == x || z == y || x == y) continue;
            
            res = min(res, f[n][x][y]);
        }
    
    cout << res <

你可能感兴趣的:(线性dp)