鲜为人知的闰年判定大坑

news2024/11/6 3:16:22

【题目描述】

输入年份,判断是否为闰年。如果是,则输出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年



        //公元前的年份+1

        int year1=year;

        if(year1<0) year1=year+1;



        //获得第1、2个表达式的计算次数

        sum4++;

        if(year1%4==0){

            sum100++;

            if(year1%100!=0)

                is_true=true;

            else

                is_true=false;

        }

        else{

            is_true=false;

        }



        //获得第3个表达式的计算次数

        if(is_true==false) {

            sum400++;

            if(year1%400==0)

                is_true=true;

            else

                is_true=false;

        }



        //输出年份是否为闰年

        if(is_true==true)

            cout<<year<<" yes"<<endl;

        else

            cout<<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年



        //公元前的年份+1

        int year1=year;

        if(year1<0) year1=year+1;



        //获得第1个表达式的计算次数

        sum400++;

        if(year1%400==0)

            is_true=true;

        else

            is_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;

        else

            cout<<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.coloradmin.cn/o/1505693.html

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

相关文章

多线程多进程处理服务器并发(多进程处理如何解决僵死进程)

目录 1.可循环发送数据的代码 2.改成循环之后每次发现只能处理一个客户端 3.服务器端处理并发问题 3.1 思路 3.2 利用多线程实现并发 ​编辑 3.3 利用多进程实现并发 3.3.1 多进程并发产生的僵死进程问题 ​3.3.2 解决僵死进程问题 1.可循环发送数据的代码 服务器代…

vue 在什么情况下在数据发生改变的时候不会触发视图更新

在 Vue 中&#xff0c;通常数据发生变化时&#xff0c;视图会自动更新。但是&#xff0c;有几种情况可能导致数据变化不会触发视图更新&#xff1a; 1.对象属性的添加或删除&#xff1a; Vue 无法检测到对象属性的添加或删除。因为 Vue 在初始化实例时对属性执行了 getter/se…

【教学类-34-09】20240310华文彩云学号拼图(3*3格子浅灰底图 深灰拼图块)(AI对话大师)

作品展示 背景需求&#xff1a; 制作了两位数的学号3*3格子&#xff0c; 【教学类-34-05】20230523拼图&#xff08;数字学号0X-长方块拼图-双色深灰浅灰&#xff09;3*3格子&#xff08;中班主题《个别化拼图》偏艺术-美术&#xff09;_灰底白色方块数字怎么制作-CSDN博客文…

PhantomCrawler:一款功能强大的多代理IP网站请求生成工具

关于PhantomCrawler PhantomCrawler是一款功能强大的多代理IP网站请求生成工具&#xff0c;该工具允许广大研究人员通过不同的代理IP地址来模拟与目标Web站点的交互行为。 PhantomCrawler基于Python、requests和BeautifulSoup实现其功能&#xff0c;并提供了一种简单且高效的方…

【Linux】gcc与make、makefile

文章目录 1 gcc/g1.1 预处理1.2 编译1.3 汇编1.4 链接1.4.1 静态链接1.4.2 动态链接 2 make和makefile2.1 依赖关系2.2 依赖方法2.3 伪目标 3 总结 1 gcc/g 当我们创建一个文件&#xff0c;并向里面写入代码&#xff0c;此时&#xff0c;我们该如何使我们的代码能够运行起来呢&…

Delphi 的Read 与Readln 的区别

结合运行窗口&#xff0c;你输入1 2 3 4 这是一行ReadLn在读入时把这四个数当成一行&#xff0c;read(a,b)只读入了前两个数&#xff1a;1 2&#xff0c;就准备读下一行了&#xff0c;下一行输入3&#xff0c;再下一行输入2&#xff0c;所以输出1232&#xff1b; Read是逐个读…

【MySQL 系列】MySQL 语句篇_DQL 语句

DQL&#xff08;Data Query Language&#xff09;&#xff0c;即数据查询语言&#xff0c;用来查询数据记录。DQL 基本结构由 SELECT FROM、WHERE、JOIN 等子句构成。 DQL 语句并不会改变数据库&#xff0c;而是让数据库将查询结果发送结果集给客户端&#xff0c;返回的结果是一…

IDEA打开项目文件目录不见了

偶尔发生新拉下来的代码&#xff0c;或者旧代码修改了包名&#xff0c;项目名称等&#xff0c;idea左侧project一栏不显示代码的文件目录。例如下面此时不要慌张&#xff0c;不用删除项目重新拉取&#xff0c;通过以下方式解决&#xff1a; 本人尝试能够解决&#xff0c;如果无…

Learn OpenGL 05 变换

万向节死锁 万向节死锁&#xff08;Gimbal Lock&#xff09;是用欧拉角定义旋转时&#xff0c;产生的在某一情况下旋转轴重合导致的系统丢失自由度的情况&#xff0c;一种最简单的解决方式是调整三维软件中的旋转轴顺序来避免该情况发生。 也就是说当中间轴旋转至90的时候就会…

Vue+ElementUI启动vue卡死的问题

0 引言 今天&#xff0c;博主在学习vueelementui的时候遇到一个问题&#xff0c;卡了博主很久。 1 问题复现 在vue页面的<template>标签中写入两个<div>标签&#xff0c; <template><div><h1>第一个div标签</h1><el-table></…

vue学习笔记21-组件传递数据_Props

组件与组件之间不是完全独立的&#xff0c;而是有交集的&#xff0c;那就是组件与组件之间是可以传递数据的 传递数据的解决方案就是props 父级&#xff1a; 在父级中引入子集 <template><h3>Parent</h3><Child/> </template><script> i…

适配器模式已经在SpringMVC中的源码实现

介绍&#xff1a; 1、适配器模式将某个类的接口转换成客户端期望的另一种接口表示 2、目的&#xff1a;兼容性&#xff0c;让原本因接口不匹配不能一起工作的两个类可以协同工作。其别名为&#xff1a;包装器。 3、属于&#xff1a;结构型模式 4、分3类&#xff1a;1&#xff0…

方程式工具包远程溢出漏洞图形界面版V0.3(内置永恒之蓝、永恒冠军、永恒浪漫等)

Part1 前言 大家好&#xff0c;我是ABC_123。我从年前到现在&#xff0c;一直在整理曾经写过的红队工具&#xff0c;逐步把自己认为比较好用的原创工具发出来给大家用一用&#xff0c;方便大家在日常的攻防比赛、红队评估项目中解放双手&#xff0c;节省时间精力和体力。本期给…

MySQL中常用的操作语句已汇总

目录 一、库语句 1.查询现有数据库 2.创建数据库 3.选中数据库 ​编辑 4.删除数据库 二、初阶表操作 1.查看数据库现有表 2.查看表结构 3.创建表 4.删除表 5.全列查询 6.删除表2 7.修改操作 三、插入操作 1.全列插入 2.指定列插入 3.一次插入多组数据 4.插入…

SpringBoot集成netty实现websocket通信

实现推送消息给指定的用户 一、依赖 <?xml version"1.0" encoding"UTF-8"?> <project xmlns"http://maven.apache.org/POM/4.0.0"xmlns:xsi"http://www.w3.org/2001/XMLSchema-instance"xsi:schemaLocation"http://m…

redis-集群 原生部署和工具自动部署

什么redis集群&#xff1f; redis集群是一个提供在多个redis节点之间共享数据的程序集。它并不像redis主从复制模式那样仅提供一个master节点来提供写服务&#xff0c;而是会提供多个master节点来提供写服务&#xff0c;每个master节点中存储的数据都不一样&#xff0c;这些数据…

HarmonyOS系统开发基础环境搭建

目录 一 鸿蒙介绍&#xff1a; 1.1 HarmonyOS系统 1.2 HarmonyOS软件编程语言 二 HarmonyOS编程环境搭建 1.1 官网下载地址 1.2搭建开发流程 1.3 创建安装目录 1.4 下载DevEco Studio​编辑 1.5 下载后点击安装 1.6 自动添加桌面快捷和bin路径 ​编辑1.7 安装好运行 …

[Angular 基础] - 表单:模板驱动表单

[Angular 基础] - 表单&#xff1a;模板驱动表单 之前的笔记&#xff1a; [Angular 基础] - routing 路由(上) [Angular 基础] - routing 路由(下) [Angular 基础] - Observable Angular 内置两种表单的支持&#xff0c;这篇写的就是第一种&#xff0c;即模板驱动表单 (Tem…

wps由于找不到krpt.dll,无法继续执行代码的解决方法

遇到由于找不到krpt.dll,无法继续执行代码的问题时&#xff0c;理解如何修复这个问题变得至关重要。本文会教大家krpt.dll的恢复流程&#xff0c;并介绍该DLL文件的相关属性。我们将一步步指导你如何处理缺失文件的情况&#xff0c;让你能够解决阻碍代码正常运行的障碍&#xf…

C语言初学10:typedef

一、作用 为用户定义的数据类型取一个新名字 二、对结构体使用typedef定义新的数据类型名字 #include <stdio.h> #include <string.h>typedef struct Books //使用 typedef 来定义一个新的数据类型名字 {char title[50];} book;int main( ) {//book是typedef定…