C语言之李白打酒(蓝桥杯省B)

题目描述

话说大诗人李白,一生好饮。幸好他从不开车。

一天,他提着酒壶,从家里出来,酒壶中有酒 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)状态可能有两种方式:

  1. 从遇店而来:(i-1,j,k/2) → (i,j,k)

  2. 从遇花而来:(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,即使后面全是花也喝不完

因为是累加,所以输出的最后一个状态的方案数就是总方案数。

相关推荐
5 小时前
java关于内部类
java·开发语言
好好沉淀5 小时前
Java 项目中的 .idea 与 target 文件夹
java·开发语言·intellij-idea
lsx2024065 小时前
FastAPI 交互式 API 文档
开发语言
VCR__5 小时前
python第三次作业
开发语言·python
码农水水5 小时前
得物Java面试被问:消息队列的死信队列和重试机制
java·开发语言·jvm·数据结构·机器学习·面试·职场和发展
wkd_0075 小时前
【Qt | QTableWidget】QTableWidget 类的详细解析与代码实践
开发语言·qt·qtablewidget·qt5.12.12·qt表格
东东5165 小时前
高校智能排课系统 (ssm+vue)
java·开发语言
余瑜鱼鱼鱼5 小时前
HashTable, HashMap, ConcurrentHashMap 之间的区别
java·开发语言
m0_736919105 小时前
模板编译期图算法
开发语言·c++·算法
【心态好不摆烂】5 小时前
C++入门基础:从 “这是啥?” 到 “好像有点懂了”
开发语言·c++