【AcWing算法提高课】1.1数字三角形模型

1.1题谱

一、摘花生

1015.摘花生 题目链接

传统的 DP 思考方式:阶段、决策、最优子结构、无后效性等,较抽象

全新的思考方式——闫氏 DP 分析法 (从集合角度分析 DP 问题):
每次考虑两个部分:

  1. 状态表示:
    (1) 集合
    (2) 属性:最大值/最小值/数量
  2. 状态计算——集合的划分:依据“最后一步”划分
    集合划分原则:不重复(仅在计算数量时需满足),不漏(必须满足)
    在考虑状态计算时有两种方法:一个状态由哪些状态更新,或者一个状态可以更新哪些状态。

1015摘花生
DP 的计算顺序问题:状态之间可以画成图的形式,可能有交叉,需要按照拓扑序计算,对于线性 DP 来说,一般只要按照下标某一顺序循环即可。

代码实现:

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

const int N = 110;

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

int main(){
    int T;
    scanf("%d", &T);
    
    while (T --){
        scanf("%d %d", &n, &m);
        for (int i = 1; i <= n; i ++)
            for (int j = 1; j <= m; j ++)
                scanf("%d", &w[i][j]);
                
        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]) + w[i][j];
        
        printf("%d\n", f[n][m]);
    }
    
    return 0;
}

二、最低通行费

1018.最低通行费 题目链接

由于从左上走到右下的最少步数就是 2 N − 1 2N-1 2N1,因此不难发现题目中 “必须在 ( 2 N − 1 ) (2N−1) (2N1) 个单位时间穿越出去” 可推出 “只能向左走或向下走,不能走回头路”,即本题与上题在模型上相同。

由于本题求的是最小值,且起点是 ( 1 , 1 ) (1,1) (1,1),因此在第一行与第一列会有边界问题,需要特判一下。

代码实现:

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

const int N = 110, INF = 1e9;

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

int main(){
    scanf("%d", &n);
    for (int i = 1; i <= n; i ++)
        for (int j = 1; j <= n; j ++)
            scanf("%d", &w[i][j]);
            
    for (int i = 1; i <= n; i ++)
        for (int j = 1; j <= n; j ++)
            if (i == 1 && j == 1) f[i][j] = w[i][j];  //左上角
            else{
                f[i][j] = INF;
                if (i > 1) f[i][j] = min(f[i][j], f[i - 1][j] + w[i][j]);  //只有不在第一行时才能从上面过来
                if (j > 1) f[i][j] = min(f[i][j], f[i][j - 1] + w[i][j]);  //只有不在第一列时才能从左边过来
            }
            
    printf("%d", f[n][n]);
    
    return 0;
}

三、方格取数

1027.方格取数 题目链接

本题需要走两次,可将走一次的做法进行推广:
只走一次: f [ i , j ] f[i,j] f[i,j] 表示所有从 ( 1 , 1 ) (1,1) (1,1) 走到 ( i , j ) (i,j) (i,j) 的路径的最大值;
走两次: f [ i 1 , j 1 , i 2 , j 2 ] f[i_1,j_1,i_2,j_2] f[i1,j1,i2,j2] 表示所有从 ( 1 , 1 ) (1,1) (1,1) 分别走到 ( i 1 , j 1 ) (i_1,j_1) (i1,j1) ( i 2 , j 2 ) (i_2,j_2) (i2,j2) 的路径的最大值。

此外还需处理 “同一个格子不能被重复选择” 的限制:
因此四维状态表示 f [ i 1 , j 1 , i 2 , j 2 ] f[i_1,j_1,i_2,j_2] f[i1,j1,i2,j2] 还不够,状态还需额外开一维来记录路径上经过的所有点,判断是否有重合。

发现只有在 i 1 + j 1 = i 2 + j 2 i_1+j_1=i_2+j_2 i1+j1=i2+j2 时,两条路径的格子才可能重合。
由于两次路径之间互不影响 (仅需特判是否经过同一格子),所以两条路径可以一起走:
f [ k , i 1 , i 2 ] f[k,i_1,i_2] f[k,i1,i2] 表示当前格子的横纵坐标之和,所有从 ( 1 , 1 ) (1,1) (1,1) 分别走到 ( i 1 , k − i 1 ) (i_1,k-i_1) (i1,ki1) ( i 2 , k − i 2 ) (i_2,k-i_2) (i2,ki2) 的路径的最大值,在 i 1 = i 2 i_1=i_2 i1=i2 两条路径通过同一格子,需要特殊处理,只加一次。

此外,还可证明最优解一定不会走到相同的格子。证明详见:AcWing 275. 证明传纸条为何可以使用方格取数的代码 by vlehr

状态计算——按最后一步:“下下”,“下右”,“右下”,“右右”,共四种情况。

1027方格取数
代码实现:

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

const int N = 15;

int n;
int w[N][N], f[N * 2][N][N];

int main(){
    scanf("%d", &n);
    
    int a, b, c;
    while (cin >> a >> b >> c, a || b || c) w[a][b] = c;
    
    for (int k = 2; k <= n + n; k ++)
        for (int i1 = 1; i1 <= n; i1 ++)
            for (int i2 = 1; i2 <= n; i2 ++){
                int j1 = k - i1, j2 = k - i2;
                if (j1 >= 1 && j1 <= n && j2 >= 1 && j2 <= n){
                    int t = w[i1][j1];
                    if (i1 != i2) t += w[i2][j2];
                    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 - 1] + t);
                    x = max(x, f[k - 1][i1][i2] + t);
                }
            }
            
    printf("%d", f[n + n][n][n]);
}

方格取数的终极版本是 k k k 取方格数,其标准做法是最小费用流。

  • 那么为什么一个动态规划问题终极版用费用流来做呢?
  • 本质上,DP是图论的子集。大部分DP问题可以转化为图论问题,而当图是拓扑图时,图论也可以转化为DP问题。

四、传纸条

275.传纸条 题目链接

本题要求先从左上走到右下,再从右下走回左上,而走的方向不影响最终结果,因此可将两次路径都看作从左上走到右下,便与上题模型相同了。

代码实现:

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

const int N = 55;

int n, m;
int w[N][N], f[N * 2][N][N];

int main(){
    scanf("%d %d", &m, &n);
    for (int i = 1; i <= m; i ++)
        for (int j = 1; j <= n; j ++)
            scanf("%d", &w[i][j]);
    
    for (int k = 2; k <= m + n; k ++)
        for (int i1 = 1; i1 <= m; i1 ++)
            for (int i2 = 1; i2 <= m; i2 ++){
                int j1 = k - i1, j2 = k - i2;
                if (j1 >= 1 && j1 <= n && j2 >= 1 && j2 <= n){
                    int t = w[i1][j1];
                    if (i1 != i2) t += w[i2][j2];
                    int &x = f[k][i1][i2];
                    x = max(x, f[k - 1][i1 - 1][i2 - 1] + t);
                    x = max(x, f[k - 1][i1][i2 - 1] + t);
                    x = max(x, f[k - 1][i1 - 1][i2] + t);
                    x = max(x, f[k - 1][i1][i2] + t);
                }
            }
            
    printf("%d", f[n + m][m][m]);
    
    return 0;
}
  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值