鲜为人知的闰年判定大坑

【题目描述】

输入年份,判断是否为闰年。如果是,则输出yes,否则输出no。

提示:简单地判断除以4的余数是不够的。

【题目来源】

刘汝佳《算法竞赛入门经典  第2版》习题1-7 年份(year)

【解析】

一、闰年的由来及设定规则

首先要明白,设置闰年的目的是为了弥补历法规定的年度天数与地球实际公转周期(一个回归年)的时间差。因为规定平年365天,而实际的一个回归年大约比平年多出0.2422天。为了补偿这个差异,使历法年与回归年相适应,产生了闰年的概念。

在现行公历中,闰年的设定遵循以下规则:

①能被4整除但不能被100整除的年份为普通闰年。

②能被400整除的年份为世纪闰年。

为什么是这么个复杂规则呢?

前面说过,一个回归年大约比平年多出0.2422天,4年就多0.2422×4=0.9688天,而历法是每4年补1天,相当于每4年多补了1-0.9688=0.0312天。

短期内这点误差没什么影响,可时间长了误差会越来越大,每400百年就会多被3.12天。所以为了减小误差,在每4年一个闰年的基础上,每400年还要减少3个闰年。

减少哪3个闰年呢?历法制定者一拍脑门,就设在世纪之交吧,在第100、200、300年各减少1个。

这个拍脑门动作直接将世纪闰年变成稀有产品,比如2000年是世纪闰年,这意味着其后300年内出生的小朋友都与世纪闰年无缘了。

以上就是“四年一闰、百年不闰、四百年又闰”的规则的由来。

二、判定闰年的程序

需把闰年的判定规则转化为对应的表达式:

规则①中“能被4整除但不能被100整除”表示这两个条件需同时成立,这是“逻辑与”的关系,用&&表示,即:year % 4 == 0 && year % 100 != 0。

规则②的表达式为:year % 400 == 0

满足规则①与②任意一项就是闰年,所二者是“逻辑或”的关系,用||表示,即:①||②。

c代码:

#include <stdio.h>int main() {int year;scanf("%d", &year);if ((year % 4 == 0 && year % 100 != 0) || year % 400 == 0) {printf("yes\n");} else {printf("no\n");}return 0;}

上述代码中用于判断的表达式① year % 4 == 0 && year % 100 != 0的括号其实是没有必要的,因为&&的优先级是高于||的。但是,为了代码的清晰性和可读性,加上括号是一个比较好的习惯——尤其在涉及复杂逻辑运算时。

好了,这道题就讲完了。

我估计解这道题的多数同学都只会给出上面的代码,认为它就是正解。

可能连出题人也都这样认为。

因为算法课上老师都是这么讲的,所以我们都容易依惯性思维给出上面的答案。

然而,这道题真的解完了吗?

NO,NO,NO,这里面还有一个大坑。

认为上述代码是正解的都忽略了一个问题。

那就是:公元前的年份。

在一般的算法竞赛的题目中,都会给出年份的范围,比如1900 <= year <= 2500。但是本题,并没有给出范围,所以公元前的年份无疑也是符合题目要求的。

问题来了,公元前的闰年与公元后的闰年判定规则是一样的吗?

如果你在网上搜索一下,公元前闰年的判定规则,会发现有如下一些表述:

这些描述都有一些共同的特点:

①表现得神乎其神,却让人看得云里雾里;

②满满的漏洞;

③将简单的问题复杂化。

公元前闰年的判定本就是很简单的问题。

只需一句话:把公元前的年份减1,然后按公元后的规则判定即可。

比如,公元前2001年,减1等于2000年,按公元后的判定规则判定为闰年。

不知为啥,网上居然没见到一个以这种方式描述的。

为什么公元前的闰年规则和公元后不一样呢?这是个数学问题。

因为公历没有公元0年,公元1年的前一年就是公元前1年,而闰年的基本规则是每4年一个闰年,所以公元前1年就变成了闰年。

前6

前5

前4

前3

前2

前1

1

2

3

4

5

6

闰年

闰年

换句话说,如果有公元0年的话,那公元前和公元后的闰年规则就是一样的了。

正因为差了这一年,导致了公元前后闰年判定的差异。

现在你明白了刚刚说的公元前要减1再判断的原因了吧?

为简化起见,咱们在编程时假定用负数表示公元前的年份,这样的话就不再是减1后判断,而是要加1了。比如公元前1年,表示为“-1”,要加1才能变成0。

原来的代码改起来很简单,只要在判断之前加一个if语句即可:

if(year<0) year+=1;

以上就是本题的答案部分。

三、逻辑表达式的效率判定

下面再讨论一下由本题衍生出来的一个问题:逻辑表达式的效率问题。

从效率角度讲,下面的表达式A与B、C与D是一样的吗?

表达式A:year % 4 == 0 && year % 100 != 0

表达式B:year % 100 != 0 && year % 4 == 0

表达式C:(year % 4 == 0 && year % 100 != 0) || year % 400 == 0

表达式D:year % 400 == 0 ||(year % 4 == 0 && year % 100 != 0)

这涉及到&&、||这两个运算符的一个运算特点:短路(short-circuit)效应。

这个短路效应其实很好理解:

①如果a为假,则b无论真假,a&&b均为假,所以就不再计算b的值。

②如果a为真,则b无论真假,a||b均为真,所以就不再计算b的值。

表达式year % 4 == 0为假指年份不能被4整除,表达式year % 100 != 0为假指年份能被100整除,从输入年份的概率角度讲,显然前者远远大于后者,所以把前者放在&&之前,就能减小计算次数,故表达式A的效率高于表达式B。

表达式year % 4 == 0 && year % 100 != 0为真指年份能被4整除但不能被100整除,表达式year % 400 == 0为真指年份能被400整除,显然前者概率远远大于后者,把前者放在||之前,也能减小计算次数,故表达式C的效率高于表达式D。

也就是说,咱们代码中逻辑表达式的排序(即表达式C)就是效率最高的。

而效率最低的是下面这个表达式:

表达式E:year % 400 == 0 ||( year % 100 != 0 && year % 4 == 0)

从概率的角度说可能不易理解,如果把这道题改成依次输出公元前3000年到公元3000年每一年是否是闰年,大家就能立刻明白逻辑表达式不同的排序计算次数会有很大的不同了。

但是能不能输出具体的计算次数对比呢?

我家孩子提供了一个思路,逻辑判断可以用if-else语句替换,通过这种替换就能直接让程序输出不同排序的计算次数。

以下是孩子编的C++代码,我没更改,只是把变量名改得更清晰些(比如将bool变量名由s改为is_true,将循环变量i改为year),加了些注释。

表达式C的计算次数代码如下:

#include <iostream>using namespace std;int main () {bool is_true; //判断表达式真假//sum4是i%4==0的计算次数//sum100是i%100!=0的计算次数//sum400是i%400==0的计算次数int sum4=0,sum100=0,sum400=0,sum;for(int year=-3000;year<=3000;year++){if(year==0) year++; //跳过0年//公元前的年份+1int year1=year;if(year1<0) year1=year+1;//获得第1、2个表达式的计算次数sum4++;if(year1%4==0){sum100++;if(year1%100!=0)is_true=true;elseis_true=false;}else{is_true=false;}//获得第3个表达式的计算次数if(is_true==false) {sum400++;if(year1%400==0)is_true=true;elseis_true=false;}//输出年份是否为闰年if(is_true==true)cout<<year<<" yes"<<endl;elsecout<<year<<" no"<<endl;}sum=sum4+sum100+sum400;cout<<sum4<<" "<<sum100<<" "<<sum400<<" "<<sum;return 0;}

表达式E的计算次数代码如下:

#include <iostream>using namespace std;int main () {bool is_true;int sum4=0,sum100=0,sum400=0,sum;for(int year=-3000;year<=3000;year++){if(year==0) year++; //跳过0年//公元前的年份+1int year1=year;if(year1<0) year1=year+1;//获得第1个表达式的计算次数sum400++;if(year1%400==0)is_true=true;elseis_true=false;//获得第2、3个表达式的计算次数if(is_true==false){sum100++;if(year1%100!=0){sum4++;if(year1%4==0)is_true=true;}}//输出年份是否为闰年if(is_true==true)cout<<year<<" yes"<<endl;elsecout<<year<<" no"<<endl;}sum=sum4+sum100+sum400;cout<<sum4<<" "<<sum100<<" "<<sum400<<" "<<sum;}

程序输出的两种表达式的计算次数如下:

表达式

year % 4 == 0

year % 100 != 0

year % 400 == 0

合计

表达式C

6000

1500

4560

12060

表达式E

5940

5985

6000

17925

E-C

-60

4485

1440

5865

上面的代码逻辑有一点点复杂,但仔细看还是能看明白的。有一点比较有意思的是,这个程序还有一个小坑,就是“公元0年”是没有的,要注意刨除掉。我本来想顺道考查下孩子会不会用continue,这小子果然不会,但人家也不含糊,想到用year++的这种方式跳过了0。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/734569.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Decontam去污染:一个尝试

为了程序运行的便利性&#xff0c;不想将Decontam放到windows的Rstudio里面运行&#xff0c;需要直接在Ubuntu中运行&#xff0c;并且为了在Decontam时进行其他操作&#xff0c;使用python去运行R 首先你需要有一个conda环境&#xff0c;安装了R&#xff0c;Decontam&#xff0…

云计算的部署方式(公有云、私有云、混合云、社区云)

云计算的部署方式(公有云、私有云、混合云、社区云) 目录 零、00时光宝盒 一、云计算的部署方式 1.1、公有云&#xff08;Public Cloud&#xff09; 1.2、私有云&#xff08;Private Cloud&#xff09;  1.3、混合云&#xff08;Hybrid Cloud&#xff09; 1.4、社区云&am…

【C++】list模拟实现list迭代器失效问题

list模拟实现&list迭代器失效问题 一&#xff0c;list模拟实现1. list的主要框架接口模拟2. list构造&拷贝构造&析构3. list迭代器3.1 普通迭代器3.2 const迭代器 4. 增删查改 二&#xff0c;迭代器失效问题1. list的迭代器失效原因2. 解决办法 一&#xff0c;list…

Java 汇编源码查看环境搭建

目录 一、简介 二、在IDEA开发环境中搭建汇编环境 2.1 在IDEA中搭建字节码查看环境 2.1.1 搭建步骤 2.1.1.1 第一步 2.1.1.2 第二步 2.1.1.3 第三步 2.1.1.4 第四步 2.1.2 验证 2.2 在IDEA开发环境中搭建汇编代码查看环境 2.2.2 配置HSDIS插件 2.2.3 验证HSDIS插件是…

[虚拟机保护逆向] [HGAME 2023 week4]vm

[虚拟机保护逆向] [HGAME 2023 week4]vm 虚拟机逆向的注意点&#xff1a;具体每个函数的功能&#xff0c;和其对应的硬件编码的*长度* 和 *含义*&#xff0c;都分析出来后就可以编写脚本将题目的opcode转化位vm实际执行的指令 &#xff1a;分析完成函数功能后就可以编写脚本输出…

深度学习在硬件和计算平台上的优化:实现更快、更高效的突破

引言 深度学习&#xff0c;作为机器学习领域的一个子集&#xff0c;通过模拟人脑神经元的连接方式&#xff0c;构建复杂的网络结构来处理和分析数据。然而&#xff0c;随着深度学习模型规模的不断扩大和复杂度的提高&#xff0c;其对计算资源的需求也呈指数级增长。因此&#…

【MySQL】表的增删改查——MySQL基本查询、数据库表的创建、表的读取、表的更新、表的删除

文章目录 MySQL表的增删查改1. Create&#xff08;创建&#xff09;1.1 单行插入1.2 多行插入1.3 替换 2. Retrieve&#xff08;读取&#xff09;2.1 select查看2.2 where条件2.3 结果排序2.4 筛选分页结果 3. Update&#xff08;更新&#xff09;3.1 更新单个数据3.2 更新多个…

如何保证消息的可靠传输

数据的丢失问题&#xff0c;可能出现在生产者、MQ、消费者中 生产者丢失&#xff1a; 生产者将数据发送到 RabbitMQ 的时候&#xff0c;可能数据就在半路给搞丢了&#xff0c;因为网络问题啥的&#xff0c;都有可能。此时可以选择用 RabbitMQ 提供的事务功能&#xff0c;就是生…

Unmanaged PowerShell

简介 在渗透测试当中经常会使用到PowerShell来执行脚本, 但是直接使用PowerShell.exe是一个非常敏感的行为, EDR等产品对PowerShell.exe进程的创建监控的很密切, 并且随着PowerShell的渗透测试工具的普及, 越来越多的EDR会利用微软提供的AMSI接口对PS脚本进行扫描, 但是对于低…

vue实现购物车功能

实现功能 CSS部分 <style>.tr {display: flex;}.th {margin: 10px;width: 20%;height: 50%;}.td {display: flex;margin: 10px;width: 20%;height: 100px;align-items: center;}.app-container .banner-box {border-radius: 20px;overflow: hidden;margin-bottom: 10px;}…

input中文输入法导致的高频事件

这是基本结构 <!DOCTYPE html> <html lang"en"><head><meta charset"UTF-8" /><meta name"viewport" content"widthdevice-width, initial-scale1.0" /><title>中文输入法的高频事件</title&…

通信-CAN-01 总线拓扑

本文主要介绍CAN总线拓扑&#xff0c;并结合实际用到CAN设备做些说明。 1 总线拓扑 拓扑结构中分为CPU&#xff0c;CAN 控制器&#xff0c;收发器&#xff0c;双绞线。CAN控制器根据两根线上的电位差来判断总线电平。发送方通过使总线发生变化&#xff0c;将消息发送给接收方…

BPSK调制解调

BPSK数字调制是相移键控PSK的一种&#xff0c;通过数字信号&#xff0c;调制载波的相位&#xff0c;利用载波的相位变化来反映数字信号&#xff0c;载波的振幅和频率均不变化。PSK应用很广泛&#xff0c;抗噪声性能比ASK和FSK要好&#xff0c;频带利用率较高。BPSK中&#xff0…

前端文件流、切片下载和上传

前端文件流、切片下载和上传技术在提升文件传输效率和优化用户体验方面发挥着关键作用。这些技术不仅可以帮助解决大文件传输过程中可能遇到的问题&#xff0c;如网络超时、内存溢出等&#xff0c;还能通过并行传输和断点续传等功能&#xff0c;提高传输速度和稳定性。 一、前端…

每日学习笔记:C++ 11的Tuple

#include <tuple> Tuple介绍(不定数的值组--可理解为pair的升级版) 定义 创建 取值 初始化 获取tuple元素个数、获取tuple某元素类型、将2个tuple类型串接为1个新tuple类型

解决Ubuntu 16.04/18.04 图形化界面异常、鼠标光标消失、鼠标变成叉叉等问题

bug场景&#xff1a; 一切从一次换源说起…叭叭叭 这篇文章解决的问题&#xff1a; 1.换源&#xff0c;默认源太慢&#xff0c;换成可用的阿里云的源 2.apt-get failed to …问题 3.图形化异常问题 4.get unmet dependence 问题 5. 鼠标光标消失和鼠标变成叉叉问题。 解决方…

【Python】time模块

专栏文章索引&#xff1a;Python 目录 一、介绍​编辑 二、常用函数​编辑 一、介绍 Python 的 time 模块提供了处理时间的函数。 二、常用函数 1.time()&#xff1a;返回当前时间的时间戳&#xff08;从1970年1月1日开始计时的秒数&#xff09;。 import timecurrent_ti…

Android Gradle 开发与应用 (五) : 基于Gradle 8.2,创建Gradle插件

1. 前言 本文介绍在Android中&#xff0c;如何基于Gradle 8.2&#xff0c;创建Gradle插件。 1.1 本文环境 Android Studio 版本 : Android Studio Hedgehog | 2023.1.1Gralde版本 : gradle 8.2 使用 Android Gradle 插件升级助理 Android Gradle 插件版本说明 1.2 为什么要写…

蓝桥杯递推与递归法|斐波那契数列|数字三角形|42点问题|数的计算|数的划分(C++)

递归是用来做dfs&#xff0c;是搜索算法的基础 递推是用来做dp部分&#xff0c;及部分其他算法&#xff0c;复杂度较低&#xff0c;不会出现爆栈问题递推法&#xff1a; 递推法是一种在数学和其他领域广泛应用的重要方法&#xff0c;它在计算机科学中被用作一种关键的数值求解…

LLM 推理优化探微 (3) :如何有效控制 KV 缓存的内存占用,优化推理速度?

编者按&#xff1a; 随着 LLM 赋能越来越多需要实时决策和响应的应用场景&#xff0c;以及用户体验不佳、成本过高、资源受限等问题的出现&#xff0c;大模型高效推理已成为一个重要的研究课题。为此&#xff0c;Baihai IDP 推出 Pierre Lienhart 的系列文章&#xff0c;从多个…