问题描述
生物学家小 R 正在研究一种特殊的兔子品种的繁殖模式。这种兔子的繁殖遵循以下规律:
- 每对成年兔子每个月会生育一对新的小兔子(一雌一雄)。
- 新生的小兔子需要一个月成长,到第二个月才能开始繁殖。
- 兔子永远不会死亡。
小 R 从一对新生的小兔子开始观察。他想知道在第 A 个月末,总共会有多少对兔子。
请你帮助小 R 编写一个程序,计算在给定的月份 A 时,兔子群体的总对数。
注意:
- 初始时有 1 对新生小兔子。
- 第 1 个月末有 1 对兔子:原来那对变成了成年兔子,并开始繁殖。
- 第 2 个月末有 2 对兔子:原来那 1 对成年兔子,繁殖了 1 对新生的小兔子。
- 从第 3 个月开始,兔子群体会按照上述规律增长。
思路分析
这是一个典型的动态规划问题,下面我将使用递推法来实现代码。
- 对于兔子而言,它需要经过出生、成熟、繁殖三个过程,所以我们利用
dp[A+1][3]来存储每日3类兔子的数量。 dp[n][0]表示第n天的幼兔数量,dp[n][1]表示第n天的成熟兔子数量,dp[n][2]表示第n天的成兔数量。- 只有成兔才能繁殖,定义状态转移方程,如下代码所示。
public class Main {
public static long solution(int A) {
// Edit your code here
long[][] dp = new long[A + 1][3];
if (A == 1) {
return 1L;
}
dp[0][0] = 1L; // 幼兔
dp[0][1] = 0L;
dp[0][2] = 0L; // 成兔
dp[1][0] = 0L;
dp[1][1] = 1L;
dp[1][2] = 0L;
for (int i = 2; i <= A; i++) {
dp[i][0] = dp[i-1][1] + dp[i-1][2];
dp[i][1] = dp[i-1][0];
dp[i][2] = dp[i-1][1] + dp[i-1][2];
}
return dp[A][0] + dp[A][1] + dp[A][2];
}
public static void main(String[] args) {
// Add your test cases here
System.out.println(solution(1) == 1L);
System.out.println(solution(5) == 8L);
System.out.println(solution(15) == 987L);
}
}
对于动态规划问题,常见的方法有两种,分别是备忘录法和递推法。下面我将解释这两种方法。
递推法(Tabulation / 迭代法)
定义:
- 递推法是一种自底向上的方法,从最简单的情况开始逐步构建解,直到得到最终结果。
- 它通常使用一个数组或表格来存储中间结果,并通过迭代的方式更新这些结果。
特点:
- 自底向上:从已知的初始状态开始,逐步计算出更复杂的状态,直到达到目标状态。
- 空间效率:通常只需要一个固定大小的数组来存储中间结果,空间复杂度较低。
- 易于理解和实现:逻辑清晰,容易理解,且不需要处理递归调用栈的问题。
- 适用于所有子问题:即使某些子问题不会被后续计算用到,也会被计算出来。
备忘录法(Memoization)
定义:
- 备忘录法是一种自顶向下的方法,它通过递归来解决问题,并在递归过程中存储已经计算过的结果,以避免重复计算。
- 它通常使用一个哈希表或数组来存储中间结果,并在递归调用时检查这些结果是否已经存在。
特点:
- 自顶向下:从目标状态开始,逐步分解为更简单的子问题,直到达到基本情况。
- 空间效率:可能需要额外的空间来存储递归调用栈,但对于某些问题,可以减少不必要的计算。
- 避免重复计算:通过缓存已经计算过的结果,避免了重复计算相同的子问题。
- 灵活性:只计算必要的子问题,对于某些问题可以提高效率。