diff --git a/README.md b/README.md index 086b3e3..34bf2e3 100644 --- a/README.md +++ b/README.md @@ -21,3 +21,8 @@ 为了便于管理和使用,简单创建了一个脚本。 这个脚本只有三个主要功能,一个是创建新问题的文件,一个是列出已创建问题,还有一个是运行指定问题的python解法。 + + +----- + +优化了装饰器Banchmark运行时间的计算函数,现在可以重复运行并计算平均运行时间。(55题(含)之后开始使用。) diff --git a/main.py b/main.py index 33e06df..0ac29e0 100644 --- a/main.py +++ b/main.py @@ -22,17 +22,49 @@ def add_newproblem(num: int, name: str | None = None) -> None: ''' import time +from functools import wraps +from typing import Any, Callable, TypeVar + +F = TypeVar("F", bound=Callable[..., Any]) -def timer(func): - def wrapper(*args, **kwargs): - start_time = time.time() - result = func(*args, **kwargs) - end_time = time.time() - elapsed_time = end_time - start_time - print(f"{func.__name__} time: {elapsed_time:.6f} seconds") - return result - return wrapper +def benchmark(repeat: int = 1) -> Callable[[F], F]: + ''' + 重复运行目标函数并计算平均耗时。 + + 平均耗时会被存储在 wrapper.avg_time 和 wrapper.total_time 中, + 同时会打印到控制台。函数的返回值不受影响。 + ''' + if repeat < 1: + raise ValueError("repeat 必须 >= 1") + + def decorator(func: F) -> F: + @wraps(func) + def wrapper(*args: Any, **kwargs: Any) -> Any: + total = 0.0 + result = None + + for _ in range(repeat): + start = time.perf_counter() + result = func(*args, **kwargs) + end = time.perf_counter() + total += end - start + + wrapper.avg_time = total / repeat # type: ignore[attr-defined] + wrapper.total_time = total # type: ignore[attr-defined] + + print( + f"[Benchmark] {func.__name__} | 重复 {repeat} 次 | " + f"平均: {wrapper.avg_time:.6f}s | 总计: {wrapper.total_time:.6f}s" # type: ignore[attr-defined] + ) + return result + + # 初始化属性,避免调用前访问报错 + wrapper.avg_time = 0.0 # type: ignore[attr-defined] + wrapper.total_time = 0.0 # type: ignore[attr-defined] + return wrapper # type: ignore[return-value] + + return decorator """) diff --git a/solutions/0055.LychrelNum/euler_55.py b/solutions/0055.LychrelNum/euler_55.py new file mode 100644 index 0000000..367728f --- /dev/null +++ b/solutions/0055.LychrelNum/euler_55.py @@ -0,0 +1,98 @@ +""" +If we take 47, reverse and add, 47 + 74 = 121, which is palindromic. + +Not all numbers produce palindromes so quickly. For example, + 349 + 943 = 1292 + 1292 + 2921 = 4213 + 4213 + 3124 = 7337 +That is, 349 took three iterations to arrive at a palindrome. + +Although no one has proved it yet, it is thought that some numbers, like 196, +never produce a palindrome. A number that never forms a palindrome through the +reverse and add process is called a Lychrel number. Due to the theoretical nature +of these numbers, and for the purpose of this problem, we shall assume that +a number is Lychrel until proven otherwise. In addition you are given that for +every number below ten-thousand, it will either (i) become a palindrome +in less than fifty iterations, or, (ii) no one, with all the computing power that exists, +has managed so far to map it to a palindrome. In fact, 10677 is the first number to +be shown to require over fifty iterations before producing a palindrome: + 4668731596684224866951378664 (53 iterations, 28-digits). + +Surprisingly, there are palindromic numbers that are themselves Lychrel numbers; +the first example is 4994. + +How many Lychrel numbers are there below ten-thousand? + +NOTE: Wording was modified slightly on 24 April 2007 to emphasise the theoretical nature of Lychrel numbers. +""" + +import time +from functools import wraps +from typing import Any, Callable, TypeVar + +F = TypeVar("F", bound=Callable[..., Any]) + + +def benchmark(repeat: int = 1) -> Callable[[F], F]: + """ + 重复运行目标函数并计算平均耗时。 + + 平均耗时会被存储在 wrapper.avg_time 和 wrapper.total_time 中, + 同时会打印到控制台。函数的返回值不受影响。 + """ + if repeat < 1: + raise ValueError("repeat 必须 >= 1") + + def decorator(func: F) -> F: + @wraps(func) + def wrapper(*args: Any, **kwargs: Any) -> Any: + total = 0.0 + result = None + + for _ in range(repeat): + start = time.perf_counter() + result = func(*args, **kwargs) + end = time.perf_counter() + total += end - start + + wrapper.avg_time = total / repeat # type: ignore[attr-defined] + wrapper.total_time = total # type: ignore[attr-defined] + + print( + f"[Benchmark] {func.__name__} | 重复 {repeat} 次 | " + f"平均: {wrapper.avg_time:.6f}s | 总计: {wrapper.total_time:.6f}s" # type: ignore[attr-defined] + ) + return result + + # 初始化属性,避免调用前访问报错 + wrapper.avg_time = 0.0 # type: ignore[attr-defined] + wrapper.total_time = 0.0 # type: ignore[attr-defined] + return wrapper # type: ignore[return-value] + + return decorator + + +def is_palindrome(n): + return str(n) == str(n)[::-1] + + +def is_lychrel(n, max_iterations=50): + for _ in range(max_iterations): + n += int(str(n)[::-1]) + if is_palindrome(n): + return False + return True + + +@benchmark(repeat=15) +def count_lychrel_numbers(limit): + count = 0 + for n in range(1, limit + 1): + if is_lychrel(n): + count += 1 + return count + + +if __name__ == "__main__": + result = count_lychrel_numbers(10000) + print(f"Number of Lychrel numbers below 10,000: {result}") diff --git a/solutions/0055.LychrelNum/euler_55_bit.py b/solutions/0055.LychrelNum/euler_55_bit.py new file mode 100644 index 0000000..f7a2b2a --- /dev/null +++ b/solutions/0055.LychrelNum/euler_55_bit.py @@ -0,0 +1,112 @@ +""" +If we take 47, reverse and add, 47 + 74 = 121, which is palindromic. + +Not all numbers produce palindromes so quickly. For example, + 349 + 943 = 1292 + 1292 + 2921 = 4213 + 4213 + 3124 = 7337 +That is, 349 took three iterations to arrive at a palindrome. + +Although no one has proved it yet, it is thought that some numbers, like 196, +never produce a palindrome. A number that never forms a palindrome through the +reverse and add process is called a Lychrel number. Due to the theoretical nature +of these numbers, and for the purpose of this problem, we shall assume that +a number is Lychrel until proven otherwise. In addition you are given that for +every number below ten-thousand, it will either (i) become a palindrome +in less than fifty iterations, or, (ii) no one, with all the computing power that exists, +has managed so far to map it to a palindrome. In fact, 10677 is the first number to +be shown to require over fifty iterations before producing a palindrome: + 4668731596684224866951378664 (53 iterations, 28-digits). + +Surprisingly, there are palindromic numbers that are themselves Lychrel numbers; +the first example is 4994. + +How many Lychrel numbers are there below ten-thousand? + +NOTE: Wording was modified slightly on 24 April 2007 to emphasise the theoretical nature of Lychrel numbers. +""" + +import time +from functools import wraps +from typing import Any, Callable, TypeVar + +from bitarray import bitarray + +F = TypeVar("F", bound=Callable[..., Any]) + + +def benchmark(repeat: int = 1) -> Callable[[F], F]: + """ + 重复运行目标函数并计算平均耗时。 + + 平均耗时会被存储在 wrapper.avg_time 和 wrapper.total_time 中, + 同时会打印到控制台。函数的返回值不受影响。 + """ + if repeat < 1: + raise ValueError("repeat 必须 >= 1") + + def decorator(func: F) -> F: + @wraps(func) + def wrapper(*args: Any, **kwargs: Any) -> Any: + total = 0.0 + result = None + + for _ in range(repeat): + start = time.perf_counter() + result = func(*args, **kwargs) + end = time.perf_counter() + total += end - start + + wrapper.avg_time = total / repeat # type: ignore[attr-defined] + wrapper.total_time = total # type: ignore[attr-defined] + + print( + f"[Benchmark] {func.__name__} | 重复 {repeat} 次 | " + f"平均: {wrapper.avg_time:.6f}s | 总计: {wrapper.total_time:.6f}s" # type: ignore[attr-defined] + ) + return result + + # 初始化属性,避免调用前访问报错 + wrapper.avg_time = 0.0 # type: ignore[attr-defined] + wrapper.total_time = 0.0 # type: ignore[attr-defined] + return wrapper # type: ignore[return-value] + + return decorator + + +def reverse_int(n: int) -> int: + return int(str(n)[::-1]) + + +@benchmark(repeat=15) +def count_lychrel_numbers(limit: int = 10000, max_iter: int = 50) -> int: + is_not_lychrel = bitarray(limit + 1) + is_not_lychrel.setall(0) + is_not_lychrel[0] = 1 + + for i in range(1, limit + 1): + if is_not_lychrel[i]: + continue + + n = i + chain = [n] + + for _ in range(max_iter): + n += reverse_int(n) + + # 检查是否形成回文 + if n == reverse_int(n): + for num in chain: + if num > limit: + break + is_not_lychrel[num] = 1 + break + + chain.append(n) + + return is_not_lychrel.count(0) + + +if __name__ == "__main__": + result = count_lychrel_numbers(10000) + print(f"Number of Lychrel numbers below 10,000: {result}") diff --git a/solutions/0055.LychrelNum/readme.md b/solutions/0055.LychrelNum/readme.md new file mode 100644 index 0000000..c1e75db --- /dev/null +++ b/solutions/0055.LychrelNum/readme.md @@ -0,0 +1,3 @@ +硬算比反馈记录快? +在这个问题上,由于数字逆序相加这个特性,造成数字快速增长,可能很多中间过程都在计算目标之外, +所以记录并未减少实际计算时间。