【算法专题】方格取数问题

方格取数问题

1. 概述

  • 给定一个 n × m n \times m n×m 的矩阵,每个位置有个非负数,我们可以从左上角走到右下角,每经过一个位置我们可以把格子上对应的数字取走,取走之后数字变为0,问我们可以获得的数字之和最大是多少?

  • 此问题存在递进的三种类型:

2. AcWing上对应题目

AcWing 1015. 摘花生

问题描述

在这里插入图片描述

分析

  • 动态规划,分析如下:

在这里插入图片描述

代码

  • C++
#include <iostream>
#include <cstring>

using namespace std;

const int N = 110;

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

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

AcWing 1027. 方格取数

问题描述

在这里插入图片描述

分析

  • 本题中两条路径不是独立的,因此不能首先求一条,然后标记上之后不能使用,然后再遍历第二条。因此要同时求解。

  • 我们可以定义状态:f[i1, j1, i2, j2]表示所有从(1, 1)分别走到(i1, j1)、(i2, j2)的路径的最大值。那么我们应该如何处理同一个格子不能被重复选择的问题?考虑什么时候两条路径的格子可能重合,当i1+j1==i2+j2的时候两个格子可能重合(当然满足这个条件也可能不重合)。

  • 因此我们可以使用f[k, i1, i2]表示所有从(1, 1)分别走到(i1, k-i1)、(i2, k-i2)的路径的最大值。k表示两条路线当前走到的格子的横纵坐标之和,即k=i1+j1=i2+j2

  • 考虑状态转移,可以根据两条路线到达(i1, k-i1)、(i2, k-i2)的前一步分为四种情况,如下图:

在这里插入图片描述

  • 我们在这四类中求出最大值就是对应的f(k, i1, i2)

代码

  • C++
#include <iostream>

using namespace std;

const int N = 15;

int n;
int w[N][N];
int f[N + N][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 * 2; 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];  // 说明(i1, j1)、(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\n", f[n * 2][n][n]);
    
    return 0;
}

AcWing 382. K取方格数

问题描述

在这里插入图片描述

分析

  • 本题是一个费用流的问题。

  • 每个方格看成原图中的一个点,需要拆点,每个点拆成入点和出点,入点和出点之间添加两条有向边:入点到出点的容量为1费用为方格中数的有向边(表示只能取一次方格中的数),入点到出点容量为正无穷费用为0的有向边(表示可以通过该点多次)。

  • 从虚拟源点S(0, 0)的入点连接一条有向边,容量为k,费用为0;从(n-1, n-1)的出点向虚拟汇点T连接一条有向边,容量为k,费用为0。

  • 对新图求一遍最大费用最大流即可得到答案。

  • 方格的编号:(0, 0)的到的两个点编号为0、1(0, 1)的到的两个点编号为2、3,依次类推。

代码

  • C++
#include <iostream>
#include <cstring>

using namespace std;

const int N = 5010, M = 20010, INF = 1e8;

int n, k, S, T;
int h[N], e[M], f[M], w[M], ne[M], idx;
int q[N], d[N], pre[N], incf[N];
bool st[N];

void add(int a, int b, int c, int d) {
    e[idx] = b, f[idx] = c, w[idx] = d, ne[idx] = h[a], h[a] = idx++;
    e[idx] = a, f[idx] = 0, w[idx] = -d, ne[idx] = h[b], h[b] = idx++;
}

// t为0表示入点,t为1表示入点
int get(int x, int y, int t) {
    return (x * n + y) * 2 + t;
}

bool spfa() {
    
    int hh = 0, tt = 1;
    memset(d, -0x3f, sizeof d);
    memset(incf, 0, sizeof incf);
    
    int cnt = 0;
    
    q[0] = S, d[S] = 0, incf[S] = INF;
    while (hh != tt) {
        int t = q[hh++];
        if (hh == N) hh = 0;
        st[t] = false;
        
        for (int i = h[t]; ~i; i = ne[i]) {
            int ver = e[i];
            if (f[i] && d[ver] < d[t] + w[i]) {
                d[ver] = d[t] + w[i];
                pre[ver] = i;
                incf[ver] = min(f[i], incf[t]);
                if (!st[ver]) {
                    q[tt++] = ver;
                    if (tt == N) tt = 0;
                    st[ver] = true;
                }
            }
        }
    }
    return incf[T] > 0;
}

int EK() {
    
    int cost = 0;
    while (spfa()) {
        int t = incf[T];
        cost += t * d[T];
        for (int i = T; i != S; i = e[pre[i] ^ 1]) {
            f[pre[i]] -= t;
            f[pre[i] ^ 1] += t;
        }
    }
    return cost;
}

int main() {
    
    scanf("%d%d", &n, &k);
    S = 2 * n * n, T = S + 1;
    memset(h, -1, sizeof h);
    
    add(S, get(0, 0, 0), k, 0);
    add(get(n - 1, n - 1, 1), T, k, 0);
    
    for (int i = 0; i < n; i++)
        for (int j = 0; j < n; j++) {
            int c;
            scanf("%d", &c);
            add(get(i, j, 0), get(i, j, 1), 1, c);  // 拆点
            add(get(i, j, 0), get(i, j, 1), INF, 0);  // 拆点
            if (i + 1 < n) add(get(i, j, 1), get(i + 1, j, 0), INF, 0);
            if (j + 1 < n) add(get(i, j, 1), get(i, j + 1, 0), INF, 0);
        }
    
    printf("%d\n", EK());
    
    return 0;
}

3. 力扣上对应题目

Leetcode 0741 摘樱桃

问题描述

分析

  • 本题的考点:动态规划

  • 本题中两条路径不是独立的,因此不能首先求一条,然后标记上之后不能使用,然后再遍历第二条。因此要同时求解。本题中动态规划中的数组对应左上角的坐标为(1, 1)

  • 我们可以定义状态:f[i1, j1, i2, j2]表示所有从(1, 1)分别走到(i1, j1)、(i2, j2)的路径的最大值。那么我们应该如何处理同一个格子不能被重复选择的问题?考虑什么时候两条路径的格子可能重合,当i1+j1==i2+j2的时候两个格子可能重合(当然满足这个条件也可能不重合)。

  • 因此我们可以使用f[i1, i2, k]表示所有从(1, 1)分别走到(i1, k-i1)、(i2, k-i2)的路径的最大值。k表示两条路线当前走到的格子的横纵坐标之和,即k=i1+j1=i2+j2

  • 为了方便,这里状态定义为重新写为f[i, j, k],因此表示从(1, 1)分别走到(i, k-i)、(j, k-j)的路径的最大值。

  • 考虑状态转移,可以根据两条路线到达(i, k-i)、(j, k-j)的前一步分为四种情况,如下图:

在这里插入图片描述

  • 本题中还需要考虑如果两条路线中有格子不合法怎么办?直接跳过即可。

  • 因为(i, k - i)要在格子中,因此1<=i<=n && 1<=k-i<=n,因此max(1, k-n)<=i<=min(n, k-1)。同理max(1, k-n)<=j<=min(n, k-1)

代码

  • C++
const int N = 55;
int f[N][N][N * 2];

class Solution {
public:
    int cherryPickup(vector<vector<int>>& grid) {

        int n = grid.size();
        memset(f, -0x3f, sizeof f);
        f[1][1][2] = grid[0][0];
        for (int k = 3; k <= n * 2; k++)
            for (int i = max(1, k - n); i <= min(n, k - 1); i++)
                for (int j = max(1, k - n); j <= min(n, k - 1); j++) {
                    
                    if (grid[i - 1][k - i - 1] == -1 || grid[j - 1][k - j - 1] == -1) continue;
                    
                    int t = grid[i - 1][k - i - 1];
                    if (i != j) t += grid[j - 1][k - j - 1];
                    
                    for (int a = i - 1; a <= i; a++)
                        for (int b = j - 1; b <= j; b++)
                            f[i][j][k] = max(f[i][j][k], f[a][b][k - 1] + t);
                }
        
        return max(0, f[n][n][n * 2]);
    }
};
  • Java
class Solution {

    static final int N = 55;
    int[][][] f = new int[N][N][N * 2];

    public int cherryPickup(int[][] grid) {
        int n = grid.length;
        for (int i = 0; i < N; i++)
            for (int j = 0; j < N; j++)
                Arrays.fill(f[i][j], (int) -1e8);
        f[1][1][2] = grid[0][0];
        for (int k = 3; k <= n * 2; k++)
            for (int i = Math.max(1, k - n); i <= Math.min(n, k - 1); i++)
                for (int j = Math.max(1, k - n); j <= Math.min(n, k - 1); j++) {
                    if (grid[i - 1][k - i - 1] == -1 || grid[j - 1][k - j - 1] == -1) continue;
                    int t = grid[i - 1][k - i - 1];
                    if (i != j) t += grid[j - 1][k - j - 1];
                    for (int a = i - 1; a <= i; a++)
                        for (int b = j - 1; b <= j; b++)
                            f[i][j][k] = Math.max(f[i][j][k], f[a][b][k - 1] + t);
                }
        return Math.max(0, f[n][n][n * 2]);
    }
}
  • Python
# TLE
class Solution:
    def cherryPickup(self, grid: List[List[int]]) -> int:
        n = len(grid)
        N = 55
        f = [[[(int) (-1e8) for _ in range(N * 2)] for _ in range(N)] for _ in range(N)]
        f[1][1][2] = grid[0][0]
        for k in range(3, n * 2 + 1):
            for i in range(max(1, k - n), min(n, k - 1) + 1):
                for j in range(max(1, k - n), min(n, k - 1) + 1):
                    if grid[i - 1][k - i - 1] == -1 or grid[j - 1][k - j - 1] == -1:
                        continue
                    t = grid[i - 1][k - i - 1]
                    if i != j:
                        t += grid[j - 1][k - j - 1]
                    for a in range(i - 1, i + 1):
                        for b in range(j - 1, j + 1):
                            f[i][j][k] = max(f[i][j][k], f[a][b][k - 1] + t)
        return max(0, f[n][n][n * 2])

时空复杂度分析

  • 时间复杂度: O ( n 3 ) O(n^3) O(n3)n为数组边长。

  • 空间复杂度: O ( n 3 ) O(n^3) O(n3)

  • 6
    点赞
  • 35
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值