Определение високосного года
Изучаю питон минут 20 и столкнулся с такой задачей:
Дано натуральное число. Требуется определить, является ли год с данным номером високосным. Если год является високосным, то выведите YES, иначе выведите NO. Напомним, что в соответствии с григорианским календарем, год является високосным, если его номер кратен 4, но не кратен 100, а также если он кратен 400.
Написал такой код и на удивление с теми тестами, которые представлены на сайте все работает, но у меня такое ощущение, что оно не должно так работать.
Подскажите пожалуйста, оно ведь не должно работать?
a = int(input())
if a % 100 != 0 and a % 4 == 0 or a % 400 == 0:
print('YES')
else:
print('NO')
Ответы (4 шт):
if a % 100 != 0 and a % 4 == 0 or a % 400 == 0:
Если бы вы написали условие именно в том порядке, как оно задано, то вообще бы вопросов не было. Хотя см. уточнения ниже.
год является високосным, если его номер кратен 4, но не кратен 100, а также если он кратен 400.
Запишем именно в таком же порядке. Это не влияет на выполнение условия, но влияет на наше понимание логики.
if a % 4 == 0 and a % 100 != 0 or a % 400 == 0:
Всё в точности как в условии:
- номер кратен 4 ->
a % 4 == 0 - но (при этом) ->
and - не кратен 100 ->
a % 100 != 0 - а также если ->
or - он кратен 400 ->
a % 400 == 0
Тут нужно пояснить, что:
- Логическое умножение
andимеет больший приоритет, чем сложениеor, поэтому тут как бы подразумеваются такие скобки:if (a % 4 == 0 and a % 100 != 0) or a % 400 == 0: - Фраза "кратен ... но не кратен ..." имеет в виду одновременное выполнение условий, т.е.
and - Фраза "а также если" в данном случае подразумевает "в случае если ... условие тоже выполняется", т.е.
or
Вариант поинтереснее ). Без этих чудовищных ветвлений
def leap_year(year: int):
if year % 4 != 0:
return False
elif year % 100 == 0:
if year % 400 == 0:
return True
else:
return False
else:
return True
year = int(input())
if leap_year(year):
print("YES")
else:
print("No")
Определение високосности года через побитовое И:
def is_leap_year(year):
return ((year * 1073750999) & 3221352463) <= 126976
Указанные "магические числа" корректно работают для диапазона от 0 до 102499 года.
Автор данного решения - Falk Hüffner (перевод на русский) . По данным ссылкам доступно объяснение, как именно было найдено это решение - оно довольно большое и сложное.
По большей части оно найдено перебором с помощью библиотеки Z3. Falk Hüffner искал подходящие константы для максимального покрытия в рамках 32-битных чисел, но с его кодом можно найти подходящее решение для диапазона от 0 до 9999 года - это более оптимальный период, т.к. по григорианскому календарю ошибка копится за ~10.000 лет (в том же Python datetime.MAXYEAR = 9999):
import z3
MAX_YEAR = 9999
BITS = 32
f, m, t, y = z3.BitVecs('f m t y', BITS)
def target(y):
return z3.And((y & 3) == 0, z3.Or(z3.URem(y, 25) != 0, (y & 15) == 0))
def candidate(x):
return z3.ULE((x * f) & m, t)
solver = z3.Solver()
solver.add(z3.ForAll(y, z3.Implies(z3.ULE(y, MAX_YEAR),
candidate(y) == target(y))))
if solver.check() == z3.sat:
print(f'found solution: {solver.model()}')
else:
print('no solution found')
Вывод:
[f = 1073836193, m = 3222241295, t = 1015808]
Соответственно, функция для проверки високосности до 9999 года:
def is_leap_year(year):
return ((year * 1073836193) & 3222241295) <= 1015808
Хм, удивительно, но похоже Python любит if и не любит формулы.
Самая длинная, в исходном коде, функция от @Дмитрий https://ru.stackoverflow.com/a/1614021/430734 оказалась самой шустрой.
Понятно, что если if y % 4 != 0: и if y % 100 != 0: заменить на if y % 4: и if y % 100: станет ещё шустрее ≈7%, но это уже "задний ум", не считается. ?
P.S.
Плюс, по совету @Stanislav Volodarskiy, замена if y % 4: на if y & 3: даёт ещё ≈9%. И, как следствие, замена y % 4 == 0 на not (y & 3) в логической формуле тоже даёт выигрыш.
test_years = list(range(10_000))
leaps = []
def leap_Catik(y: int) -> bool:
return y % 100 != 0 and y % 4 == 0 or y % 400 == 0
leaps.append(leap_Catik)
def leap_CrazyElf(y: int) -> bool:
return y % 4 == 0 and y % 100 != 0 or y % 400 == 0
leaps.append(leap_CrazyElf)
def leap_CrazyElf_1(y: int) -> bool:
return (y % 4 == 0 and y % 100 != 0) or y % 400 == 0
leaps.append(leap_CrazyElf_1)
def leap_Дмитрий(y: int) -> bool:
if y % 4 != 0:
return False
elif y % 100 == 0:
if y % 400 == 0:
return True
else:
return False
else:
return True
leaps.append(leap_Дмитрий)
def leap_Stanislav_Volodarskiy(y: int) -> bool:
if y % 400 == 0: return True
if y % 100 == 0: return False
return y % 4 == 0
leaps.append(leap_Stanislav_Volodarskiy)
def leap_Serge3leo(y: int) -> bool:
return y % 4 == 0 and (y % 100 != 0 or y % 400 == 0)
leaps.append(leap_Serge3leo)
def leap_mrgervant(y: int) -> bool:
return ((y * 1073750999) & 3221352463) <= 126976
leaps.append(leap_mrgervant)
assert not leaps[0](1899)
assert not leaps[0](1900)
assert leaps[0](1980)
assert leaps[0](2000)
for lf in leaps[1:]:
for y in test_years:
assert leaps[0](y) == lf(y)
for lf in leaps:
print(f"{lf.__name__}: Длина байткода={len(lf.__code__.co_code)}")
%timeit for y in test_years: lf(y)
%timeit lf(2025)
Время и размер байт-кода для Python 3.13:
leap_Catik: Длина байткода=78
1.72 ms ± 2.49 μs per loop (mean ± std. dev. of 7 runs, 1,000 loops each)
172 ns ± 0.137 ns per loop (mean ± std. dev. of 7 runs, 10,000,000 loops each)
leap_CrazyElf: Длина байткода=78
1.43 ms ± 1.6 μs per loop (mean ± std. dev. of 7 runs, 1,000 loops each)
134 ns ± 0.399 ns per loop (mean ± std. dev. of 7 runs, 10,000,000 loops each)
leap_CrazyElf_1: Длина байткода=78
1.43 ms ± 3.49 μs per loop (mean ± std. dev. of 7 runs, 1,000 loops each)
134 ns ± 0.317 ns per loop (mean ± std. dev. of 7 runs, 10,000,000 loops each)
leap_Дмитрий: Длина байткода=64
986 μs ± 1.06 μs per loop (mean ± std. dev. of 7 runs, 1,000 loops each)
85.3 ns ± 0.475 ns per loop (mean ± std. dev. of 7 runs, 10,000,000 loops each)
leap_Stanislav_Volodarskiy: Длина байткода=58
1.72 ms ± 3.09 μs per loop (mean ± std. dev. of 7 runs, 1,000 loops each)
159 ns ± 0.351 ns per loop (mean ± std. dev. of 7 runs, 10,000,000 loops each)
leap_Serge3leo: Длина байткода=78
1.03 ms ± 1.04 μs per loop (mean ± std. dev. of 7 runs, 1,000 loops each)
88.4 ns ± 0.212 ns per loop (mean ± std. dev. of 7 runs, 10,000,000 loops each)
leap_mrgervant: Длина байткода=24
2 ms ± 5.34 μs per loop (mean ± std. dev. of 7 runs, 1,000 loops each)
195 ns ± 0.179 ns per loop (mean ± std. dev. of 7 runs, 10,000,000 loops each)
Сравнение быстрых if с быстрыми формулами:
leaps = []
def leap_быстрый_if(y: int) -> bool:
if y & 3:
return False
elif y % 100:
return True
return y % 400 == 0
leaps.append(leap_быстрый_if)
def leap_быстрый_fif(y: int) -> bool:
return False if y & 3 else True if y % 100 else y % 400 == 0
leaps.append(leap_быстрый_fif)
def leap_быстрый_формула(y: int) -> bool:
return not (y & 3) and (y % 100 != 0 or y % 400 == 0)
leaps.append(leap_быстрый_формула)
for lf in leaps:
for y in test_years:
assert leap_Serge3leo(y) == lf(y)
for lf in leaps:
print(f"{lf.__name__}: Длина байткода={len(lf.__code__.co_code)}")
%timeit for y in test_years: lf(y)
%timeit lf(2025)
Время и размер байт-кода для Python 3.13:
leap_быстрый_if: Длина байткода=62
852 μs ± 520 ns per loop (mean ± std. dev. of 7 runs, 1,000 loops each)
71.8 ns ± 0.0888 ns per loop (mean ± std. dev. of 7 runs, 10,000,000 loops each)
leap_быстрый_fif: Длина байткода=66
875 μs ± 11.6 μs per loop (mean ± std. dev. of 7 runs, 1,000 loops each)
73.4 ns ± 0.237 ns per loop (mean ± std. dev. of 7 runs, 10,000,000 loops each)
leap_быстрый_формула: Длина байткода=82
925 μs ± 2.23 μs per loop (mean ± std. dev. of 7 runs, 1,000 loops each)
76.2 ns ± 0.193 ns per loop (mean ± std. dev. of 7 runs, 10,000,000 loops each)