(经典dp) I型 L型 铺盖2*n

前言

既然是都是经典dp了,那各个平台的标题和描述难免有些差异

各平台

吐槽一句,作为一道非常经典的dp,2022蓝桥杯初赛直接题面都不编一下故事就拿来考了

洛谷:P1990 覆盖墙壁

力扣:790. 多米诺和托米诺平铺

蓝桥杯:积木画 “蓝桥杯”练习系统

等等平台

题面描述

1*2的I型三个单位的L型铺盖2*n的取余,求方案数

下图来自力扣

这两玩意还有专门的名称多米诺托米诺

lc-domino.jpg (362×195) (leetcode.com)

长度为3时的所有情况

lc-domino1.jpg (803×363) (leetcode.com)

题解

正常分析前缀和

暴力枚举前几个情况总结规律 推荐题解:找不到规律?请看图!0x3f

个人的分析解释,可能有点玄学

定义dp[n]表示到第i个位置,正好能平铺的方案数


  • 考虑最后放的是1*2的I型

    • 竖着放1个 dp[i - 1]
    • 横着放2个 dp[i - 2]
  • 考虑最后放L型

L型的链接边必须放在最右侧,单个点放于左侧和左侧的突出点链接

L型的摆放具有对称性,因此只考虑一种情况最后*2即可

核心: 左侧区域至少也要有一个L型来进行缺口的补充 (即上图的情况45)

两个L型拼接占用长度为3

解释1:

# 中间补充奇数个1*2
# i-4
▢▢▣▣
▢◉◉▣
# i-6
▢▢◉◉▣▣
▢◉◉◉◉▣

# 中间补充偶数个1*2
# i-3
▢▣▣
▢▢▣
# i-5
▢◉◉▣▣
▢▢◉◉▣

# 综上 从i-3到0的所有可能叠加
# 即 i-3的前缀和

解释2

有点玄学

# 本质是用一个突出的点来拟合最右侧的L
▣▣
◉▣
# 这个点必须伴随左侧有一个L的提供 因此长度至少是i-3
▢▣▣
▢▢▣
# 将拟合点的作用分配给前面i-3的每个位置上
# 累计所有情况,前缀和
const int mod = 1e9 + 7;
const int M = 10 + 1000;

long long dp[M];
long long pre[M];

int __init__ = []() {
    // 0长度也是一种情况
    dp[0] = 1, dp[1] = 1, dp[2] = 2;
    pre[0] = 1, pre[1] = 2, pre[2] = 4;
    for (int i = 3; i < M; i += 1) {
        dp[i] = (dp[i - 1] + dp[i - 2] + 2 * pre[i - 3]) % mod;
        pre[i] = (pre[i - 1] + dp[i]) % mod;
    }
    return 0;
}();

class Solution {
public:
    int numTilings(int n) {
        return (int)::dp[n];
    }
};

普通线性dp表示

有些朋友直接列举前几项情况,玄学般的得出结论

dp[n] = 2 * dp[n - 1] + dp[i - 3]

其实就是将前缀和化简通向公式

① dp[n] = dp[n - 1] + dp[n - 2] + 2 * (dp[n - 3] + ... + 1)
② dp[n - 1] = dp[n - 2] + dp[n - 3] + 2 * (dp[n - 4] + ... + 1)
① - ② => dp[n] = 2 * dp[n - 1] + dp[n - 3];
const int mod = 1e9 + 7;
const int M = 10 + 1000;

long long dp[M];

int __init__ = []() {
// ① dp[n] = dp[n - 1] + dp[n - 2] + 2 * (dp[n - 3] + ... + 1)
// ② dp[n - 1] = dp[n - 2] + dp[n - 3] + 2 * (dp[n - 4] + ... + 1)
// ① - ② => dp[n] = 2 * dp[n - 1] + dp[n - 3];
    dp[0] = 1;
    dp[1] = 1;
    dp[2] = 2;
    for (int i = 3; i < M; i += 1) {
        dp[i] = (2 * dp[i - 1] + dp[i - 3]) % mod;
    }
    return 0;
}();

class Solution {
public:
    int numTilings(int n) {
        return (int)::dp[n];
    }
};

状态机dp

放一张力扣官方的题解图

1.png (721×481) (leetcode-cn.com)

将问题的情况进行广义化

原题问的是最后一列铺满的情况

我们考虑所有情况进行广义化

共四种情况

  • 最后一列 空
  • 最后一列 上方一个点
  • 最后一列 下方一个点
  • 最后一列 铺满

定义 dp[][4] 表示第i个位置处于状态j (i位置前是铺满态)

上图的正方形有点迷惑性,直接展示了一个正方形,我们关心的是最后一列

考虑对第i列作用,并且使得第i-1正好补全


思维坑:为什么不要dp[i][0] += dp[i-1][0] 放一个I

# 若 dp[i][0] += dp[i-1][0] 放一个I
# 则可表示为下面的状态

# 放置前(准备放)
i-2 i-1 i
 ▣  空  空
 ▣  空  空
# 放置后(完成放置)
i-2 i-1 i
 ▣  ▣  空
 ▣  ▣  空

# 看似完成了,但这里的实质是对i-1的操作,不符合我们对dp[][4]的定义
# 换一种思路,这种情况已经在dp[i-1][3]中考虑了
const int mod = 1e9 + 7;
const int M = 10 + 1000;

// 表示第i-1位前全铺满
// 第i位所处的状态的种类
// 0 空
// 1 上点
// 2 下点
// 3 满
long long dp[M][4];

int __init__ = []() {
    // i-1状态全满,则第0的状态处理视为满的
    dp[0][3] = 1;
    for (int i = 1; i < M; i += 1) {
        // 空则 前满+不操作
        dp[i][0] = dp[i - 1][3];
        // 上点 前空+L 前下点+一
        dp[i][1] = dp[i - 1][0] + dp[i - 1][2];
        // 下点 前空+L 前上点+一
        dp[i][2] = dp[i - 1][0] + dp[i - 1][1];
        // 满 前空+二 前满+I 前上点+L 前下点+L
        dp[i][3] = dp[i - 1][0] + dp[i - 1][3] + dp[i - 1][1] + dp[i - 1][2];
        for (int j = 0; j < 4; j += 1) {
            dp[i][j] %= mod;
        }
    }
    return 0;
}();

class Solution {
public:
    int numTilings(int n) {
        return (int)::dp[n][3];
    }
};



END

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值