携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第4天,点击查看活动详情
题目链接:2320. 统计放置房子的方式数
题目描述
一条街道上共有 n * 2
个 地块 ,街道的两侧各有 n
个地块。每一边的地块都按从 1
到 n
编号。每个地块上都可以放置一所房子。
现要求街道同一侧不能存在两所房子相邻的情况,请你计算并返回放置房屋的方式数目。由于答案可能很大,需要对 取余后再返回。
注意,如果一所房子放置在这条街某一侧上的第 i
个地块,不影响在另一侧的第 i
个地块放置房子。
提示:
示例 1:
输入:n = 1
输出:4
解释:
可能的放置方式:
1. 所有地块都不放置房子。
2. 一所房子放在街道的某一侧。
3. 一所房子放在街道的另一侧。
4. 放置两所房子,街道两侧各放置一所。
示例 2:
输入: n = 2
输出: 9
解释: 如上图所示,共有 9 种可能的放置方式。
整理题意
题目给定两行长度为 n
的地块(也就是每行有 n
个地块),需要在这些地块上放置房子,要求在同一行上的房子不能够相邻,两行独立存在,互不影响。
问放置房屋的方案数。题目提示由于答案可能很大,需要对 取余后再返回。
解题思路分析
由于题目说两行之间放置房子没有限制,独立存在且互不影响,所以可以先考虑一行摆放的方案数,那么两行摆放的方案数就可以使用 乘法原理 进行计算。
那么问题转换为求 n
个位置摆放房子的方案数,要求房子之间不能相邻。
遇到计数问题可以尝试使用动态规划来解决。
具体实现
- 首先定义状态:
dp[i][j]
表示第i
个房子是(j = 1
)否(j = 0
)放置房子的总方案数。 - 状态转移:对于第
i
个房子:- 如果放的话第
i - 1
个房子就不能放,所以:dp[i][1] = dp[i - 1][0]
; - 如果不放的话第
i - 1
个房子可以放也可以不放,所以:dp[i][0] = dp[i - 1][0] + dp[i - 1][1]
;
- 如果放的话第
- 初始化边界状态:
dp[0][0] = 1
和dp[0][1] = 0
两种状态,分别表示没有地块的时候房子放和不放的方案数。
为什么只初始化
dp[0][0]
和dp[0][1]
,是因为递推式用到了dp[i - 1][0]
和dp[i - 1][1]
,当i
从1
开始递推时需要初始化0
为边界两种状态即可。
- 最后递推求得第
n
个房子放置和不放置的方案数总和为:tol = dp[n][0] + dp[n][1]
- 使用乘法原理即可求得两行的总方案数:
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
个房子放或者不放的总方案数。那我们可以直接记录每个位置放和不放的总方案数即可。
- 重新定义状态:
dp[i]
表示前i
个地块放置房子的方案数,这里包括放和不放第i
个地块的房子。 - 状态转移:对于第
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]
(标准的斐波那契数列)
- 如果不放的话第
- 初始化边界状态:
dp[0] = 1
和dp[1] = 2
,因为没有地块的时候不放房子也是一种方案,当有一块地块的时候放和不放总共有两种方案。
为什么边界要考虑
0
和1
,因为根据状态转移方程可以得知i
不能从0
和1
开始,会导致下标出现负数的情况,所以需要初始化0
和1
的情况。
- 最终递推至得第
n
个房子放置和不放置的方案数总和为:tol = dp[n]
- 使用乘法原理即可求得两行的总方案数:
tol * tol
优化时间
同时我们可以提前预处理:将 dp[1]
到 dp[10000]
进行预处理(因为数据范围 n
在 [1, 10000]
之内),这样就不用每次都递推一遍,预处理后直接返回 dp[n] * dp[n]
即可。
复杂度分析
- 时间复杂度:
,
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;
}
};
总结
- 计数问题可以尝试使用动态规划来解决。
- 该题类似于斐波那契数列,可以通过递推得到答案。
- 最终结果需要使用 乘法原理。
- 动态规划种初始化边界需要根据转移方程来确定。
- 测试结果:
可以看到优化后的效果还是很明显的。
结束语
动手慢一秒,就可能与机会失之交臂;汗水少一分,也许就与梦想的实现擦肩而过。眼前的不利,可能是五年、十年前的懈怠造成的,而今天的付出,将会成为对未来最好的馈赠。