0
点赞
收藏
分享

微信扫一扫

每日一题 —— LC. 790 多米诺和托米诺


有两种形状的瓷砖:一种是 ​​2 x 1​​​ 的多米诺形,另一种是形如 “​​L​​” 的托米诺形。两种形状都可以旋转。

每日一题 —— LC. 790 多米诺和托米诺_算法

给定整数 ​​n​​​ ,返回可以平铺 ​​2 x n​​​ 的面板的方法的数量。返回对 ​​10^9 + 7​​ 取模 的值。

平铺指的是每个正方形都必须有瓷砖覆盖。两个平铺不同,当且仅当面板上有四个方向上的相邻单元中的两个,使得恰好有一个平铺有一个瓷砖占据两个正方形。

示例

每日一题 —— LC. 790 多米诺和托米诺_i++_02

输入: n = 3
输出: 5
解释: 五种不同的方法如上所示。

状态表示

我们从左到右依次扫描每一列,并维护当前列的状态。

这样来定义状态数组:

设​​dp[i][s]​​​表示,前​​i​​​列已经全部填满,且第​​i + 1​​​列的填充状态为​​s​​时的总方案数。

比如,容易得知,​​dp[0][0] = 1​​​。什么意思呢?就是把第​​0​​​列全部填满,且第​​1​​​列的填充状态为0(一个方块也没有被填充),这种状态可以通过在第​​0​​​列竖着插入一个​​2 × 1​​的多米诺形达到,所以其方案数为1,如下图

每日一题 —— LC. 790 多米诺和托米诺_状态压缩_03

我们继续,看一下在第​​0​​​列被填满,第​​1​​列的状态还可能是哪些呢?比较明显,还有下面3种情况:

每日一题 —— LC. 790 多米诺和托米诺_状态压缩_04

每日一题 —— LC. 790 多米诺和托米诺_状态转移_05

每日一题 —— LC. 790 多米诺和托米诺_i++_06

这三种情况对应的状态分别是

​dp[0][3]​​​(3的二进制表示是11,表示第​​1​​列的2个方块都被填充了)

​dp[0][2]​​​(2的二进制表示是10,高位的1表示第​​1​​列最顶部的方块被填充了)

​dp[0][1]​​​(1的二进制表示是01,低位的1表示第​​1​​列最底部的方块被填充了)

这4种状态就是我们动态规划的边界条件,或者初始状态。

所以我们的初始状态有:​​dp[0][0] = dp[0][1] = dp[0][2] = dp[0][3] = 1​​。

接下来看看最终答案是什么。很明显,最终答案应当是,将前​​n - 1​​​列全部填满,且第​​n​​​列的填充状态为0时的总方案数,即​​dp[n - 1][0]​​。

状态转移

然后来看状态转移。

我们初始时先把第​​0​​​列填满了,所以我们从第​​1​​​列开始填充,即​​i​​​从​​1​​​开始循环。
每次枚举,前​​​i - 1​​​列都全部填满时,第​​i​​​列的状态 (一共只有4种状态) ,然后看看当第​​i​​​列处于某种状态时,我们可以如何把第​​i​​列填满。

  1. i - 1列都填满,且第​i​列的状态为​00​时(​dp[i - 1][0]​)

每日一题 —— LC. 790 多米诺和托米诺_状态转移_07

此时,对于第​​i​​列,我们可以

  • 竖着插入一个 ​​2 × 1​​​的多米诺,此时能得到这样的状态:前​​i​​​列都填满了,且第​​i + 1​​​列的状态为​​00​​​(​​dp[i][0]​​)
  • 横着插入2个​​2 × 1​​​的多米诺,此时第​​i + 1​​​列的状态为​​11​​​(​​dp[i][3]​​)
  • 插入1个​​L​​​形的托米诺,由于这个​​L​​​形的托米诺能够旋转,所以能得到两种​​i + 1​​​列的状态:​​01​​​和​​10​​​(​​dp[i][1]​​​和​​dp[i][2]​​)

所以,由​​dp[i - 1][0]​​​,能够转换到​​dp[i][0]​​​,​​dp[i][3]​​​,​​dp[i][1]​​​,​​dp[i][2]​​​。也就是说​​dp[i - 1][0]​​​对​​dp[i][0~3]​​​全部4种状态都有贡献,所以转换到​​dp[i][0~3]​​​这4种状态时,都要加上​​dp[i - 1][0]​​。

  1. i - 1列都填满,且第​i​列的状态为​01​时(​dp[i - 1][1]​)

每日一题 —— LC. 790 多米诺和托米诺_状态转移_08

此时,对于第​​i​​列,我们可以

  • 横着插入一个​​2 × 1​​​的多米诺,第​​i + 1​​​列的状态为​​10​​​(​​dp[i][2]​​)
  • 插入一个​​L​​​形的托米诺,第​​i + 1​​​列的状态为​​11​​​(​​dp[i][3]​​)

所以,由​​dp[i - 1][1]​​​,能够转换到​​dp[i][2]​​​,​​dp[i][3]​

  1. i - 1列都填满,且第​i​列状态为​10​时(​dp[i - 1][2]​)

每日一题 —— LC. 790 多米诺和托米诺_动态规划_09

此时,对于第​​i​​列,我们可以

  • 横着插入一个​​2 × 1​​​的多米诺,第​​i + 1​​​列的状态为​​01​​​(​​dp[i][1]​​)
  • 插入一个​​L​​​形的托米诺,第​​i + 1​​​列的状态为​​11​​​(​​dp[i][3]​​)

所以,由​​dp[i - 1][2]​​​,能够转换到​​dp[i][1]​​​,​​dp[i][3]​

  1. i - 1列都填满,且第​i​列状态为​11​时(​dp[i - 1][3]​)

每日一题 —— LC. 790 多米诺和托米诺_动态规划_10

此时,对于第​​i​​​列,我们无法再插入任何方块。只能得到第​​i + 1​​​列的状态为​​00​​​(​​dp[i][0]​​)

所以,由​​dp[i - 1][3]​​​,能够转换到​​dp[i][0]​

我们将上面,​​dp[i - 1][0~3]​​​与​​dp[i][0~3]​​的关系,进行一下整理,容易得到状态转移方程如下

  • ​dp[i][0] = dp[i - 1][0] + dp[i - 1][3]​
  • ​dp[i][1] = dp[i - 1][0] + dp[i - 1][2]​
  • ​dp[i][2] = dp[i - 1][0] + dp[i - 1][1]​
  • ​dp[i][3] = dp[i - 1][0] + dp[i - 1][1] + dp[i - 1][2]​

代码

于是就能写出如下代码

typedef long long LL;
const int MOD = 1e9 + 7;
class Solution {
public:
int numTilings(int n) {
vector<vector<LL>> dp(n, vector<LL>(4, 0));
// dp[i][s]表示前i列已经填好, 伸出去第i + 1列情况为 s 时的方案数
dp[0][0] = dp[0][1] = dp[0][2] = dp[0][3] = 1;
for (int i = 1; i < n; i++) {
dp[i][0] = (dp[i - 1][0] + dp[i - 1][3]) % MOD;
dp[i][1] = (dp[i - 1][0] + dp[i - 1][2]) % MOD;
dp[i][2] = (dp[i - 1][0] + dp[i - 1][1]) % MOD;
dp[i][3] = (dp[i - 1][0] + dp[i - 1][1] + dp[i - 1][2]) % MOD;
}
// 答案返回前n - 1列已经全部填好, 且伸出去第i + 1列情况为0时的方案数
return (int) dp[n - 1][0];
}
};

并且由于​​dp[i][0~3]​​​只依赖于​​dp[i - 1][0~3]​​,还能用滚动数组进行优化

typedef long long LL;
const int MOD = 1e9 + 7;
class Solution {
public:
int numTilings(int n) {
LL s0, s1, s2, s3;
s0 = s1 = s2 = s3 = 1;
for (int i = 1; i < n; i++) {
LL t0 = (s0 + s3) % MOD;
LL t1 = (s0 + s2) % MOD;
LL t2 = (s0 + s1) % MOD;
LL t3 = (s0 + s1 + s2) % MOD;
s0 = t0;
s1 = t1;
s2 = t2;
s3 = t3;
}
return (int) s0;
}
};

PS: 这些都是俺从y总那里学到的(●’◡’●)

y总牛逼!(❤ ω ❤)



举报

相关推荐

0 条评论