题目描述
话说大诗人李白,一生好饮。幸好他从不开车。
一天,他提着酒壶,从家里出来,酒壶中有酒 2 斗。他边走边唱:
无事街上走,提壶去打酒。
逢店加一倍,遇花喝一斗。
这一路上,他一共遇到店 N 次,遇到花 M 次。已知最后一次遇到的是花,他正好把酒喝光了。
请你计算李白这一路遇到店和花的顺序,有多少种不同的可能?
注意:壶里没酒(0 斗)时遇店是合法的,加倍后还是没酒;但是没酒时遇花是不合法的。
输入格式
第一行包含两个整数 N 和 M。
输出格式
输出一个整数表示答案。由于答案可能很大,输出模 1000000007(即 10^9+7)的结果。
cs
输入
5 10
输出
14
说明/提示
【样例说明】
如果我们用 0 代表遇到花,1 代表遇到店,14 种顺序如下:
010101101000000
010110010010000
011000110010000
100010110010000
011001000110000
100011000110000
100100010110000
010110100000100
011001001000100
100011001000100
100100011000100
011010000010100
100100100010100
101000001010100
【评测用例规模与约定】
对于 40% 的评测用例:1≤N,M≤10。
对于 100% 的评测用例:1≤N,M≤100。
cs
#include <stdio.h>
#define MOD 1000000007LL
long long f[105][105][105];
int main() {
int n, m;
// 输入n和m
scanf("%d %d", &n, &m);
// 初始化f数组全为0
for (int i = 0; i <= n; i++) {
for (int j = 0; j <= m; j++) {
for (int k = 0; k <= m; k++) {
f[i][j][k] = 0;//i个店,j朵花,剩k斗酒
}
}
}
// 初始状态:遇0店0花,酒有2斗,方案数为1
f[0][0][2] = 1;
// 动态规划递推
for (int i = 0; i <= n; i++) {
for (int j = 0; j <= m; j++) {
// 跳过初始状态
if (i == 0 && j == 0) continue;
for (int k = 0; k <= 100; k++) {
// 情况1:最后一步遇到店(酒量翻倍)
if (k % 2 == 0 && i > 0) {
f[i][j][k] = (f[i][j][k] + f[i-1][j][k/2]) % MOD;
}
// 情况2:最后一步遇到花(喝1斗酒)
if (j > 0 && k + 1 <= m) {
f[i][j][k] = (f[i][j][k] + f[i][j-1][k+1]) % MOD;
}
}
}
}
// 输出结果:遇到n个店,m-1个花,剩1斗酒的方案数
printf("%lld\n", f[n][m-1][1]);
return 0;
}
一.核心思路:逆向动态规划
关键思想:题目要求最后遇到花且酒喝完,我们从最终状态逆向推导到初始状态。
二.解析一下动态规划
三维动态规划:特别解析一下该动态规划的代码
// 动态规划递推
for (int i = 0; i <= n; i++) {
for (int j = 0; j <= m; j++) {
// 跳过初始状态
if (i == 0 && j == 0) continue;
for (int k = 0; k <= 100; k++) {
// 情况1:最后一步遇到店(酒量翻倍)
if (k % 2 == 0 && i > 0) {
f[i][j][k] = (f[i][j][k] + f[i-1][j][k/2]) % MOD;
}
// 情况2:最后一步遇到花(喝1斗酒)
if (j > 0 && k + 1 <= m) {
f[i][j][k] = (f[i][j][k] + f[i][j-1][k+1]) % MOD;
}
}
}
}
出现这么多符合题意的情况,主要是两种情况共同实现的:最后一步遇到店或最后一步遇到花
为什么需要每次计算的时候还加上f[i][j][k]?
1.方案数的累加:
f[i][j][k] 代表到达状态(i,j,k)的总方案数。
到达这个状态可能有多条不同的路径,我们需要累加所有可能的路径。
因为到达(i,j,k)状态可能有两种方式:
-
从遇店而来:
(i-1,j,k/2) → (i,j,k) -
从遇花而来:
(i,j-1,k+1) → (i,j,k)
注意 :等式左边 的 f[i][j][k] 和右边 的 f[i][j][k] 不是同时计算的!
cs
// 时间点1:开始计算 f[i][j][k] 时
// f[i][j][k] 的值是未定义的(或者是0,如果没被初始化)
// 时间点2:检查是否可以从遇店转移
if (k % 2 == 0 && i) {
// 此时 f[i][j][k] 还没有被赋值(或者是0)
f[i][j][k] = f[i][j][k] + f[i-1][j][k/2];
// 现在 f[i][j][k] 被赋值了
}
// 时间点3:检查是否可以从遇花转移
if (j && k + 1 <= m) {
// 此时 f[i][j][k] 可能已经有值了(从上一步来的)
f[i][j][k] = f[i][j][k] + f[i][j-1][k+1];
// 现在的 f[i][j][k] 包含了两种可能
}
cs
假设我们要计算 f[2][2][4]:
// 初始:f[2][2][4] = 0
// 第一步:检查是否可以从遇店转移
// k=4是偶数,i=2>0 ✓
// f[1][2][2] 假设值为 5
f[2][2][4] = 0 + 5 = 5
// 第二步:检查是否可以从遇花转移
// j=2>0,k+1=5<=m(假设m=5)✓
// f[2][1][5] 假设值为 3
f[2][2][4] = 5 + 3 = 8
所以最终 f[2][2][4] = 8,它等于两种路径方案数之和。
为什么要对MOD取余?
1.题目要求:结果对 1,000,000,007 (1e9+7) 取模
2.在动态规划中,方案数可能非常非常大,超过数据类型表示范围
// 假设 n=50, m=50
// 方案数可能达到的数量级:
// C(100,50) ≈ 1.0e29 // 100步中选50步遇店
// 这个数字远远超过 long long 的最大值 (约9.2e18)
三.运行过程
初始化问题:
-
初始:0店0花,酒2斗
-
最终:5店10花,最后一步遇花
-
答案:
f[5][9][1](5店9花,剩1斗酒)
cs
初始化:
f[0][0][2] = 1
其他所有状态 = 0
上述代码的初始化是根据题干意思,没有经过店和花,所以i=0,j=0,一开始剩余的酒是2斗,所以要初始化f[0][0][2]这个确定的状态。
f[i][j][k]表示的是方案数,而对于上述状态只有一种方式到达,因为一开始就在那里。
但是为什么要初始化其他所有状态是0呢?
因为其他的状态还不能确定,所以就只能先初始化为0
1.未初始化的变量包含垃圾值
cs
long long f[105][105][105]; // 声明数组
// 此时数组中的值是未定义的,是内存中的随机值
// 可能是0,也可能是任何其他值
2.不初始化有危险
cs
// 错误示例:不初始化
long long f[105][105][105];
f[0][0][2] = 1;
// 当计算 f[1][1][2] 时:
f[1][1][2] = f[1][1][2] + f[0][1][1];
// f[1][1][2] 是未初始化的随机值!
// 可能是 1234567890,导致结果完全错误
3.根据转移方程,必须要进行初始化为0
条件问题
大多数条件能想到是根据求出的转移方程。但是唯独第二个if条件循环不是,因为要考虑到k+1<=m,这个条件是因为遇到花和酒有关系,所以考虑出来这个条件。
-
k:当前酒量(斗) -
m:总共需要遇到的花的数量
关键是要考虑最多能喝多少酒
-
总共要遇到
m次花 -
每次遇花喝 1 斗
-
所以 最多能喝 m 斗酒
因为这道题目是逆向思维
如果当前酒量是 k,且这是从遇花操作得到的:
-
前一个状态酒量是
k+1(喝1斗变成k) -
那么
k+1不能超过m -
因为如果前状态酒量 > m,即使后面全是花也喝不完
因为是累加,所以输出的最后一个状态的方案数就是总方案数。