AcWing 1027. 方格取数

AcWing 1027. 方格取数

一、题目描述

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

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

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

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

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

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

行和列编号从 1 开始。

一行0 0 0表示结束。

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

数据范围
N10

输入样例

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

二、走两次行不行

这是一种 贪心 解法,第一次选择最大的,然后把最大路径上的数字都置为空,第二次再选择最大的。这就是 只见树木不见森林 的方法:

第一次走为局部最优并且也对第二次走造成了影响,第二次走是在第一次影响下所能走的局部最优,不具备 无后效性,因此分开两次走并不是全局最优解。

举一个反例来证明,

9 0 3
0 9 2
0 2 0

最长路径是9+9+2=20,假设这次走的路线是第一行的9、第二行的92,那么在第二次走的时候,地图变成

0 0 3
0 0 0
0 2 0

显然最长路径是3,总和为23

然而,这不是最优解,最优解是第一次走第一行的9、第二行的9、第三行的2(仍是20),但第二次可以走第一行的3、第二行的2,得到5,总和达到25

打个比方吧,以我熟悉的篮球比赛为例子,假设现在中国篮协选了杜峰和李楠两个人同时成为主教练(好奇葩的作法,是吧~),他们一起执教同一场对澳大利亚的比赛:

  • 方案1:杜锋执教上半场,只关注上半场局势,不关心下半场。李楠执教下半场,只关注下半场局势,不关心上半场。结果,上半场往死了打,每个球员犯规5次,体力用尽,上半场比分与澳大利亚打平。可是到了下半场,球员都无法上场了,也没劲了,只能一节得3分,被羞辱~,为什么会这样呢?因为需要整体考虑,根据自身情况,制订合理战术,该节约体能需要节约,要控制犯规,最后一节好好冲刺,就可以与袋鼠一战,可因为两个教练互相独立,不关心另一半,当然不会考虑这些,只保证自己好就行了,最终的整体结果当然不一定是最好的。

  • 方案2:两人通力合作,权衡全场,该攻就攻,该守就守,合理分配体能,不过早的暴露攻击点,最后关键时刻给敌人致命一击,成功拿下比赛,结果最重要,国家荣誉最重要。

很明显,傻子都知道方案2是最优选择。

两个小朋友 一起走 为什么就行呢?因为他们在一起整体考虑,互相关心、互相照顾~,彼此能知道对方走了哪个点,自己在不影响最优取值的情况下,尽可能的取次优的点,这样,就可以得到全局最优解。

一起走:两个小朋友步调一致,“一二三” 一起走,也就是任意时刻,两人走的步数是完全一样的。

二、四维状态解法

四维的状态表示f[x1][y1][x2][y2]的含义是很明显的,就是两个小朋友分别在(x1,y1),(x2,y2)的状态下,此时的整体最优解是多少。

它的前序状态应该是

f(x1,y1,x2,y2){f(x11,y1)f(x21,y2)f(x11,y1)f(x2,y21)f(x1,y11)f(x21,y2)f(x1,y11)f(x2,y21)

这四种情况,都有可能是f[x1][y1][x2][y2]的前序,具体走哪个,需要对四个进行求max,谁大取谁。

int t = f[x1 - 1][y1][x2 - 1][y2];     //下下
t = max(t, f[x1][y1 - 1][x2][y2 - 1]); //右右
t = max(t, f[x1 - 1][y1][x2][y2 - 1]); //下右
t = max(t, f[x1][y1 - 1][x2 - 1][y2]); //右下

还有一个问题没有解决 : 没有加上当前状态新到的位置上的数字,这个需要讨论一下:

  • 两个小朋友走的格子不是同一个:
    w=t+w[x1][y1]+w[x2][y2]

  • 两个小朋友走的格子是同一个
    因为题目要求每个格子只能取一次,就是说如果同一时间走到同一个格子中,应该是取一次值即可:
    w=t+w[x1][y1]

利用四层循环,从上到下,从左到右的完成状态表的填充,就可以取得正确答案:

#include <bits/stdc++.h>

using namespace std;
const int N = 15;

int n;             //方格的宽度和高度
int w[N][N];       //每个方格里面的数字
int f[N][N][N][N]; //四维的DP数组

int main() {
    cin >> n;
    //接下来的每行有三个整数,第一个为行号数,第二个为列号数,第三个为在该行、该列上所放的数。
    int a, b, c;
    //一行 0 0 0 表示结束。
    while (cin >> a >> b >> c, a || b || c) w[a][b] = c;
    //开始递推
    for (int x1 = 1; x1 <= n; x1++)
        for (int y1 = 1; y1 <= n; y1++)
            for (int x2 = 1; x2 <= n; x2++)
                for (int y2 = 1; y2 <= n; y2++) {
                    if (x1 + y1 == x2 + y2) {
                        int t = f[x1 - 1][y1][x2 - 1][y2];     //下下
                        t = max(t, f[x1][y1 - 1][x2][y2 - 1]); //右右
                        t = max(t, f[x1 - 1][y1][x2][y2 - 1]); //下右
                        t = max(t, f[x1][y1 - 1][x2 - 1][y2]); //右下
                        //加上这个点的数值
                        f[x1][y1][x2][y2] = t + w[x1][y1];
                        //如果这个点没有被重复走,那么再加一次w(x2,y2)
                        if (x1 != x2 && y1 != y2) f[x1][y1][x2][y2] += w[x2][y2];
                    }
                }
    printf("%d", f[n][n][n][n]);
    return 0;
}

三、四维状态优化版本

上面四维的写法,就足以AC掉这道题了,但是在时间上还是可以优化的。原因是使用瞪眼大法仔细看,明白上面的递推关系式中,(x1,y1)(x2,y2)其实是有关系的,刚才我们把关系写在了四层循环的内侧,这样其实有很多情况是无效的枚举,我们只关心x1+y1=x2+y2的情况,所以可以利用这个关系式,减少一层循环:

#include <bits/stdc++.h>

using namespace std;
const int N = 15;

int n;              //方格的宽度和高度
int w[N][N];        //每个方格里面的数字
int f[N][N][N][N];  //四维的DP数组

int main() {
    cin >> n;
    //接下来的每行有三个整数,第一个为行号数,第二个为列号数,第三个为在该行、该列上所放的数。
    int a, b, c;
    //一行 0 0 0 表示结束。
    while (cin >> a >> b >> c, a || b || c) w[a][b] = c;

    //开始递推
    for (int x1 = 1; x1 <= n; x1++)
        for (int y1 = 1; y1 <= n; y1++)
            for (int x2 = 1; x2 <= n; x2++) {
                int y2 = x1 + y1 - x2;   //通过计算可以减少一维循环
                if (y2 >= 1 && y2 <= n) { //要小心点,别整出一个不符合实际情况的数
                    //上一个状态
                    int t = f[x1 - 1][y1][x2 - 1][y2];
                    t = max(t, f[x1][y1 - 1][x2][y2 - 1]);
                    t = max(t, f[x1 - 1][y1][x2][y2 - 1]);
                    t = max(t, f[x1][y1 - 1][x2 - 1][y2]);
                    //加上这个点的数值
                    f[x1][y1][x2][y2] = t + w[x1][y1];
                    //如果这个点没有被重复走,那么再加一次
                    if (x1 != x2 && y1 != y2) f[x1][y1][x2][y2] += w[x2][y2];
                }
            }
    printf("%d", f[n][n][n][n]);
    return 0;
}

四、三维状态版本

既然刚才想到了利用两者的 坐标关系和 来实现少写一层循环,那可不可以基于这一点深挖一下,获取更大的利益呢?答案是可以的,我们可以重新定义状态:

f[k][x1][x2]

其中k是指x1+y1的坐标和,当kx1确定时,kx1就是y1; 当kx2确定时,kx2就是y2;这样的话,就 可以在空间上减小为三维表示法

下面来思考一下起点和终点的情况:

  • 起点:(1,1)是两个小朋友的出发点,所以f[2][1][1]是起点状态值,初始值是w[1][1]
  • 终点:(n,n)是两个小朋友的终点,所以f[2n][n][n]是终点状态值,也就是答案。
#include <bits/stdc++.h>

using namespace std;
const int N = 15;

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

int main() {
    cin >> n;
    //接下来的每行有三个整数,第一个为行号数,第二个为列号数,第三个为在该行、该列上所放的数。
    int a, b, c;
    //一行 0 0 0 表示结束。
    while (cin >> a >> b >> c, a || b || c) w[a][b] = c;

    // k表示两个小朋友所在位置的x+y的和,最多是2*n
    for (int k = 2; k <= 2 * n; k++)
        for (int x1 = 1; x1 <= n; x1++)//第一个小朋友竖着走的距离
            for (int x2 = 1; x2 <= n; x2++) {//第二个小朋友竖着走的距离
                int y1 = k - x1, y2 = k - x2; //计算横着走的距离
                //不能出界,只走有效的位置
                if (y1 >= 1 && y1 <= n && y2 >= 1 && y2 <= n) {
                    // PK获取到最优的上一个状态
                    int t = f[k - 1][x1 - 1][x2];
                    t = max(t, f[k - 1][x1 - 1][x2 - 1]);
                    t = max(t, f[k - 1][x1][x2 - 1]);
                    t = max(t, f[k - 1][x1][x2]);
                    //将本位置的数值加上
                    f[k][x1][x2] = t + w[x1][y1];
                    //如果不是重复的位置,还可以继续加上
                    if (x1 != x2 && y1 != y2) f[k][x1][x2] += w[x2][y2];
                    }
            }
    //输出DP的结果
    printf("%d\n", f[2 * n][n][n]);
    return 0;
}

五、总结与反思

  • 当你发现当前的状态表示无法描述当前所有信息时,考虑增加一维状态.一维不行就增加两维
  • 普通情况下的每一个状态,思考它是从哪些可能的前序状态转移而来,注意:只考虑前一步,不要把问题想复杂了
  • 在状态转移式确定下来后,再考虑入口(递推起点,初始化)和出口(结果,答案)
posted @   糖豆爸爸  阅读(418)  评论(0编辑  收藏  举报
编辑推荐:
· 开发者必知的日志记录最佳实践
· SQL Server 2025 AI相关能力初探
· Linux系列:如何用 C#调用 C方法造成内存泄露
· AI与.NET技术实操系列(二):开始使用ML.NET
· 记一次.NET内存居高不下排查解决与启示
阅读排行:
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!
· 没有Manus邀请码?试试免邀请码的MGX或者开源的OpenManus吧
· 【自荐】一款简洁、开源的在线白板工具 Drawnix
· 园子的第一款AI主题卫衣上架——"HELLO! HOW CAN I ASSIST YOU TODAY
· Docker 太简单,K8s 太复杂?w7panel 让容器管理更轻松!
历史上的今天:
2019-12-03 关于无限试用JetBrains产品的方案
2014-12-03 常用的LUA片段
Live2D
点击右上角即可分享
微信分享提示