问题描述:
- 给定一个
m
×
n
m\times n
m×n 的二维网格,并且给定
i
c
ic
ic 个内向的人和
e
c
ec
ec 个外向的人并尝试将这些人放置到网格中。
- 不需要所有人都在网格中。
- i i i 人放置在网格中就会加 120 120 120 幸福感,但每存在一个邻居都会失去 30 30 30 幸福感。
- e e e 人放置在网格中就会加 40 40 40 幸福感,且每存在一个邻居都会得到 20 20 20 幸福感。
- 返回最大可能的幸福感总和。
- 满足以下条件:
- 1 ≤ m , n ≤ 5 1\leq m,n\leq 5 1≤m,n≤5。
- 0 ≤ i c , e c ≤ min ( m ∗ n , 6 ) 0\leq ic,ec\leq \min(m*n,6) 0≤ic,ec≤min(m∗n,6)。
解题思路:
- 该题可以用动态规划来做,转移的思路比较简单,但是要进行相当多的处理。
- 下面的讨论中,将用积分代表幸福感, i i i 人代表内向的人, e e e 人代表外向的人。
- 首先要理解的是要求得积分,只需要统计总体积分即可,不需要实时更新网格中角色的积分。
- 每一个网格均有三种情况: 0 0 0 代表网格无人, 1 1 1 代表放置 i i i 人, 2 2 2 代表放置 e e e 人。
- 由网格左上逐一向右下放置人员,假设此时将于
(
i
,
j
)
(i,j)
(i,j) 中放置人员,则可得知以下几点:
- 前 i i i 行( 0 ∼ i − 1 0\sim i-1 0∼i−1 行)已经放置好人员。
- 为 ( i , j ) (i,j) (i,j) 放置人员只会影响到 ( i − 1 , j ) (i-1, j) (i−1,j) 和 ( i , j − 1 ) (i, j-1) (i,j−1),即只会影响左邻居和上邻居,但因为后续人员未有放置,因此此时不需要考虑对右邻居和下邻居的影响。
- 由前面第 2 点可以知道:
- 当我们要在第 i i i 行放置人员并需要更新积分时,只需要考虑第 i i i 行产生的行内积分,以及第 i i i 行和 i − 1 i-1 i−1 行之间产生的行间积分。【状态转移】
- 因此我们只需要利用动态规划思想,往递归函数中传递当前行数、剩余 i i i 人数目、剩余 e e e 人数目。
- 因为 n ≤ 5 n\leq 5 n≤5,所以单独某一行的所有可能情况数为 3 n 3^n 3n,用变量 m a s k ∈ [ 0 , 3 n − 1 ] mask\in [0,3^n-1] mask∈[0,3n−1] 代表当前行的情况,我们只需要遍历 m m m 行的所有符合条件的 m a s k mask mask 即可更新得到最大的总体积分。【停止条件之一:遍历完 m m m 行】
- 所谓的符合条件的 m a s k mask mask 指的就是在当前递归中, m a s k mask mask 所代表的当行放置情况中的 i i i 人数目和 e e e 人数目,分别不能超过当前剩余 i i i 人数目和当前剩余 e e e 人数目。【剪枝】【停止条件之二:剩余 i i i 人数目和剩余 e e e 人数目均为 0 0 0】
- 因为 m m m、 n n n 的范围较小,因此可以通过提前计算得到不同 m a s k mask mask 的行内积分和它们之间的行间积分。
- 最后总结以下:
- 该题考察了动态规划(记忆化搜索)、位运算(三进制计算确定当前行的状态掩码 m a s k mask mask 并用掩码 m a s k mask mask 进行行内积分与行间积分的计算),即状态压缩 dp 题目。
- 该题的动态规划转移思路不难,但是难于考虑到用状态压缩思路进行一系列的预处理。
- 该题的较为简单解法就是前面讨论的三进制逐行枚举,而官方解答中的方法二则是逐格枚举(基于轮廓线的动态规划),方法二细节更多,懒得看了,以后再做这题的时候再看看。
代码实现:
- 代码参考自用户 zerotrac 的题解(参考内容二),加了一些注释:
class Solution { private: int masks[243][5]; // (masks,3^5*5) n<=5,masks代表单行所有可能情况的集合,其预处理是为了快速取出集合中单个mask对应位置上的值 int in[243], en[243], s[243], os[243][243]; //mask对应的(in i人数,en e人数,s 行内分数,os 行间分数) int dp[243][5][7][7]; // dp[上一行mask][当前行][剩余i人][剩余e人] int help[3][3] = {{0,0,0},{0,-60,-10},{0,-10,40}}; // 邻居间的积分更新,如help[1][2]表示i人和e人相邻的积分更新 public: int getMaxGridHappiness(int m, int n, int ic, int ec) { // 1 预处理所有数据 int n3 = pow(3, n); // 本次调用中mask的最大可能情况 for(int mask = 0; mask < n3; ++mask) { // 1.1 处理masks数组 for(int tmp = mask, i = 0; i < n; ++i, tmp /= 3) masks[mask][i] = tmp % 3; // mask对应的第i位数值究竟是多少(0、1、2) // 1.2 处理in、en、s数组 in[mask] = en[mask] = s[mask] = 0; // 初始化值为0 for(int i = 0; i < n; ++i) { if(masks[mask][i] == 0) continue; // 第i位没有人,则没有分数影响,不需要更新 if(masks[mask][i] == 1) ++in[mask], s[mask] += 120; // 第i位是i人,先更新分数,后续再更新对其他位置的影响 else if(masks[mask][i] == 2) ++en[mask], s[mask] += 40; // 第i位是e人 if(i > 0) // 计算行内人中相互之间的影响,只需要将第i个人和第i-1个人比较更新即可 s[mask] += help[masks[mask][i-1]][masks[mask][i]]; } } // 1.3 处理os数组 for(int m0 = 0; m0 < n3; ++m0) for(int m1 = 0; m1 < n3; ++m1) { os[m0][m1] = 0; // 初始化值为0 for(int i = 0; i < n; ++i) os[m0][m1] += help[masks[m0][i]][masks[m1][i]]; } // 2 动态规划 memset(dp,-1,sizeof(dp)); // 初始化dp数组 // pmask表示上一行mask,row表示当前行数,ic表示剩余i人数,ec表示剩余e人数 function<int(int, int, int, int)> dfs = [&](int pmask, int row, int ic, int ec) ->int { if(row == m or ic+ec == 0) return 0; // 停止条件:处理完网格或者无人可放置 if(dp[pmask][row][ic][ec] != -1) return dp[pmask][row][ic][ec]; int score = 0; for(int mask = 0; mask < n3; ++mask) { if(ic < in[mask] or ec < en[mask]) continue; // 如果当前mask的人数不足,则跳过即可 score = max(score, s[mask]+os[mask][pmask]+dfs(mask, row+1, ic-in[mask], ec-en[mask])); } return dp[pmask][row][ic][ec] = score; }; return dfs(0, 0, ic, ec); } };