【C/C++】2320. 统计放置房子的方式数

933 阅读5分钟

携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第4天,点击查看活动详情


题目链接:2320. 统计放置房子的方式数

题目描述

一条街道上共有 n * 2地块 ,街道的两侧各有 n 个地块。每一边的地块都按从 1n 编号。每个地块上都可以放置一所房子。

现要求街道同一侧不能存在两所房子相邻的情况,请你计算并返回放置房屋的方式数目。由于答案可能很大,需要对 109+710^9 + 7 取余后再返回。

注意,如果一所房子放置在这条街某一侧上的第 i 个地块,不影响在另一侧的第 i 个地块放置房子。

提示:

  • 1n1041 \leqslant n \leqslant 10^4

示例 1:

输入:n = 1
输出:4
解释:
可能的放置方式:
1. 所有地块都不放置房子。
2. 一所房子放在街道的某一侧。
3. 一所房子放在街道的另一侧。
4. 放置两所房子,街道两侧各放置一所。

示例 2:

arrangements.png

输入: n = 2
输出: 9
解释: 如上图所示,共有 9 种可能的放置方式。

整理题意

题目给定两行长度为 n 的地块(也就是每行有 n 个地块),需要在这些地块上放置房子,要求在同一行上的房子不能够相邻,两行独立存在,互不影响。

问放置房屋的方案数。题目提示由于答案可能很大,需要对 109+710^9 + 7 取余后再返回。

解题思路分析

由于题目说两行之间放置房子没有限制,独立存在且互不影响,所以可以先考虑一行摆放的方案数,那么两行摆放的方案数就可以使用 乘法原理 进行计算。

那么问题转换为求 n 个位置摆放房子的方案数,要求房子之间不能相邻。

遇到计数问题可以尝试使用动态规划来解决。

具体实现

  1. 首先定义状态:dp[i][j] 表示第 i 个房子是(j = 1)否(j = 0)放置房子的总方案数。
  2. 状态转移:对于第 i 个房子:
    • 如果放的话第 i - 1 个房子就不能放,所以:dp[i][1] = dp[i - 1][0]
    • 如果不放的话第 i - 1 个房子可以放也可以不放,所以:dp[i][0] = dp[i - 1][0] + dp[i - 1][1]
  3. 初始化边界状态:dp[0][0] = 1dp[0][1] = 0 两种状态,分别表示没有地块的时候房子放和不放的方案数。

为什么只初始化 dp[0][0]dp[0][1],是因为递推式用到了 dp[i - 1][0]dp[i - 1][1],当 i1 开始递推时需要初始化 0 为边界两种状态即可。

  1. 最后递推求得第 n 个房子放置和不放置的方案数总和为:tol = dp[n][0] + dp[n][1]
  2. 使用乘法原理即可求得两行的总方案数:tol * tol

优化空间

根据上述对于第 i 个房子如果放的话 dp[i][1] = dp[i - 1][0],而 dp[i - 1][0] = dp[i - 2][0] + dp[i - 2][1],也就是 dp[i][1] = dp[i - 2][0] + dp[i - 2][1],这么一分析得知,如果放置第 i 个房子,那么第 i - 1 个房子就不能放,也就是确定了这两个位置的放置情况,所以总的方案数就等于第 i - 2 个房子放或者不放的总方案数。那我们可以直接记录每个位置放和不放的总方案数即可。

  1. 重新定义状态:dp[i] 表示前 i 个地块放置房子的方案数,这里包括放和不放第 i 个地块的房子。
  2. 状态转移:对于第 i 个地块:
    • 如果不放的话第 i - 1 个房子可以放也可以不放,所以:dp[i] = dp[i - 1]
    • 如果放的话第 i - 1 个房子就不能放,所以:dp[i] = dp[i - 2]

    这里也就是固定了 i 放置房子,i - 1 不放置房子,固定这两个位置后总的方案数就是 i - 2 放和不放的方案数。

    • 所以状态转移方程为:dp[i] = dp[i - 1] + dp[i - 2](标准的斐波那契数列)
  3. 初始化边界状态:dp[0] = 1dp[1] = 2,因为没有地块的时候不放房子也是一种方案,当有一块地块的时候放和不放总共有两种方案。

为什么边界要考虑 01,因为根据状态转移方程可以得知 i 不能从 01 开始,会导致下标出现负数的情况,所以需要初始化 01 的情况。

  1. 最终递推至得第 n 个房子放置和不放置的方案数总和为:tol = dp[n]
  2. 使用乘法原理即可求得两行的总方案数:tol * tol

优化时间

同时我们可以提前预处理:将 dp[1]dp[10000] 进行预处理(因为数据范围 n[1, 10000] 之内),这样就不用每次都递推一遍,预处理后直接返回 dp[n] * dp[n] 即可。

复杂度分析

  • 时间复杂度:O(n)O(n)n 为一行地块的数量,需要递推一遍。
  • 空间复杂度:O(n)O(n)n 为一行地块的数量,需要存储递推结果。

代码实现

优化前:

class Solution {
public:
    int countHousePlacements(int n) {
        int mod = 1e9 + 7;
        //dp[i][0] 表示第i个位置不放房子的方案数
        //dp[i][1] 表示第i个位置放房子的方案数
        long long int dp[n + 1][2];
        memset(dp, 0, sizeof(dp));
        //初始化边界情况
        dp[1][0] = dp[1][1] = 1;
        //递推每个位置放房子和不放房子的方案数
        for(int i = 2; i <= n; i++){
            //当前位置不放房子时,前一个位置可以放房子也可以不放房子,所以是总和
            dp[i][0] = (dp[i - 1][1] + dp[i - 1][0]) % mod;
            //当前位置放房子时,前一个位置只能不放
            dp[i][1] = dp[i - 1][0];
        }
        //只考虑一条街道时放置房子方案数为 tol
        long long int tol = (dp[n][0] + dp[n][1]) % mod;
        //当考虑两条街道时使用乘法原则
        long long ans = (tol * tol) % mod;
        return ans;

    }
};

优化后:

const int mod = 1e9 + 7;
int dp[10001] = {1, 2};
int init = []() {
    for(int i = 2; i <= 10000; i++){
        dp[i] = (dp[i - 1] + dp[i - 2]) % mod;
    }
    return 0;
}();
class Solution {
public:
    int countHousePlacements(int n) {
        return ((long long)dp[n] * dp[n]) % mod;
    }
};

总结

  • 计数问题可以尝试使用动态规划来解决。
  • 该题类似于斐波那契数列,可以通过递推得到答案。
  • 最终结果需要使用 乘法原理
  • 动态规划种初始化边界需要根据转移方程来确定。
  • 测试结果:

2320.png

2320-优化.png

2320-优化时间.png 可以看到优化后的效果还是很明显的。

结束语

动手慢一秒,就可能与机会失之交臂;汗水少一分,也许就与梦想的实现擦肩而过。眼前的不利,可能是五年、十年前的懈怠造成的,而今天的付出,将会成为对未来最好的馈赠。