DP-数字三角形总结


摘花生

单路径 取最大

题目描述:Hello Kitty想摘点花生送给她喜欢的米老鼠。她来到一片有网格状道路的矩形花生地(如下图),从西北角进去,东南角出来。地里每个道路的交叉点上都有种着一株花生苗,上面有若干颗花生,经过一株花生苗就能摘走该它上面所有的花生。Hello Kitty只能向东或向南走,不能向西或向北走。问Hello Kitty最多能够摘到多少颗花生
摘花生

输入格式: 第一行是一个整数T,代表一共有多少组数据。接下来是T组数据
每组数据的第一行是两个整数,分别代表花生苗的行数R和列数 C
每组数据的接下来R行数据,从北向南依次描述每行花生苗的情况。每行数据有C个整数,按从西向东的顺序描述了该行每株花生苗上的花生数目M

输出格式: 对每组输入数据,输出一行,内容为Hello Kitty能摘到得最多的花生颗数
数据范围:
1≤T≤100,
1≤R,C≤100,
0≤M≤1000

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

const int N = 105;
int f[N][N], a[N][N];

int main()
{
    int T;
    cin >> T;   // T 组数据
    
    while(T --){
        int m, n;
        cin >> n >> m;  // n 行 m 列
        for(int i = 1; i <= n; i ++)
            for(int j = 1; j <= m; j ++){
                cin >> a[i][j];
            }
        /* 
        状态: 
        	集合:
        		f[i, j] 为 从 [1, 1] 到 [i, j] 取得的最大花生数 
        	属性:
        		max 求左上到右下最大数
        状态转移:
        		上一个状态对当前状态的影响 MAX
         		f[i, j] = max(f[i - 1, j], f[i, j - 1])
         		当前状态的加持
         		f[i, j] += a[i, j]
         		结果: 从左上到[i, j] 的能捡到最大的花生数
        */
        for(int i = 1; i <= n; i ++)
            for(int j = 1; j <= m; j ++)
                f[i][j] = max(f[i][j - 1], f[i - 1][j]) + a[i][j];

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

最低通行费

单路径 取最小

题目描述: 一个商人穿过一个 N×N的正方形的网格,去参加一个非常重要的商务活动。他要从网格的左上角进,右下角出。每穿越中间 1个小方格,都要花费 1 个单位时间。商人必须在 (2N−1)个单位时间穿越出去。而在经过中间的每个小方格时,都需要缴纳一定的费用。这个商人期望在规定时间内用最少费用穿越出去
请问至少需要多少费用?

注意:不能对角穿越各个小方格(即,只能向上下左右四个方向移动且不能离开网格)。

输入格式: 第一行是一个整数,表示正方形的宽度 N。后面 N行,每行 N个不大于 100的正整数,为网格上每个小方格的费用。

输出格式:
输出一个整数,表示至少需要的费用。

数据范围
1≤N≤100

分析: 2N-1个时间单位,从左上到右下,则说明可供选择方向只有向右或向下。立即想到数字三角形,不同的是从求最大变成了求最小。我认为此题是提醒我们注意DP问题中数组边界处理。

#include <iostream>
#include <algorithm>

using namespace std;

const int N = 110;
int f[N][N], w[N][N];

int main()
{
    int n;
    
    cin >> n;  // n 行
    for(int i = 1; i <= n; i ++)
        for(int j = 1; j <= n; j ++)
            cin >> w[i][j];         
   /*
   状态:
   		集合
   			f[i, j] 为 从 [1, 1] 到 [i, j] 取得的最小花费
   		属性
   			min 花费取最小
   	状态转移
   		边界: 1. f[1, 1] = a[1, 1]
   			  2. f[1, j] = f[1, j - 1] j = 2...n
   			  3. f[i, 1] = f[i - 1, j] i = 2...n
   		中间: f[i, j] = min(f[i - 1, j], f[i, j - 1]) + a[i, j]
   		如果整个f经过初始化(初始化为Inf)且数组a是从[1, 1]开始计算,
   		则无需上述边界分析
   */
    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{
                if(i == 1) f[i][j] = w[i][j] + f[i][j - 1];
                else if(j == 1) f[i][j] = w[i][j] + f[i - 1][j];
                else f[i][j] = min(f[i - 1][j], f[i][j - 1]) + w[i][j];
            }
        }
    cout << f[n][n] << endl;
    return 0;
}

方格取数

双路径 方向相同 且 路径可重叠

题目描述: 设有 N×N 的方格图,我们在其中的某些方格中填入正整数,而其它的方格中则放入数字0。如下图所示
方格取数
某人从图中的左上角 A 出发,可以向下行走,也可以向右行走,直到到达右下角的 B 点。在走过的路上,他可以取走方格中的数 (取走后的方格中将变为数字0) 。此人从 A 点到 B 点共走了两次,试找出两条这样的路径,使得取得的数字和为最大

输入格式: 第一行为一个整数N,表示 N×N 的方格图。接下来的每行有三个整数,第一个为行号数,第二个为列号数,第三个为在该行、该列上所放的数。行和列编号从 1开始。一行“0 0 0”表示结束。

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

数据范围
N≤10

朴素想法: 从摘花生问题中的一条路径拓展到本问题中的两条路径,问题中选取格子中的数之后,将其置为0,使得此问题略有不同。如果使用DFS做本题,第一次从左上走到右下需要搜索过程中保存路径,到达合适边界后,再将路径上的点一一置零,第二次就转化为普通的摘花生问题,将两次答案求和就是结果。

DP思考:
两条人同时走,走两条不同的路径,使用f[i, j, k, t] 表示两人的位置, 其中 [i, j]表示其中一个人,[k, t] 表示另一个人 。两人状态转移为:

f[i][j][k][t] = max(
  				   max(f[i - 1][j][k - 1][t], f[i - 1][j][k][t - 1]),
                   max(f[i][j - 1][k - 1][t], f[i][j - 1][k][t - 1])
                   )

如果 i = = k 且 j = = t 则 f[i][j][k][t] += a[i][j] (两条路线上重叠的交点只能取一次值)
否则 f[i][j][k][t] += (a[i][j] + a[k][t]) (两条路线上不重合的点,数值都可以取)

此题的状态转移方程考虑情况稍微复杂些,思维复杂来源于维度上升和不同情况的考虑,开始并不容易理清其中关系。而且要将题中的走两次转化为两次同时走,从模拟到DP的思路不容易去想到。

能想到上方的状态转移,其实来源于装态中状态集合能考虑到,这是先决条件。
上面四维数组所代表的集合就是同时表示两个人所有不同的位置, 这里可能相同,可能不同,对上述为什么是四个点之间取最大值,可以这么看:f[i][j][k][t] 所代表的是,从起点出发到[i, j], [k, t] 两点所拥有的最大数值。而能到这两点的只有题意描述的四个点到[i, j,k t], 分别[i, j]、[k, t]的上方和左方,在向上看一遍状态转移结合这段话,仔细思考,可以看其他博客画的图,结合理解就能明白。

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

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

int main()
{

    cin >> n;
    int r, l, c;
    // 输入数据
    while(cin >> r >> l >> c, r || l  || c)
        a[r][l] = c;
    
    for(int i = 1; i <= n; i ++){
        for(int j = 1; j <= n; j ++){
            for(int k = 1; k <= n; k ++){
                for(int t = 1; t <= n; t ++){
                    int &temp = f[i][j][k][t];
                    // 两个点来源处组合得到四个位置取最大,得到上一阶段对当前的影响
                    temp = max(
                        max(f[i - 1][j][k - 1][t], f[i - 1][j][k][t - 1]),
                        max(f[i][j - 1][k - 1][t], f[i][j - 1][k][t - 1])
                        );
                    // 当前阶段的处理,重叠路径,只去其中一个数值点,反之取两个
                    if(i == k && j == t) temp += a[i][j];
                    else temp += (a[i][j] + a[k][t]);
                            
                }
            }
            
        }
    }
    cout << f[n][n][n][n] << endl;
    
    return 0;
}

优化代码:思路从模拟两人的全部位置改进为两人同步走,用k表示两人走了k步,状态f[k][i1][i2] 表示为 第一个人在i1 行 k - i1 列,第二个人在 i2行k - i2 列,再用到上面的状态转移方程,得到如下改进代码。(空间复杂度得到优化)

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

const int N = 20;
int n;
int f[N * 2][N][N], a[N][N];

int main()
{

    cin >> n;
    int r, l, c;
    while(cin >> r >> l >> c, r || l  || c)
        a[r][l] = c;
    for(int k = 1; k <= 2 * 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 = f[k][i1][i2];
                    if(i1 != i2) t += a[i2][j2];
                    t += max(
                            max(f[k - 1][i1][i2], f[k - 1][i1 - 1][i2]),
                            max(f[k - 1][i1][i2 - 1], f[k - 1][i1 - 1][i2 - 1])
                        ) + a[i1][j1];
                }
            }
        }
    // 从左上走到右下得走 2 * n 步, 第一个人和第二个人都走到了n行 n列
    cout << f[2 * n][n][n] << endl;
    return 0;
}

当然上述代码可以继续优化其空间复杂度,我认为此题第一是考虑问题的转换(从模拟思想转化到DP思想,考虑到状态的集合空间),第二是其中的状态转移方程比较复杂,如果说最低通行费是提醒我们考虑数组边界处理问题,那么这题就提醒我们要注意问题中特殊情况的处理(此题中就是考虑上一个状态对当前状态的转移以及重叠和不重叠路径时的处理)。


传纸条

双路径 方向相反 且 路径不可重叠

题目描述: 小渊和小轩是好朋友也是同班同学,他们在一起总有谈不完的话题。一次素质拓展活动中,班上同学安排坐成一个 m行 n列的矩阵,而小渊和小轩被安排在矩阵对角线的两端,因此,他们就无法直接交谈了。幸运的是,他们可以通过传纸条来进行交流。纸条要经由许多同学传到对方手里,小渊坐在矩阵的左上角,坐标 (1,1)小轩坐在矩阵的右下角,坐标 (m,n)。从小渊传到小轩的纸条只可以向下或者向右传递,从小轩传给小渊的纸条只可以向上或者向左传递

在活动进行中,小渊希望给小轩传递一张纸条,同时希望小轩给他回复。

班里每个同学都可以帮他们传递,但只会帮他们一次也就是说如果此人在小渊递给小轩纸条的时候帮忙,那么在小轩递给小渊的时候就不会再帮忙,反之亦然

还有一件事情需要注意,全班每个同学愿意帮忙的好感度有高有低(注意:小渊和小轩的好心程度没有定义,输入时用 0表示),可以用一个 0∼100的自然数来表示,数越大表示越好心。

小渊和小轩希望尽可能找好心程度高的同学来帮忙传纸条,即找到来回两条传递路径使得这两条路径上同学的好心程度之和最大

现在,请你帮助小渊和小轩找到这样的两条路径。

输入格式:
第一行有 2个用空格隔开的整数 m和 n,表示学生矩阵有 m行 n列。
接下来的 m行是一个 m×n的矩阵,矩阵中第 i行 j列的整数表示坐在第 i行 j列的学生的好心程度,每行的 n个整数之间用空格隔开。

输出格式:
输出一个整数,表示来回两条路上参与传递纸条的学生的好心程度之和的最大值

分析:首先,确定求解目标,两条路径上好心程度最大之和,与从哪个方向来的无关,不管是左上角只能向下或者向右走,还是右下的只能向上或者向左走没有关系。第二点需要注意此时与方格取数不同的是规定了不能重叠路径(同学最多只帮传一次)这样很容易得到如下代码,整体代码与方格取数非常类似

#include <iostream>
#include <algorithm>
#include <cstring>

using namespace std;

const int N = 55;
int f[N * 2][N][N], a[N][N];

int main()
{
    int m, n;
    cin >> m >> n;
    
    for(int i = 1; i <= m; i ++)
        for(int j = 1; j <= n; j ++)
            cin >> a[i][j];
            
    for(int k = 1; 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 = f[k][i1][i2];
                    // 不同位置,两个好感度都加
                    if(i1 != i2) t += (a[i1][j1] + a[i2][j2]);
                    t += max(
                            max(f[k - 1][i1][i2], f[k - 1][i1 - 1][i2]),
                            max(f[k - 1][i1][i2 - 1], f[k - 1][i1 - 1][i2 - 1])
                        );
                    // 两个点重叠且不是最后一个点,这是无意义的状态,直接置零
                    if(i1 == i2 && i1 != m && j1 != n) t = 0;
                }
            }
        }
        
    cout << f[n + m][m][m] << endl;
    return 0;
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值