洛谷 B4358 [GESP202506 三级] 奇偶校验

点点关注 是我创作的最大动力

恩师:hnjzsyjyj

一、题目介绍:初识奇偶校验问题

大家好呀!今天咱们要一起学习洛谷上的一道经典编程题 ——B4358 [GESP202506 三级] 奇偶校验。这道题虽然难度不大,但却是理解 “二进制运算” 和 “循环统计” 的绝佳案例,非常适合刚接触编程不久的同学巩固基础知识。咱们从题目本身出发,一步步揭开它的面纱~

1.1 题目来源与定位

洛谷 B4358 来自 2025 年 6 月的 GESP 三级考试真题,难度评级为 “入门” 到 “普及” 之间。它主要考察对二进制数的理解、循环结构的灵活运用以及简单的统计逻辑,是将数学概念与编程实践结合的典型题目。如果你已经掌握了for循环、while循环和基本的算术运算,那么解决这道题会非常轻松~

1.2 题目描述

咱们先来搞清楚题目要我们做什么。题目描述可以简化为:

输入一个整数n,表示接下来有n个整数;然后输入这n个整数,我们需要完成以下操作:

  1. 统计这n个整数的二进制表示中,所有 “1” 的总个数(记为ans);
  2. 计算ans除以 2 的余数(即ans % 2);
  3. 最后输出ansans % 2这两个结果,中间用空格隔开。

举几个例子帮助理解:

  • 示例 1:输入2 3 5。3 的二进制是11(含 2 个 1),5 的二进制是101(含 2 个 1),总共有2+2=4个 1,4%2=0,所以输出4 0
  • 示例 2:输入3 1 2 4。1 的二进制是1(1 个 1),2 是10(1 个 1),4 是100(1 个 1),总共有1+1+1=3个 1,3%2=1,所以输出3 1
  • 示例 3:输入1 0。0 的二进制是0(0 个 1),所以输出0 0

通过这些例子可以看出,问题的核心就是将每个整数转换为二进制后统计 “1” 的总数,再计算总数的奇偶性

1.3 题目难度与适合人群

这道题属于 “入门到普及过渡” 的难度,适合已经掌握 C++ 基本循环结构和算术运算的同学练习。如果你理解二进制的概念,会用while循环处理重复操作,并且知道取余运算(%)的用法,那么这道题对你来说会很轻松。即使你对二进制不太熟悉,跟着我一步步分析,也能很快掌握解题方法~

二、解题思路:三步搞定奇偶校验问题

面对编程题,直接写代码容易出错,咱们先理清思路。解决 “奇偶校验” 问题可以分三步:读取输入数据→统计每个数的二进制中 1 的个数→计算结果并输出。咱们详细分析每一步的逻辑~

2.1 第一步:读取输入数据

题目要求先输入n,再输入n个整数。这意味着:

  • 首先用cin读取n的值;
  • 然后用一个for循环,循环n次,每次读取一个整数,存储到数组中(方便后续处理)。

数组就像一个 “储物柜”,把这n个整数按顺序存起来,需要的时候再一个个取出来处理。这里我们可以定义一个大小合适的数组(比如a[105]),足够存储题目中的输入数据。

2.2 第二步:统计每个数的二进制中 1 的个数

这是题目最核心的步骤。对于每个整数a[i],我们需要统计它的二进制表示中 “1” 的数量。怎么统计呢?

二进制数的特点是 “逢二进一”,每个数位只有 0 或 1。我们可以用 “除以 2 取余” 的方法逐步拆解二进制数:

  • 每次将整数除以 2,得到的余数要么是 0,要么是 1(这个余数就是当前二进制的最后一位);
  • 如果余数是 1,就把计数器ans加 1;
  • 然后将整数更新为 “除以 2 的商”,重复上述过程,直到整数变成 0 为止。

举个例子:统计5的二进制中 1 的个数:

  • 5 ÷ 2 = 2,余数 1(是 1,ans+1,此时 ans=1);
  • 2 ÷ 2 = 1,余数 0(不是 1,ans 不变);
  • 1 ÷ 2 = 0,余数 1(是 1,ans+1,此时 ans=2);
  • 整数变成 0,停止循环,所以 5 的二进制中有 2 个 1。

这种 “循环取余” 的方法,能精准地提取二进制数中的每一位,从而统计 1 的个数。

2.3 第三步:计算结果并输出

当所有数的 1 的个数都统计完毕后,我们需要:

  • 计算ans(总个数);
  • 计算ans % 2(总个数的奇偶性,0 表示偶数,1 表示奇数);
  • cout输出这两个结果,中间用空格隔开。

总结一下完整的解题流程:

  1. 输入nn个整数,存入数组a
  2. 初始化计数器ans = 0
  3. 对数组中的每个数a[i]
    • while循环拆解二进制:
      • 计算a[i] % 2,如果结果是 1,ans加 1;
      • a[i]更新为a[i] / 2
      • 重复直到a[i]变成 0;
  4. 计算ans % 2,输出ans和这个结果。

三、代码解析:逐行读懂奇偶校验的实现

根据上面的解题思路,用户提供的代码完美实现了所有功能。咱们逐行分析这段代码:

cpp

#include<bits/stdc++.h>
using namespace std;
int n,a[105],ans; 
int main() {
	cin>>n;
	for(int i=1;i<=n;i++){
		cin>>a[i];
		while(a[i]>0){
			if(a[i]%2==1)ans++;
			a[i]/=2;
		}
	}
	cout<<ans<<" "<<ans%2;
	return 0;
}

这段代码虽然简短,但逻辑清晰,咱们一步步拆解:

3.1 头文件与命名空间:代码的 “基础配置”

cpp

#include<bits/stdc++.h>
using namespace std;

这两行是 C++ 代码的标准开头,作用是:

  • #include<bits/stdc++.h>:引入 C++ 的万能头文件,包含了所有常用的标准库(如输入输出、数组处理等),省去了逐个引入头文件的麻烦;
  • using namespace std;:声明使用标准命名空间,这样我们可以直接使用cincout等标准库函数,不用每次都写std::cinstd::cout

对于入门级题目,这两行代码几乎是标配,大家可以直接记住~

3.2 变量定义:存储数据的 “容器”

cpp

int n,a[105],ans; 

这行代码定义了三个变量,分别是:

  • int n:用来存储输入的整数个数;
  • int a[105]:定义了一个大小为 105 的整数数组,用来存储输入的n个整数(105 的大小足够应对题目中的数据范围);
  • int ans:用来作为计数器,统计所有二进制中 1 的总个数,初始值默认为 0(全局变量未初始化时默认值为 0)。

这里将变量定义在main函数外面(全局变量),所以ans初始值为 0。如果定义在main函数内部,需要手动初始化ans = 0,否则可能会有随机值,导致结果错误~

3.3 主函数:程序的 “核心逻辑”

main函数是程序的入口,所有操作都在这里执行。咱们分步骤解析:

3.3.1 读取整数个数 n

cpp

cin>>n;

这行代码从输入中读取n的值,n就是接下来要处理的整数的数量。比如输入3,表示后面有 3 个整数需要处理。

3.3.2 循环读取 n 个整数并处理

cpp

for(int i=1;i<=n;i++){
    cin>>a[i];
    while(a[i]>0){
        if(a[i]%2==1)ans++;
        a[i]/=2;
    }
}

这部分是整个程序的核心,咱们分两层分析:

外层 for 循环:读取并处理每个整数

  • 循环条件:i从 1 到n(因为数组下标从 1 开始更符合咱们的计数习惯);
  • 每次循环先执行cin>>a[i]:读取一个整数,存储到数组的第i个位置;
  • 然后执行内层的while循环,处理这个整数,统计它的二进制中 1 的个数。

内层 while 循环:统计二进制中 1 的个数

  • 循环条件:a[i]>0(只要当前整数大于 0,就继续拆解);
  • if(a[i]%2==1)ans++:计算当前整数除以 2 的余数,如果余数是 1(说明二进制最后一位是 1),就把计数器ans加 1;
  • a[i]/=2:将当前整数除以 2(相当于去掉二进制的最后一位),继续下一次循环。

举个例子,当a[i] = 5时(二进制是 101):

  • 第一次循环:5%2=1(余数是 1),ans加 1(ans=1);5/2=2(此时 a [i] 变为 2);
  • 第二次循环:2%2=0(余数是 0),ans不变;2/2=1(a [i] 变为 1);
  • 第三次循环:1%2=1(余数是 1),ans加 1(ans=2);1/2=0(a [i] 变为 0);
  • 此时a[i] = 0while循环结束,5 的二进制中共有 2 个 1,统计完成。

这个过程就像 “剥洋葱”,一层层剥开二进制数的每一位,遇到 1 就计数,非常直观~

3.3.3 输出结果

cpp

cout<<ans<<" "<<ans%2;

当所有整数都处理完毕后,ans中存储的是所有二进制中 1 的总个数。这行代码输出两个结果:

  • 第一个是ans(总个数);
  • 第二个是ans%2(总个数除以 2 的余数,即奇偶性);
  • 两个结果之间用空格隔开,符合题目要求的输出格式。
3.3.4 程序结束

cpp

return 0;

表示main函数执行完毕,程序正常退出。

四、代码执行过程演示:用实例理解逻辑

为了让大家更清楚代码的执行过程,咱们以示例 1(输入2 3 5)为例,一步步演示程序的运行:

  1. 初始状态n未赋值,a数组为空,ans=0
  2. 读取 ncin>>n后,n=2
  3. 外层循环 i=1
    • 读取a[1]cin>>a[1]后,a[1]=3
    • 内层while(a[1]>0)循环(处理 3):
      • 第一次循环:3%2=1ans=13/2=1a[1]=1
      • 第二次循环:1%2=1ans=21/2=0a[1]=0
      • 循环结束(a[1]=0);
  4. 外层循环 i=2
    • 读取a[2]cin>>a[2]后,a[2]=5
    • 内层while(a[2]>0)循环(处理 5):
      • 第一次循环:5%2=1ans=35/2=2a[2]=2
      • 第二次循环:2%2=0ans不变;2/2=1a[2]=1
      • 第三次循环:1%2=1ans=41/2=0a[2]=0
      • 循环结束(a[2]=0);
  5. 所有循环结束:输出ansans%2,即4 0,与示例结果一致。

通过这个实例可以看出,代码的执行过程完全按照咱们的解题思路进行,每一步都清晰可控~

五、易错点分析:这些坑千万别踩!

这道题虽然逻辑不复杂,但初学者很容易在细节上出错。咱们盘点几个常见的易错点,帮助大家避坑~

5.1 易错点 1:数组越界或大小不足

问题描述:

如果数组定义得太小(比如a[10]),而输入的n大于数组大小(比如n=15),就会导致数组越界,程序可能崩溃或输出错误结果。

错误示例:

cpp

int a[10]; // 错误:数组大小太小
cin>>n; // 假设n=15
for(int i=1;i<=n;i++){
    cin>>a[i]; // 当i=10时,a[10]超出数组范围
}
解决办法:

定义数组时预留足够的空间。题目中n的范围通常不会太大(GESP 三级题一般n≤100),定义a[105]a[1005]完全足够,避免数组越界。

5.2 易错点 2:计数器 ans 未初始化

问题描述:

如果ans定义在main函数内部且没有初始化,初始值会是随机数,导致统计结果错误。

错误示例:

cpp

int main() {
    int n,a[105],ans; // 错误:ans未初始化,初始值随机
    cin>>n;
    // ...循环代码...
}

假设ans的随机初始值是 5,当输入n=1 0时,正确结果是0 0,但程序会输出5 1

解决办法:
  • 要么将ans定义为全局变量(如用户提供的代码),全局变量默认初始值为 0;
  • 要么在main函数内定义时手动初始化:int ans = 0;

5.3 易错点 3:处理 0 的情况

问题描述:

0 的二进制表示是0,其中没有 1,但有些同学可能会误以为 0 有 1 个 1,或者在while循环中漏处理 0 的情况。

错误示例:

cpp

while(a[i]>=0){ // 错误:循环条件写成>=0,0会进入循环
    if(a[i]%2==1)ans++;
    a[i]/=2;
}

a[i]=0时,0%2=0ans不变,但0/2=0,会导致while循环无限执行(死循环)。

解决办法:

while循环的条件必须是a[i]>0,确保 0 不会进入循环(因为 0 的二进制中没有 1,无需统计)。当a[i]=0时,while循环不执行,ans保持不变,符合预期。

5.4 易错点 4:整数除以 2 的方式错误

问题描述:

在更新a[i]时,误写成a[i] = a[i] / 2但忘记整数除法的特性,或者写成a[i] %= 2(取余而不是除法)。

错误示例:

cpp

a[i]%=2; // 错误:这是取余操作,不是除法

比如a[i]=3时,3%2=1,之后a[i]一直是 1,导致循环无限执行。

解决办法:

正确使用除法更新a[i]a[i] /= 2(等价于a[i] = a[i] / 2),确保每次循环都能去掉二进制的最后一位,逐步将a[i]减小到 0。

5.5 易错点 5:输出格式错误

问题描述:

题目要求输出两个结果,中间用空格隔开,但有的同学可能会忘记加空格,或者多输出一个空格,导致格式错误。

错误示例 1(没加空格):

cpp

运行

cout<<ans<<ans%2; // 输出结果连在一起,比如“40”
错误示例 2(多一个空格):

cpp

运行

cout<<ans<<" "<<ans%2<<" "; // 最后多一个空格,比如“4 0 ”
解决办法:

严格按照题目要求输出,两个结果之间用一个空格隔开,不要多也不要少。正确写法是cout<<ans<<" "<<ans%2;,简洁规范。

5.6 易错点 6:循环变量范围错误

问题描述:

外层for循环的范围错误,比如写成i=0开始或i<n结束,导致漏处理或多处理数据。

错误示例:

cpp

运行

for(int i=0;i<n;i++){ // 错误:i从0开始,但输入到a[0]
    cin>>a[i];
    // ...处理代码...
}

如果n=2,会读取a[0]a[1],虽然也能处理,但与代码中数组下标从 1 开始的习惯不符,容易混淆。更严重的是如果循环条件写成i<=n,会多循环一次,读取不存在的数据。

解决办法:

外层循环严格控制范围i从 1 开始,到n结束(i<=n),确保正好读取n个整数,与题目要求一致。

六、优化方向:让代码更高效简洁

用户提供的代码已经能正确解题,但我们还可以从几个角度优化,让代码更高效、更易读~

6.1 优化 1:不用数组,直接处理数据

题目中并不需要保存所有整数,我们可以边读取边处理,省去数组的使用,节省内存空间。

优化代码:

cpp

运行

#include<bits/stdc++.h>
using namespace std;
int n,x,ans; 
int main() {
    cin>>n;
    for(int i=1;i<=n;i++){
        cin>>x; // 直接用变量x存储当前整数,不存数组
        while(x>0){
            if(x%2==1)ans++;
            x/=2;
        }
    }
    cout<<ans<<" "<<ans%2;
    return 0;
}
优点:
  • 省去数组的定义和存储,代码更简洁;
  • 减少内存占用,尤其在n较大时更明显。

6.2 优化 2:使用位运算统计 1 的个数

在计算机中,二进制数的操作可以用位运算更高效地实现。x & 1可以直接获取x二进制的最后一位(等价于x%2),x >>= 1可以实现x = x / 2的效果,且位运算速度更快。

优化代码:

cpp

运行

#include<bits/stdc++.h>
using namespace std;
int n,x,ans; 
int main() {
    cin>>n;
    for(int i=1;i<=n;i++){
        cin>>x;
        while(x>0){
            if(x & 1)ans++; // x&1等价于x%2==1
            x >>= 1; // x>>=1等价于x /= 2
        }
    }
    cout<<ans<<" "<<ans%2;
    return 0;
}
优点:
  • 位运算的执行效率比算术运算更高,尤其在处理大量数据时更明显;
  • 代码更贴近计算机底层逻辑,适合理解二进制操作。

6.3 优化 3:提前处理负数(拓展)

题目中输入的是整数,但如果输入负数(虽然题目可能不包含负数,但作为拓展),负数的二进制表示有符号位,需要特殊处理。在 C++ 中,负数采用补码表示,符号位为 1,会导致统计错误。

处理负数的优化代码:

cpp

运行

#include<bits/stdc++.h>
using namespace std;
int n,x,ans; 
int main() {
    cin>>n;
    for(int i=1;i<=n;i++){
        cin>>x;
        unsigned int num = x; // 用无符号整数处理负数,忽略符号位
        while(num>0){
            if(num & 1)ans++;
            num >>= 1;
        }
    }
    cout<<ans<<" "<<ans%2;
    return 0;
}
优点:
  • unsigned int存储数据,将负数视为无符号数处理,避免符号位的干扰;
  • 拓展了代码的适用范围,能正确处理负数的二进制统计。

七、拓展练习:巩固二进制统计能力

学会了奇偶校验的解法,咱们可以试试类似的题目,巩固二进制统计和循环处理的知识点~

7.1 拓展练习 1:统计每个数的二进制 1 的个数

题目:

输入nn个整数,对每个整数,输出它的二进制表示中 1 的个数,每个结果占一行。

思路:

对每个数单独统计 1 的个数,统计完后立即输出,不需要累计总和。

参考代码:

cpp

运行

#include<bits/stdc++.h>
using namespace std;
int main() {
    int n,x;
    cin>>n;
    while(n--){
        cin>>x;
        int cnt=0;
        while(x>0){
            if(x%2==1)cnt++;
            x/=2;
        }
        cout<<cnt<<endl;
    }
    return 0;
}

7.2 拓展练习 2:计算二进制中 1 的个数的最大值

题目:

输入nn个整数,找出其中二进制表示中 1 的个数最多的数,输出这个最大值。如果有多个数的 1 的个数相同且最多,输出其中最大的数。

思路:
  • 对每个数统计 1 的个数,记录最大个数和对应的数;
  • 如果遇到个数更多的数,更新最大值和对应数;
  • 如果个数相同,比较数的大小,保留较大的数。
参考代码:

cpp

运行

#include<bits/stdc++.h>
using namespace std;
int main() {
    int n,x;
    cin>>n;
    int max_cnt=0, result=0;
    while(n--){
        cin>>x;
        int cnt=0, temp=x;
        while(temp>0){
            if(temp%2==1)cnt++;
            temp/=2;
        }
        if(cnt>max_cnt || (cnt==max_cnt && x>result)){
            max_cnt=cnt;
            result=x;
        }
    }
    cout<<result;
    return 0;
}

7.3 拓展练习 3:二进制中 1 的位置

题目:

输入一个整数x,输出它的二进制表示中所有 1 所在的位置(从右往左数,最右边为第 1 位)。如果x=0,输出-1

思路:
  • 用循环拆解二进制,记录每个 1 出现的位置(循环次数 + 1 就是位置);
  • 存储位置信息,最后按顺序输出。
参考代码:

cpp

运行

#include<bits/stdc++.h>
using namespace std;
int main() {
    int x;
    cin>>x;
    if(x==0){
        cout<<-1;
        return 0;
    }
    vector<int> pos; // 存储1的位置
    int idx=1; // 位置从1开始
    while(x>0){
        if(x%2==1)pos.push_back(idx);
        x/=2;
        idx++;
    }
    for(int i=0;i<pos.size();i++){
        cout<<pos[i]<<" ";
    }
    return 0;
}

八、总结:奇偶校验背后的编程思维

通过学习洛谷 B4358 奇偶校验,咱们不仅学会了这道题的解法,更掌握了几个重要的编程思维和知识点:

8.1 二进制的理解与操作

二进制是计算机的基础,“除以 2 取余” 是将十进制转换为二进制的经典方法。通过这道题,咱们学会了如何用编程实现二进制的拆解和统计,理解了二进制数的存储逻辑。

8.2 循环的嵌套使用

外层for循环控制数据的输入和整体流程,内层while循环处理单个数据的二进制拆解,这种 “外层控制数量,内层处理细节” 的嵌套循环结构,是处理批量数据的常用技巧。

8.3 计数器的使用

计数器ans从 0 开始,通过条件判断累计符合要求的数量,最终得到结果。这种 “初始化→循环累计→输出结果” 的计数模式,在统计类问题中非常常见。

8.4 细节处理的重要性

数组大小的选择、计数器的初始化、循环条件的设置、输出格式的规范,这些细节直接影响程序的正确性。编程时要养成 “关注细节、多测试边缘情况” 的习惯(如输入 0、输入较大的数)。

九、PPT 展示建议

如果用这篇内容制作 PPT,建议按以下结构分页,突出重点,方便讲解:

  1. 封面页:标题 “洛谷 B4358 奇偶校验”+ 二进制相关背景图(如 0 和 1 组成的图案),突出题目名称;
  2. 题目介绍页:用简洁语言描述题目要求,搭配 1-2 个示例(输入输出对比),用表格展示更清晰;
  3. 解题思路页:用流程图展示 “输入数据→二进制拆解→统计 1 的个数→计算结果” 的过程,标注关键步骤;
  4. 二进制原理页:简单介绍二进制的基本概念,用 “除以 2 取余” 的例子(如 5→101)帮助理解;
  5. 代码框架页:展示完整代码,用不同颜色标出核心部分(外层循环、内层循环、计数器);
  6. 代码解析页:分 2-3 页,分别讲解外层for循环、内层while循环的作用,结合实例(如 n=2,3 和 5)演示执行过程;
  7. 易错点页:用 “错误代码 + 正确代码” 的对比表格,列出常见错误及解决办法,重点标注数组越界、计数器初始化等问题;
  8. 优化方向页:展示优化后的代码(如位运算版本),对比讲解优化思路和优点;
  9. 拓展练习页:列出 1-2 道拓展题,简要说明思路,鼓励练习巩固;
  10. 总结页:用 bullet point 提炼核心知识点(二进制操作、循环嵌套、计数器使用),强化记忆。

这样的结构逻辑清晰,重点突出,能让初学者快速理解题目解法和背后的编程思想,非常适合课堂讲解或自我复习~

十、写在最后:从奇偶校验到更多二进制问题

奇偶校验虽然是一道简单的题目,但它涉及的二进制操作和循环统计逻辑,是很多复杂问题的基础。比如计算机网络中的数据校验、数字电路中的逻辑设计、密码学中的加密算法,都离不开二进制的处理。

学习编程时,不要满足于 “写出能运行的代码”,更要理解代码背后的原理:为什么用while(x>0)循环?为什么x%2能得到二进制的最后一位?这些原理的理解,能帮助你解决更复杂的问题。

多动手敲代码,多测试不同的输入(比如 0、负数、较大的数),多思考 “有没有更优的解法”,你的编程能力会在不知不觉中提升。下次遇到涉及二进制的问题,相信你一定能举一反三,轻松解决!加油呀!

Logo

智能硬件社区聚焦AI智能硬件技术生态,汇聚嵌入式AI、物联网硬件开发者,打造交流分享平台,同步全国赛事资讯、开展 OPC 核心人才招募,助力技术落地与开发者成长。

更多推荐