(2017)第八届蓝桥杯大赛个人赛省赛(软件类) C/C++ 大学A组 题解(第八题包子凑数)

第八题
题目
标题:包子凑数
小明几乎每天早晨都会在一家包子铺吃早餐。他发现这家包子铺有N种蒸笼,其中第i种蒸笼恰好能放Ai个包子。每种蒸笼都有非常多笼,可以认为是无限笼。
每当有顾客想买X个包子,卖包子的大叔就会迅速选出若干笼包子来,使得这若干笼中恰好一共有X个包子。比如一共有3种蒸笼,分别能放3、4和5个包子。当顾客想买11个包子时,大叔就会选2笼3个的再加1笼5个的(也可能选出1笼3个的再加2笼4个的)。
当然有时包子大叔无论如何也凑不出顾客想买的数量。比如一共有3种蒸笼,分别能放4、5和6个包子。而顾客想买7个包子时,大叔就凑不出来了。
小明想知道一共有多少种数目是包子大叔凑不出来的。
输入

第一行包含一个整数N。(1 <= N <= 100)
以下N行每行包含一个整数Ai。(1 <= Ai <= 100)
输出

一个整数代表答案。如果凑不出的数目有无限多个,输出INF。
例如,
输入:
2
4
5
程序应该输出:
6
再例如,
输入:
2
4
6
程序应该输出:
INF
样例解释:
对于样例1,凑不出的数目包括:1, 2, 3, 6, 7, 11。
对于样例2,所有奇数都凑不出来,所以有无限多个。
资源约定:
峰值内存消耗(含虚拟机) < 256M
CPU消耗 < 1000ms
请严格按要求输出,不要画蛇添足地打印类似:“请您输入…” 的多余内容。
注意:
main函数需要返回0;
只使用ANSI C/ANSI C++ 标准;
不要调用依赖于编译环境或操作系统的特殊函数。
所有依赖的函数必须明确地在源文件中 #include
不能通过工程设置而省略常用头文件。
提交程序时,注意选择所期望的语言类型和编译器类型。

第一种做法
分析
这道题如果要直接分析计算有多少个数无法被凑出来,似乎非常难。

那么能不能求出哪些数是无法被凑出来的呢?也很难。

但是哪些数能被凑出来是可以通过给定的数去用程序硬凑的。

这样,我们就可以去判断一个数能不能被凑出来了。

那我们就先实现一个吧。

那么这个数可以被凑出来要符合哪些条件呢?

1.这个数是给定的n个数之一。

2.这个数减去给定的n个数中的其中一个之后,依然可以被凑出来。

第1条显然成立,第2条我们稍作思考也会发现是对的,而且这两条包含了所有的情况。

当然,这种做法无法解决凑不出来的数是否无限。

代码及运行结果

#include 
using namespace std;
const int MAX_N = 1e2 + 5;
int n, a[MAX_N];
bool judge(int x) {
    if (x <= 0) return false;
    for (int i = 0; i < n; i++) {
        if (x == a[i]) return true;
        if (judge(x - a[i])) return true;
    }
    return false;
}
int main() {
    int ans = 0;
    scanf("%d", &n);
    for (int i = 0; i < n; i++) scanf("%d", &a[i]);
    for (int i = 1; i <= 1000; i++) {
        if (!judge(i)) ans++;
    }
    printf("%d\n", ans);
    return 0;
}

(2017)第八届蓝桥杯大赛个人赛省赛(软件类) C/C++ 大学A组 题解(第八题)_第1张图片

观察代码我们可以发现,这段判断函数其实就是一个深搜。

在不考虑无限多个数无法凑出来的情况下,这种做法看上去并没有什么问题。

但是我们仔细分析一下时间复杂度,对于每一个数,都有n种可能进入另外一个数,那么在数很大的情况下,计算每一个数的时间复杂度就会呈指数级增长。

(2017)第八届蓝桥杯大赛个人赛省赛(软件类) C/C++ 大学A组 题解(第八题)_第2张图片

还有一个问题就是,在这段代码中,我们默认只要判断到1000就足矣。但事实果真如此吗?

现在我们来着手解决这些问题。

第二种解法
分析
先考虑时间复杂度的问题。

我们会发现在从1计算到1000的过程中,每一个数都只考虑比它小的数的情况,不需要考虑比它大的数的情况。那么,我们在之前既然已经解决了这些比它小的数的情况,何不把它们记下来呢?这样对于每一个数,我们最多就只需要执行n次判断了。

那么这个时候我们再来考虑判断的范围。题目限制1s,也就是1e8次运算。那么每个数最多执行n次的情况下,保险起见我们也可以算到5e5个数。虽然我们无法确定这个范围是否足够,但是比1000就要保险得多。

代码及运行结果

#include 
using namespace std;
const int MAX_N = 1e2 + 5, MAX_M = 5e5 + 5;
int n, a[MAX_N];
bool able[MAX_M];
// 下面这段代码通常被称为记忆化搜索
bool judge(int x) {
    if (x <= 0) return false;
    if (able[x]) return true;
    for (int i = 0; i < n; i++) {
        if (x == a[i]) return able[x] = true;
        if (able[x - a[i]]) return able[x] = true;
    }
    return false;
}
int main() {
    int ans = 0;
    scanf("%d", &n);
    for (int i = 0; i < n; i++) scanf("%d", &a[i]);
    for (int i = 1; i <= 5e5; i++) {
        if (!judge(i)) ans++;
    }
    printf("%d\n", ans);
    return 0;
}

之所以被称为记忆化搜索,是因为它在搜索的过程中记录了所有的结果。

这样做的好处是,如果每次搜索都是基于前面搜索的结果得出的话,效率就会被大大提高。

(2017)第八届蓝桥杯大赛个人赛省赛(软件类) C/C++ 大学A组 题解(第八题)_第3张图片

相较而言,这种做法的答案更大(从而更准确),速度也更快。

但是,我们依旧没有从根本解决之前所述的几个问题。当然,如果时间不允许你深入思考,做到这里也是可以的。

第三种做法
分析
现在,我们来尝试进一步优化程序。

首先考虑INF的情况。

样例提出的INF是由于给出的数都是偶数,所以无法凑出奇数。

那么我们很容易发现,其实只要给出的数都是某一个数的倍数,即它们的最大公约数为k>1,那么不是k的倍数的数就无法被凑出来。

但是如果它们的最大公约数为1呢?我们无法确定。

当然,我们现在就可以将我们的发现加入到程序当中,来提高我们的得分。

那么问题来了,怎么才能求出最大公约数呢?

如果你听过我讲的上一届的省赛题,你一定会记得最后一题的解法:模拟辗转相减。

那么我们用辗转相减,就可以完成求最大公约数了。不过,其实我们有效率更高的方法:辗转相除法。

实际上,辗转相除就是将辗转相减中的多个减法连在了一起。因为一个较大数减去一个较小数直到差小于较小数为止,这种操作就等同于模运算(取余)。

它的时间复杂度是多少呢?考虑到被除数=除数*商+余数,余数小于除数,也就是说最劣情况下余数也不会超过被除数的一半。所以辗转相除法的时间复杂度是O(logn)的,其中n为被除数,也就是求最大公约数里两个数中较大的那个。

代码及运行结果

#include 
using namespace std;
const int MAX_N = 1e2 + 5, MAX_M = 5e5 + 5;
int n, a[MAX_N];
bool able[MAX_M];
// 以下这段代码被称为辗转相除法(欧几里得算法)
int gcd(int x, int y) {
    if (y == 0) return x;
    else return gcd(y, x % y);
}
bool judge(int x) {
    if (x <= 0) return false;
    if (able[x]) return true;
    for (int i = 0; i < n; i++) {
        if (x == a[i]) return able[x] = true;
        if (able[x - a[i]]) return able[x] = true;
    }
    return false;
}
int main() {
    int ans = 0, GCD = 0;
    scanf("%d", &n);
    for (int i = 0; i < n; i++) {
        scanf("%d", &a[i]);
        GCD = gcd(a[i], GCD);
    }
    if (GCD > 1) {
        printf("INF\n");
    }
    else {
        for (int i = 1; i <= 5e5; i++) {
            if (!judge(i)) ans++;
        }
        printf("%d\n", ans);
    }


    return 0;
}

(2017)第八届蓝桥杯大赛个人赛省赛(软件类) C/C++ 大学A组 题解(第八题)_第4张图片

实际上,到目前为止,我们已经不用再往下研究了,因为这个程序足以让我们获得满分。当然,在比赛当中,如果你有充裕的时间,往下想也是理所应当的。

第八题的拓展
作为拓展内容,下面的内容会涉及到比较多的算法及数学知识。

第三种做法的正确性
首先我们来探究在给定的数最大公约数为1的情况下无法凑出的数是否是有限的。

根据上面的讲解:如果一个数可以被凑出来,那么它肯定是给定的数或者一个可以凑出来的数加上一个给定的数。

同理,如果一个数不可以被凑出来,那么它减去一个给定的数也是不能被凑出来的。

那么我们就会发现,不可以被凑出来的数在最稀疏的情况下也应该隔x出现一次(x为一个给定的数)。

我们来考虑这个性质的本质:对于x来说,所有的数其实都可以分成x类:根据对x的余数来分。

只要某类里的一个数被凑出来,那么这类数中无法被凑出来的数就一定是有限的。

这种划分出来的类被称为x的完全剩余系。

那么我们只需要考虑模x=0,1,2,3……x-1的数能不能凑出来就可以了。

很明显,如果模x=1可以被凑出来,其它就一定能被凑出来,所以我们只需要考虑这一种情况。

那么也就是说,我们考虑的是这些数凑出一个数减去若干个x能否等于1。

现在我们来学习一个新知识:裴蜀定理

以上摘自维基百科

如果你擅长离散数学,那么你会发现裴蜀定理在整环上不证自明:在主理想环中,a和b的最大公约元被定义为理想aA + bA的生成元。

现在我们假设x以外的n-1个数为 y 1 y_1 y1 y 2 y_2 y2…… y n − 1 y_{n-1} yn1。那么对于这n个数,裴蜀定理能否成立呢?

由于最大公约数这种运算本身具有结合律,所以我们将任意两个数合并,替换成它们的最大公约数,这样进行n-2次操作,就会转化为裴蜀定理的形式了。

也就是说,如果这n个数互素,那么它们凑不出来的数一定是有限个。

下一个问题,如何确定我们判断的范围呢?

考虑我们凑数的过程,实际上就是凑x的完全剩余系。

我们假定x的完全剩余系中,每个类的数被凑出来之后就不再凑这一类。

那么最大的无法凑出来的数加上x就是最后一个被凑出来的数。

所以我们考虑这里最后一个被凑出来的数,假设它是由m个给定的数相加而成。

按照顺序,我们记为 a 1 a_1 a1 a 2 a_2 a2 a 3 a_3 a3…… a m a_m am,并记 ∑ i = 1 k a i \sum_{i=1}^{k}a_i i=1kai S k S_k Sk

我们可以发现一个性质:如果 S i S_i Si S j S_j Sj模x同余,那么 a i + 1 + a i + 2 + … … a j a_{i+1}+a_{i+2}+……a_j ai+1+ai+2+aj就是不必要的,它们的和模x=0。

根据这个性质,我们可以得到一个结论:在最优情况下,m必定小于等于x。

这里用到了组合数学里的一个著名定理:鸽巢原理

在这里插入图片描述

以上摘自维基百科

我们可以发现,由于要满足每一个 S k S_k Sk都不相同,它们的数量必然不会超过x的完全剩余系的大小:x。

这样我们就可以确定无法凑出的数的上限了:x*剩余数里的最大数。

带回题中,我们考虑x不超过100,而剩余的数最大也不超过100,我们只要判断到10000即可。

动态规划
分析
我们前面提到的记忆化搜索,实际上是一种动态规划的实现形式。而动态规划,是对一类算法的总称。

在这里插入图片描述

以上摘自维基百科

如果一个问题可以用动态规划解决,它需要具有无后效性:每一个问题的答案只由它的子问题答案构成,未来问题的答案不会对其产生影响。

例如当前这道题:每一个可以凑出来的数只由比它小的那些数决定,比它大的数无论能不能凑出来都不会对它本身产生影响。

动态规划除去记忆化搜索以外,还有一种解法:递推,这种方法写起来会更加简便。

而递推中需要有递推式,正如记忆化搜索中的递归式。对于这道题而言,我们将每一个给定的数都更新所有的数,那些可以被凑出来的数加上给定的这个数若干倍,其和都可以被凑出来。

代码及运行结果

#include 
using namespace std;
const int MAX_N = 1e2 + 5, MAX_M = 1e4 + 5;
int n, a[MAX_N];
bool able[MAX_M];
int gcd(int x, int y) {
    if (y == 0) return x;
    else return gcd(y, x % y);
}
int main() {
    int ans = 0, GCD = 0;
    able[0] = true;
    scanf("%d", &n);
    for (int i = 0; i < n; i++) {
        scanf("%d", &a[i]);
        GCD = gcd(a[i], GCD);
        // 以下这段代码被称为动态规划的递推式
        for (int j = 0; j + a[i] <= 1e4; j++) {
            able[j + a[i]] = (able[j + a[i]] || able[j]);
        }
    }
    if (GCD > 1) {
        printf("INF\n");
    }
    else {
        for (int i = 1; i <= 1e4; i++) {
            if (!able[i]) ans++;
        }
        printf("%d\n", ans);
    }
    return 0;
}

(2017)第八届蓝桥杯大赛个人赛省赛(软件类) C/C++ 大学A组 题解(第八题)_第6张图片

(2017)第八届蓝桥杯大赛个人赛省赛(软件类) C/C++ 大学A组 题解(第八题)_第7张图片

测试一下:

Γ ( n ) = ( n − 1 ) ! ∀ n ∈ N \Gamma(n) = (n-1)!\quad\forall n\in\mathbb N Γ(n)=(n1)!nN

x = − b ± b 2 − 4 a c 2 a x = \dfrac{-b \pm \sqrt{b^2 - 4ac}}{2a} x=2ab±b24ac

已标记关键词 清除标记
©️2020 CSDN 皮肤主题: 撸撸猫 设计师:设计师小姐姐 返回首页