线性dp

文章目录

  • 线性dp
    • 1.算法分析
    • 2. 典型例题
      • 2.1 LIS模型
        • 2.1.1 母题:最长上升子序列
        • 2.1.2 扩展1:最长上升子序列打印路径
        • 2.1.3 LIS的NlogN做法:贪心
        • 2.1.4 求^型序列(既要求最长上升,也要求最长下降)
        • 2.1.5 满足LIS性质的应用
        • 2.1.6 魔改最长上升子序列状态表示:最长上升子序列和
        • 2.1.7 LIS的区间覆盖问题
          • 2.1.7.1 用n个最长不下降子序列或者k个最长不上升子序列来覆盖区间:dilworth定理
          • 2.1.7.2 用n1个最长上升子序列和n2个最长下降子序列来覆盖区间:dfs
      • 2.2 最长公共子序列模型
        • 2.2.1 母题:LCS问题
        • 2.2.2 编辑距离问题
        • 2.2.3 最长公共上升子序列问题
      • 2.3 数字三角形模型
        • 2.2.1 母题:数字三角形
        • 2.2.2 摘花生问题
        • 2.2.3 方格取数问题
      • 2.4 扩展习题

线性dp

1.算法分析

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

    线性dp可以有几类典型模型:

  • 最长上升子序列: 强调在一维情况下,当前状态与前面所有状态的关系
  • 最长公共子序列模型: 在二维情况下,当前状态与前面所有状态的关系
  • 数字三角形模型: 强调当前状态与前面几个状态的关系

2. 典型例题

2.1 LIS模型

2.1.1 母题:最长上升子序列

acwing895. 最长上升子序列
题意: 给定一个长度为N的数列,求数值严格单调递增的子序列的长度最长是多少。
N < = 1 0 3 , 1 < = a i < = 1 0 9 N <= 10^3, 1 <= ai <= 10^9 N<=103,1<=ai<=109
代码:

#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;
}

2.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<int> 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;
}

2.1.3 LIS的NlogN做法:贪心

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

using namespace std;

int const N = 1e3 + 10;
vector<int> 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;
}

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

acwing1017怪盗基德的滑翔翼
题意: 假设城市中一共有N幢建筑排成一条线,每幢建筑的高度各不相同。初始时,怪盗基德可以在任何一幢建筑的顶端。他可以选择一个方向逃跑,但是不能中途改变方向(因为中森警部会在后面追击)。因为滑翔翼动力装置受损,他只能往下滑行(即:只能从较高的建筑滑翔到较低的建筑)。他希望尽可能多地经过不同建筑的顶部,这样可以减缓下降时的冲击力,减少受伤的可能性。
请问,他最多可以经过多少幢不同建筑的顶部(包含初始时的建筑)?
题解: 求最长上升子序列和最长下降子序列(直接把a数组reverse一下即可)
代码:

#include 

using namespace std;

int const N = 1e2 + 10;
vector<int> 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() {
    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个景点,并且决定按照顺序来浏览这些景点,即每次所浏览景点的编号都要大于前一个浏览景点的编号。同时队员们还有另一个登山习惯,就是不连续浏览海拔相同的两个景点,并且一旦开始下山,就不再向上走了。队员们希望在满足上面条件的同时,尽可能多的浏览景点,你能帮他们找出最多可能浏览的景点数么?
题解: 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() {
    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;
}

2.1.5 满足LIS性质的应用

acwing1012. 友好城市
**题意:**Palmia国有一条横贯东西的大河,河有笔直的南北两岸,岸上各有位置各不相同的N个城市。北岸的每个城市有且仅有一个友好城市在南岸,而且不同城市的友好城市不相同。每对友好城市都向政府申请在河上开辟一条直线航道连接两个城市,但是由于河上雾太大,政府决定避免任意两条航道交叉,以避免事故。编程帮助政府做出一些批准和拒绝申请的决定,使得在保证任意两条航线不相交的情况下,被批准的申请尽量多。
题解: 假设固定北岸的点为[1, 2, 3, 4],南岸和北岸配对的点为[3, 1, 2, 4],
那么要想不交叉的尽可能多,那么南岸的点需要满足lis性质,因为南岸的点如果不单调递增,必然存在交叉的情况
代码:

#include 

using namespace std;

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

int main() {
    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;
}

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

acwing1016. 最大上升子序列和
题意:
一个数的序列 b i b_i bi,当 b 1 < b 2 < … < b S b_1 < b_2 <… < b_S b1<b2<<bS 的时候,我们称这个序列是上升的。对于给定的一个序列(a1,a2,…,aN),我们可以得到一些上升的子序列(ai1,ai2,…,aiK),这里1 ≤ i1 < i2 < … < iK ≤N。比如,对于序列(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)。 1 ≤ N ≤ 1000 1≤N≤1000 1N1000
题解: 求最长上升子序列和可以魔改最长上升子序列的状态定义,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() {
    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;
}

2.1.7 LIS的区间覆盖问题

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

dilworth定理:

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

acwing1010. 拦截导弹
题意: 某国为了防御敌国的导弹袭击,发展出一种导弹拦截系统。 但是这种导弹拦截系统有一个缺陷:虽然它的第一发炮弹能够到达任意的高度,但是以后每一发炮弹都不能高于前一发的高度。 某天,雷达捕捉到敌国的导弹来袭。 由于该系统还在试用阶段,所以只有一套系统,因此有可能不能拦截所有的导弹。 输入导弹依次飞来的高度(雷达给出的高度数据是不大于30000的正整数,导弹数不超过1000),计算这套系统最多能拦截多少导弹,如果要拦截所有导弹最少要配备多少套这种导弹拦截系统。
题解:
第一问:求最长单调不上升子序列
第二问:求最少几个单调不上升子序列能够覆盖全部的序列
对于第一问,直接做lis即可;
对于第二问,依据dilworth定理,
能覆盖整个序列的最少的不上升子序列的个数”等价于“该序列的最长上升子序列长度,
能覆盖整个序列的最少的不下降子序列的个数”等价于“该序列的最长下降子序列长度
那么直接求最长上升子序列即可
代码:

#include 

using namespace std;

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

int main() {
    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;
}
2.1.7.2 用n1个最长上升子序列和n2个最长下降子序列来覆盖区间:dfs

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

acwing187. 导弹防御系统
题意: 为了对抗附近恶意国家的威胁,R国更新了他们的导弹防御系统。 一套防御系统的导弹拦截高度要么一直 严格单调 上升要么一直 严格单调 下降。 例如,一套系统先后拦截了高度为3和高度为4的两发导弹,那么接下来该系统就只能拦截高度大于4的导弹。 给定即将袭来的一系列导弹的高度,请你求出至少需要多少套防御系统,就可以将它们全部击落。 1 ≤ n ≤ 50 1≤n≤50 1n50
题解: 每个数字要不然属于上升序列,要不然属于下降序列,可以枚举每个数字属于上升还是下降序列
时间为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() {
    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;
}

2.2 最长公共子序列模型

2.2.1 母题:LCS问题

acwing897. 最长公共子序列
题意: 给定两个长度分别为N和M的字符串A和B,求既是A的子序列又是B的子序列的字符串长度最长是多少。
1 ≤ N ≤ 1000 1≤N≤1000 1N1000
代码:

#include 

using namespace std;

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

int main() {
    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;
}

2.2.2 编辑距离问题

acwing902. 最短编辑距离
题意: 给定两个字符串A和B,现在要将A经过若干操作变为B,可进行的操作有:

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

题解:
编辑距离问题: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() {
    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;
}

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

acwing272. 最长公共上升子序列
题意: 熊大妈的奶牛在小沐沐的熏陶下开始研究信息题目。 小沐沐先让奶牛研究了最长上升子序列,再让他们研究了最长公共子序列,现在又让他们研究最长公共上升子序列了。 小沐沐说,对于两个数列A和B,如果它们都包含一段位置不一定连续的数,且数值是严格递增的,那么称这一段数是两个数列的公共上升子序列,而所有的公共上升子序列中最长的就是最长公共上升子序列了。
奶牛半懂不懂,小沐沐要你来告诉奶牛什么是最长公共上升子序列。 不过,只要告诉奶牛它的长度就可以了。
数列A和B的长度均不超过3000。 1 ≤ N ≤ 3000 1≤N≤3000 1N3000,序列中的数字均不超过 2 31 − 1 2^{31}−1 2311
题解:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-8mRGnGLy-1600309332023)(https://i.loli.net/2020/03/03/INL32EJxOcinG7s.jpg)]
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;
}

2.3 数字三角形模型

2.2.1 母题:数字三角形

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

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

1≤n≤500,
−10000≤三角形中的整数≤10000
代码:

#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;
}

2.2.2 摘花生问题

acwing1015摘花生
题意: Hello Kitty在网格中,从西北角出发,只能向东或向南走,不能向西或向北走。问Hello Kitty最多能够摘到多少颗花生。
线性dp_第1张图片
1 ≤ T ≤ 100 , 1 ≤ R , C ≤ 100 , 0 ≤ M ≤ 1000 1≤T≤100,1≤R,C≤100,0≤M≤1000 1T100,1R,C100,0M1000
代码:

#include 

using namespace std;

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

int main() {
    cin >> t;
    while (t--) {
        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;
}

2.2.3 方格取数问题

acwing1027方格取数
题意: 设有 N×N 的方格图,我们在其中的某些方格中填入正整数,而其它的方格中则放入数字0。如下图所示:
线性dp_第2张图片
某人从图中的左上角 A 出发,可以向下行走,也可以向右行走,直到到达右下角的 B 点。在走过的路上,他可以取走方格中的数(取走后的方格中将变为数字0)。此人从 A 点到 B 点共走了两次,试找出两条这样的路径,使得取得的数字和为最大。 N ≤ 10 N≤10 N10
题解: 可以假设两条路同时从(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() {
    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;
}

2.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

1 ≤ k ≤ 5 , 学 生 总 人 数 不 超 过 30 人 1≤k≤5,学生总人数不超过30人 1k5,30
题解: 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。
1 ≤ N ≤ 2000 , 0 ≤ A i ≤ 1 0 9 1≤N≤2000, 0≤A_i≤10^9 1N2000,0Ai109
题解:
本题可以证明: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(N3)->O(N2)了
代码:

#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(){
    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。公司必须按顺序依次满足所有请求,目标是最小化公司花费,请你帮忙计算这个最小花费。 3 ≤ L ≤ 200 , 1 ≤ N ≤ 1000 3≤L≤200,1≤N≤1000 3L200,1N1000
题解:
本题可以很容易想到使用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() {
    // 读入
    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 <<endl;

    return 0;
}

Codeforces Round #667 (Div. 3) E. Two Platforms
题意: 给定一张竖直平面上n个点,每个点坐标位(xi, yi),给定2个板子,板子的长度均为k。问将两个板子放置在哪可以保证两个板子接到的点数和最多。 ∑ n < = 2 ∗ 1 0 5 , x i < = 1 0 9 , y i < = 1 0 9 , 1 < = k < = 1 0 9 \sum n <= 2*10^5, x_i<=10^9, y_i <= 10^9, 1 <= k <= 10^9 n<=2105,xi<=109,yi<=109,1<=k<=109
题解: 动态规划处理,r[i]表示用一块板子在i右侧能够接到的点最多为多少,l[i]表示用一块板子在i左侧能够接到的点最多为多少。那么答案就是 r e s = m a x { l [ i ] + r [ i + 1 ] } 。 res = max\lbrace l[i] + r[i + 1] \rbrace 。 res=max{l[i]+r[i+1]}本题点的坐标到达 1 0 9 10^9 109,本来应该离散化,但是可以特殊处理、利用尺取的思想就可以避免离散化,具体见代码。
代码:

#include 

using namespace std;

typedef long long LL;
int const N = 2e5 + 10;
int T, n, x[N], k, l[N], r[N];

int main() {
    cin >> T;
    while(T--) {
        memset(l, 0, sizeof l);
        memset(r, 0, sizeof r);
        scanf("%d%d", &n, &k);
        for (int i = 1; i <= n; ++i) scanf("%d", &x[i]);
        for (int i = 1, t; i <= n; ++i) scanf("%d", &t);
        if (n == 1) {
            printf("1\n");
            continue;
        }
        sort(x + 1, x + 1 + n);
        int j = n;
        for (int i = n; i >= 1; --i) {
            while(x[j] - x[i] > k && j >= i) j--;
            r[i] = j - i + 1;
            if (i < n) r[i] = max(r[i], r[i + 1]);
        }
        
        j = 1; 
        for (int i = 1; i <= n; ++i) {
            while(x[i] - x[j] > k && j <= i) ++j;
            l[i] = i - j + 1;
            if (i > 1) l[i] = max(l[i], l[i - 1]);
        }
        
        int res = 1;
        for (int i = 1; i < n; ++i) {
            res = max(res, l[i] + r[i + 1]);
        }
        printf("%d\n", res);
    }
    return 0;
}

你可能感兴趣的:(ACM--动态规划)