百鸡问题-洛谷B3836 [GESP202303 二级]

题目描述

“百鸡问题”是出自我国古代《张丘建算经》的著名数学问题。大意为:

“每只公鸡 5 元,每只母鸡 3 元,每 3 只小鸡 1 元;现在有 100 元,买了 100 只鸡,共有多少种方案?”

小明很喜欢这个故事,他决定对这个问题进行扩展,并使用编程解决:如果每只公鸡 x 元,每只母鸡 y 元,每 z 只小鸡 1 元;现在有 n 元,买了 m 只鸡,共有多少种方案?

输入格式

输入一行,包含五个整数,分别为问题描述中的 x,y,z,n,m。约定 1≤x,y,z≤10,1≤n,m≤1000。

输出格式

输出一行,包含一个整数 C,表示有 C 种方案。

说明/提示

【样例 1 解释】

这就是问题描述中的“百鸡问题”。4 种方案分别为:

  • 公鸡 0 只、母鸡 25 只、小鸡 75 只。
  • 公鸡 4 只、母鸡 18 只、小鸡 78 只。
  • 公鸡 8 只、母鸡 11 只、小鸡 81 只。
  • 公鸡 12 只、母鸡 4 只、小鸡 84 只。

GESP二级编程考试重点考察的是字符和嵌套循环的编程能力。因此对于嵌套循环类题目,考生必须具备暴力枚举和枚举优化能力,暴力能够拿部分分,只有优化才能拿满分,因此要求练习时先写出纯暴力枚举代码,再逐步优化。只有进行优化循环次数和优化循环层数的大量练习,才能确保顺利通过GESP二级考试。

       枚举算法两大特点:①解的有限性、②解的可判定性。枚举问题所有可能的解必须是有限个,且能在合理时间内列举完,如果解是无限的(比如实数范围内的解),枚举将无法终止;如果无法判定某个候选解是否正确,枚举就失去意义,判定过程必须明确无歧义。

1、初步无脑思路:暴力枚举

       这种无脑思路,就是将每一种买鸡的情况罗列出来,由计算机来检查是否符合题目的要求,所谓题目要求就是等量关系:钱等量关系是100钱,鸡的等量关系也是100只。

    其实小学生都学过枚举思维了,只不过这次需要让计算机代替人去做,这是计算机最擅长做的事情。越往高级别学习,尤其到了算法阶段的学习,本质上都是让计算机更加聪明的执行枚举过程。好的我们来分析题目吧!

    题目中描述的公鸡、母鸡、小鸡三种物品,因此我们需要三层嵌套循环来实现题目的暴力枚举,假设公鸡i只,母鸡j只,小鸡k只,可以得到如下等式关系:

    百钱关系:5i+3j+k/3=100隐含关系:小鸡只数为3的倍数;

    百鸡关系:i+j+k=100

   根据以上等式关系,通过暴力枚举公鸡、母鸡、小鸡的只数范围(0~100)进行解题。

#include <bits/stdc++.h>
using namespace std;
int main(){
    for(int i = 0; i <= 100; i++){
        for(int j = 0; j <= 100; j++){
            for(int k = 0; k <= 100; k+=3){
                // 只数等量       钱数等量
                if(i+j+k==100 && 5*i+3*j+k/3 == 100){
                    cout << "公鸡" << i << " 母鸡" << j << " 小鸡" << k << endl;
                }
            }
        }
    }
    return 0;
}

2、枚举次数优化

      我们对于枚举,可以进行优化,而不是像上面那样无脑循环遍历与枚举!首先思考减少枚举数据的范围。对于本题我们不难发现,公鸡和母鸡的数目不用从0枚举到100,买到的公鸡在达到20只时,就已经达到了预期的100钱。同理母鸡只数最多到33只,因此我们修改程序,将循环的范围缩小。

#include <bits/stdc++.h>
using namespace std;
int main(){
    for(int i = 0; i<=20; i++){
        for(int j = 0; j<=33; j++){
            for(int k = 0; k <= 100; k+=3){
                // 只数等量       钱数等量
                if(i+j+k==100 && 5*i+3*j+k/3 == 100){
                    cout << "公鸡" << i << " 母鸡" << j << " 小鸡" << k << endl;
                }
            }
        }
    }
    return 0;
}

3、clock()函数的使用

为了体验优化的效果,将百钱百鸡改为万钱万鸡,同时在程序中加入clock()函数,以此检查程序运行的速度是否变快。


#include <bits/stdc++.h>
using namespace std;
int main(){
    for(int i=0; i<=10000/5; i++){
        for(int j=0; j<=10000/3; j++){
            for(int k=0; k<=10000; k+=3){
                // 只数等量       钱数等量
                if(i+j+k==10000 && 5*i+3*j+k/3 == 10000){
                    cout << "公鸡" << i << " 母鸡" << j << " 小鸡" << k << endl;
                }
            }
        }
    }
    cout << "耗时: " << ((double)clock() / CLOCKS_PER_SEC) << " 秒";
    return 0;
}

clock() 是 C/C++ <ctime>(或 <time.h>)里的一个函数,用来测量进程从启动到当前时刻所用的 CPU 时钟数。把它换算成“秒”就可以得到运行时间。

(1)原型与返回值


#include <ctime>          // 或 <time.h>
clock_t  clock(void);

  • 返回类型:clock_t(通常是 long)。
  • 单位:CPU 时钟“滴答”(ticks)。
  • 宏 CLOCKS_PER_SEC 表示 1 秒包含多少 ticks(Linux/Windows 下通常是 1000000)。

(2)在代码里的三步用法

a)开始计时
代码并没有显式保存开始时刻,因为三重循环一开始就执行,所以把clock() 放在循环结束后即可测得总耗时。
b)  结束计时

double elapsed = (double)clock() / CLOCKS_PER_SEC;

先把 clock_t 强制转换成 double,再除以 CLOCKS_PER_SEC,得到 秒数。因为除法需要小数精度,而 clock_t 本质是整型(long),如果不转 double,clock() / CLOCKS_PER_SEC 会先做整数除法,结果把小数部分直接截断为 0,导致永远得到 0 秒。
强制转换成 double 后,整个表达式变为 浮点除法,才能保留毫秒级的小数部分。

(3)输出

cout << "耗时: " << elapsed << " 秒";

(4)常见注意点:

  • 单位混淆 :忘记除以 CLOCKS_PER_SEC,直接输出会是一串大数字。 
  • 精度:Windows 下 clock() 精度约 1 ms;Linux 下可用 clock_gettime 更高精度。 
  • 多线程:clock() 统计的是进程CPU 时间,不包括阻塞 I/O;多线程场景可用 std::chrono。 

(5)一句话总结

clock() 就是“秒表”:
开始 → 结束 → (结束-开始)/CLOCKS_PER_SEC 得到秒数。

4、优化枚举次数对比

对于万钱买万鸡的情况,上面两种枚举分别花的时间如下:

无脑暴力:

枚举次数优化后

我们发现:数量到达一定级别后,优化枚举次数效果非常明显,执行时间由原来的508多秒到优化后的13秒多。但是执行时间还是不可接受!!!

5、枚举层数优化

我们i,j枚举时,先确定公鸡和母鸡的个数,然后剩下的钱全部用来买小鸡,小鸡数k=10000-i-j,我们只需要判断小鸡数是3的倍数,且花掉的钱数正好是10000钱即可!

#include <bits/stdc++.h>
using namespace std;
int main(){
    for(int i=0; i<=10000/5; i++){
        for(int j=0; j<=10000/3; j++){
            int k=10000-i-j;// 只数等量
            if(k%3) continue;
            // 钱数等量
            if(5*i+3*j+k/3 == 10000){
                cout << "公鸡" << i << " 母鸡" << j << " 小鸡" << k << endl;
            }
        }
    }
    cout << "耗时: " << ((double)clock() / CLOCKS_PER_SEC) << " 秒";
    return 0;
}

我们发现运行时间差距巨大,优化循环层数的执行时间居然差不多是150毫秒,根本不是一个数量级!

所以最后结论就是循环层数越少越好,专业一点说法就是:嵌套循环层数越少越好,当暴力枚举消耗时间太多,首先要想到优化循环层数,其次想办法优化循环次数。

6、代码(每只公鸡 x 元,每只母鸡 y 元,每 z 只小鸡 1 元;现在有 n 元,买了 m 只鸡)

#include <iostream>
using namespace std;

int main() {
    int x, y, z, n, m, cnt = 0;
    cin >> x >> y >> z >> n >> m;
    
    for (int i = 0; i <= n / x && i <= m; i++) {
        for (int j = 0; j <= (n - i * x) / y && j <= m - i; j++) {
            int k = m - i - j;
            if (k >= 0 && k % z == 0) { //可以if(k%z||k<0) continue;
                if (i * x + j * y + k / z == n) { //这两个if条件可以合并在一起
                    cnt++;
                }
            }
        }
    }
    
    cout << cnt;
    return 0;
}

B3836 [GESP202303 二级] 百鸡问题 - 洛谷

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

智码行者

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值