打家劫舍
一个环形数组,小偷可以选择偷不相邻下标的现金,求最大偷取金额
首先,如果是线性的,那么是一个经典的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-1,i,i+1,下一次中心节点可以选取i-2,i+2,i-3,i+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 取余的结果。
子问题拆解
如果pizza有m行n列
- 垂直切,枚举左侧列数
w,剩余m行n-w列的矩形,还需k-2刀 - 水平切,枚举上方行数
h,剩余m-h行n列的矩形,还需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];
}
}