从线性01背包到环形数组背包

97 阅读4分钟

打家劫舍

一个环形数组,小偷可以选择偷不相邻下标的现金,求最大偷取金额

首先,如果是线性的,那么是一个经典的01背包,只需用两个值分别记录不选两种情况即可

环形的情况下,首尾不能同时选择,那么分别求出,取最值即可。

class Solution {
    public int rob(int[] nums) {
        int n = nums.length;
        if (n == 0) return 0;
        if (n == 1) return nums[0];
        int a = rob_line(Arrays.copyOfRange(nums, 0, n - 1));
        int b = rob_line(Arrays.copyOfRange(nums, 1, n));
        return Math.max(a, b);
    }
​
    public int rob_line(int[] nums) {
        int n = nums.length;
        int[] f = new int[2];
        f[0] = 0;
        f[1] = nums[0];
​
        for (int i = 1; i < n; i++) {
            int pre = f[0];
            f[0] = Math.max(f[0], f[1]);
            f[1] = pre + nums[i];
        }
​
        return Math.max(f[0], f[1]);
    }
}

3N披萨

给你一个披萨,它由 3n 块不同大小的部分组成,现在你和你的朋友们需要按照如下规则来分披萨:

  • 你挑选 任意 一块披萨。
  • Alice 将会挑选你所选择的披萨逆时针方向的下一块披萨。
  • Bob 将会挑选你所选择的披萨顺时针方向的下一块披萨。
  • 重复上述过程直到没有披萨剩下。

每一块披萨的大小按顺时针方向由循环数组 slices 表示。

请你返回你可以获得的披萨大小总和的最大值。

对于任何的节点i,一次选取i-1ii+1,下一次中心节点可以选取i-2i+2i-3i+3,... 均可,初步判断,只需两个节点不相邻即可同时选取。

原问题=>从3N长度的环状序列中中选取N个不相邻的节点的最大和

首先考虑线性的情况,之后转为环状即可

dp[i][j]代表前i个数中选择j个不相邻的数的最大和:

  • i \lt 2 或 j = 0时 dp[i][j] = 0

  • i \ge 2 且 j \gt 0时

    • 如果选取i,那么i-1不能被选择,dp[i][j] = dp[i-2][j-1] + slices[i]
    • 如果不选取i,那么,dp[i][j] = dp[i-1][j]

=> dp[i][j] = max(dp[i-2][j-1] + slices[i], dp[i-1][i])

class Solution {
    public int maxSizeSlices(int[] slices) {
        int N = slices.length;
        if (N == 0) return 0;
        if (N == 3) return Math.max(slices[0], Math.max(slices[1], slices[2]));
        int a = maxInLine(Arrays.copyOfRange(slices, 0, N - 1));
        int b = maxInLine(Arrays.copyOfRange(slices, 1, N));
        return Math.max(a, b);
    }
​
    // 线性情况下的最大值
    public int maxInLine(int[] slices) {
        // 难点在于需要将题意转化为从该序列中选取n个不相邻的数的最大值
        int N = slices.length;
        int n = (N + 1) / 3;
​
        int[][] f = new int[N][n+1];
        for (int i = 0; i < N; i++) {
            Arrays.fill(f[i], Integer.MIN_VALUE);
        }
​
        f[0][0] = 0;
        f[0][1] = slices[0];
        f[1][0] = 0;
        f[1][1] = Math.max(slices[1], f[0][1]);
​
        for (int i = 2; i < N; i++) {
            f[i][0] = 0;
            for (int j = 1; j <= n; j++) {
                f[i][j] = Math.max(f[i-1][j], f[i-2][j-1] + slices[i]);
            }
        }
        return f[N-1][n];
    }
}

切披萨

给你一个 rows x cols 大小的矩形披萨和一个整数 k ,矩形包含两种字符: 'A' (表示苹果)和 '.' (表示空白格子)。你需要切披萨 k-1 次,得到 k 块披萨并送给别人。

每一刀可以选择垂直或者水平切,垂直则左半部分送人,水平则上半部分送人,最后一道切完后需要把最后一块给最后一个人。

返回确保每一块至少一个苹果的方案数,返回它对 10^9 + 7 取余的结果。

子问题拆解

如果pizzamn

  1. 垂直切,枚举左侧列数w,剩余mn-w列的矩形,还需k-2
  2. 水平切,枚举上方行数h,剩余m-hn列的矩形,还需k-2

如何编码?

按照题目的切法,我们的右下角一直是固定的,因此只需要左上角的坐标,即可确定矩形。

dfs(c, i, j)表示将(i,j)对应的矩形,切c刀的方案数

dfs(c, i, j) = \underset{j \lt j2 \lt n}{\Sigma} dfs(c - 1, i, j_2) + \underset{i < i2 \lt m}{\Sigma} dfs(c-1, i_2, j)

如何统计当前矩形内的苹果个数?

=> 前缀和

class Solution {
    public static final int MOD = (int) 1e9 + 7;
​
    public int ways(String[] pizza, int k) {
        MatrixSum ms = new MatrixSum(pizza);
        int m = pizza.length, n = pizza[0].length();
        var memo = new int[k][m][n];
        for (int i = 0; i < k; i++)
            for (int j = 0; j < m; j++)
                Arrays.fill(memo[i][j], -1); // -1 表示没有计算过
        return dfs(k - 1, 0, 0, memo, ms, m, n);
    }
​
    private int dfs(int c, int i, int j, int[][][] memo, MatrixSum ms, int m, int n) {
        if (c == 0) // 递归边界:无法再切了
            return ms.query(i, j, m, n) > 0 ? 1 : 0;
        if (memo[c][i][j] != -1) // 之前计算过
            return memo[c][i][j];
        int res = 0;
        for (int j2 = j + 1; j2 < n; j2++) // 垂直切
            if (ms.query(i, j, m, j2) > 0) // 有苹果
                res = (res + dfs(c - 1, i, j2, memo, ms, m, n)) % MOD;
        for (int i2 = i + 1; i2 < m; i2++) // 水平切
            if (ms.query(i, j, i2, n) > 0) // 有苹果
                res = (res + dfs(c - 1, i2, j, memo, ms, m, n)) % MOD;
        return memo[c][i][j] = res; // 记忆化
    }
}
​
// 二维前缀和模板('A' 的 ASCII 码最低位为 1,'.' 的 ASCII 码最低位为 0)
class MatrixSum {
    private final int[][] sum;
​
    public MatrixSum(String[] matrix) {
        int m = matrix.length, n = matrix[0].length();
        sum = new int[m + 1][n + 1];
        for (int i = 0; i < m; i++) {
            for (int j = 0; j < n; j++) {
                sum[i + 1][j + 1] = sum[i + 1][j] + sum[i][j + 1] - sum[i][j] + (matrix[i].charAt(j) & 1);
            }
        }
    }
​
    // 返回左上角在 (r1,c1) 右下角在 (r2-1,c2-1) 的子矩阵元素和(类似前缀和的左闭右开)
    public int query(int r1, int c1, int r2, int c2) {
        return sum[r2][c2] - sum[r2][c1] - sum[r1][c2] + sum[r1][c1];
    }
}