第14章:数学与位运算技巧
数学思维是算法问题的基石。许多看似复杂的算法题,一旦发现其中的数学规律,就能用简洁优雅的方式解决。对于有数学和函数式编程背景的工程师来说,这种思维方式尤其自然——我们习惯于寻找问题的代数结构,用数学归纳法证明正确性,用位运算优化性能。本章将系统梳理 LeetCode 中常见的数学技巧,帮助你建立从数学角度审视算法问题的直觉。
14.1 位运算:异或的妙用、位掩码技巧
位运算基础操作
位运算直接操作二进制位,是计算机最底层也是最高效的运算。掌握位运算不仅能优化代码性能,更重要的是它提供了一种全新的问题视角。
基本位运算符:
&(AND):两位都为1时结果为1|(OR):至少一位为1时结果为1^(XOR):两位不同时结果为1~(NOT):按位取反<<(左移):相当于乘以2的幂>>(右移):相当于除以2的幂
常用位运算技巧:
# 判断奇偶
is_odd = n & 1
# 获取第i位
bit_i = (n >> i) & 1
# 设置第i位为1
n |= (1 << i)
# 清除第i位
n &= ~(1 << i)
# 翻转第i位
n ^= (1 << i)
# 清除最低位的1
n &= (n - 1)
# 获取最低位的1
lowest_bit = n & (-n)
# 判断是否为2的幂
is_power_of_2 = n > 0 and (n & (n - 1)) == 0
异或运算的性质与应用
异或运算有几个重要的代数性质,这使得它在算法中有独特的应用:
- 自反性:
a ^ a = 0 - 恒等性:
a ^ 0 = a - 交换律:
a ^ b = b ^ a - 结合律:
(a ^ b) ^ c = a ^ (b ^ c)
这些性质的直接应用:找出数组中只出现一次的数字。
def singleNumber(nums):
"""#136: 数组中所有数字都出现两次,只有一个数字出现一次"""
result = 0
for num in nums:
result ^= num
return result
# 原理:相同的数字异或为0,0异或任何数等于该数本身
位掩码与状态压缩
位掩码是用二进制位表示集合或状态的技术。每一位代表一个元素是否存在或某个状态是否开启。
class Bitmask:
def __init__(self):
self.mask = 0
def add(self, i):
"""添加元素i到集合"""
self.mask |= (1 << i)
def remove(self, i):
"""从集合中移除元素i"""
self.mask &= ~(1 << i)
def contains(self, i):
"""检查集合是否包含元素i"""
return (self.mask >> i) & 1
def intersection(self, other):
"""求两个集合的交集"""
return self.mask & other.mask
def union(self, other):
"""求两个集合的并集"""
return self.mask | other.mask
实例分析:#137 只出现一次的数字 II
题目:数组中每个元素都出现三次,只有一个元素出现一次,找出这个元素。
这题不能简单地用异或解决,需要更巧妙的位运算技巧:
def singleNumber(nums):
"""
思路:统计每一位上1出现的次数,如果某位上1的个数不是3的倍数,
说明只出现一次的数在该位上是1
"""
result = 0
for i in range(32): # 处理32位整数的每一位
bit_sum = 0
for num in nums:
# 统计第i位上1的个数
bit_sum += (num >> i) & 1
# 如果该位上1的个数不是3的倍数,结果的该位为1
if bit_sum % 3:
# Python需要特殊处理负数
if i == 31: # 符号位
result -= (1 << 31)
else:
result |= (1 << i)
return result
更优雅的解法使用有限状态机:
def singleNumber_FSM(nums):
"""
使用两个变量 ones 和 twos 记录各位出现1的次数模3的结果
ones: 记录出现1次的位
twos: 记录出现2次的位
"""
ones = twos = 0
for num in nums:
# 更新twos:原来出现1次的位,现在又出现了,变成出现2次
twos |= ones & num
# 更新ones:异或操作,新出现的位变成1,已有的位变成0
ones ^= num
# 清除出现3次的位(ones和twos中都有的位)
threes = ones & twos
ones &= ~threes
twos &= ~threes
return ones
实例分析:#260 只出现一次的数字 III
题目:数组中有两个元素只出现一次,其余元素都出现两次,找出这两个元素。
def singleNumber(nums):
"""
思路:
1. 先对所有数字异或,得到两个单独数字的异或结果
2. 找到异或结果中任意一个为1的位(说明两个数在该位不同)
3. 根据该位将数组分成两组,每组包含一个单独的数字
"""
# 第一步:得到两个单独数字的异或结果
xor_result = 0
for num in nums:
xor_result ^= num
# 第二步:找到最低位的1(两个数字不同的位)
diff_bit = xor_result & (-xor_result)
# 第三步:根据该位分组并分别异或
num1 = num2 = 0
for num in nums:
if num & diff_bit:
num1 ^= num
else:
num2 ^= num
return [num1, num2]
14.2 数论基础:GCD、质数、模运算
最大公约数与最小公倍数
欧几里得算法(辗转相除法)是计算最大公约数的经典算法:
def gcd(a, b):
"""递归版本"""
if b == 0:
return a
return gcd(b, a % b)
def gcd_iterative(a, b):
"""迭代版本"""
while b:
a, b = b, a % b
return a
def lcm(a, b):
"""最小公倍数 = a * b / gcd(a, b)"""
return a * b // gcd(a, b)
扩展欧几里得算法可以找到满足 ax + by = gcd(a, b) 的整数解:
def extended_gcd(a, b):
"""
返回 (gcd, x, y) 使得 ax + by = gcd(a, b)
"""
if b == 0:
return a, 1, 0
gcd, x1, y1 = extended_gcd(b, a % b)
x = y1
y = x1 - (a // b) * y1
return gcd, x, y
质数判定与筛法
判断一个数是否为质数:
def is_prime(n):
"""O(√n) 时间复杂度"""
if n < 2:
return False
if n == 2:
return True
if n % 2 == 0:
return False
# 只需检查到 √n
i = 3
while i * i <= n:
if n % i == 0:
return False
i += 2
return True
埃拉托斯特尼筛法(Sieve of Eratosthenes)找出范围内所有质数:
def sieve_of_eratosthenes(n):
"""找出所有小于等于n的质数"""
if n < 2:
return []
is_prime = [True] * (n + 1)
is_prime[0] = is_prime[1] = False
for i in range(2, int(n**0.5) + 1):
if is_prime[i]:
# 标记i的所有倍数为非质数
for j in range(i*i, n + 1, i):
is_prime[j] = False
return [i for i in range(n + 1) if is_prime[i]]
模运算的性质
模运算在算法中经常用于防止整数溢出和循环处理:
# 模运算基本性质
# (a + b) % m = ((a % m) + (b % m)) % m
# (a * b) % m = ((a % m) * (b % m)) % m
# (a - b) % m = ((a % m) - (b % m) + m) % m # 注意处理负数
def mod_inverse(a, m):
"""
计算a在模m下的乘法逆元(要求gcd(a, m) = 1)
使用扩展欧几里得算法
"""
gcd, x, _ = extended_gcd(a, m)
if gcd != 1:
return None # 逆元不存在
return x % m
快速幂算法
计算 $a^n \bmod m$ 的高效算法:
def power_mod(a, n, m):
"""
计算 (a^n) % m,时间复杂度 O(log n)
基于二进制表示:a^n = a^(b_k * 2^k + ... + b_1 * 2 + b_0)
"""
result = 1
a = a % m
while n > 0:
if n & 1: # 如果n的最低位是1
result = (result * a) % m
a = (a * a) % m # a = a^2
n >>= 1 # n = n // 2
return result
实例分析:#166 分数到小数
题目:给定两个整数,表示分数的分子和分母,返回小数形式的字符串。如果是循环小数,将循环部分括在括号内。
def fractionToDecimal(numerator, denominator):
"""
关键:检测循环的方法是记录余数出现的位置
当余数重复出现时,说明开始循环
"""
if numerator == 0:
return "0"
result = []
# 处理符号
if (numerator < 0) != (denominator < 0):
result.append("-")
numerator, denominator = abs(numerator), abs(denominator)
# 整数部分
result.append(str(numerator // denominator))
numerator %= denominator
if numerator == 0:
return "".join(result)
result.append(".")
# 小数部分:记录余数出现的位置
remainder_map = {}
while numerator != 0:
if numerator in remainder_map:
# 发现循环
index = remainder_map[numerator]
result.insert(index, "(")
result.append(")")
break
remainder_map[numerator] = len(result)
numerator *= 10
result.append(str(numerator // denominator))
numerator %= denominator
return "".join(result)
实例分析:#50 Pow(x, n)
题目:实现 pow(x, n),即计算 x 的 n 次幂。
def myPow(x, n):
"""
使用快速幂算法,时间复杂度 O(log n)
"""
if n == 0:
return 1
if n < 0:
x = 1 / x
n = -n
result = 1
current_product = x
while n > 0:
if n & 1: # 如果n是奇数
result *= current_product
current_product *= current_product
n >>= 1
return result
def myPow_recursive(x, n):
"""递归版本,更直观"""
if n == 0:
return 1
if n < 0:
return 1 / myPow_recursive(x, -n)
if n & 1: # 奇数
return x * myPow_recursive(x * x, n >> 1)
else: # 偶数
return myPow_recursive(x * x, n >> 1)
14.3 组合数学:排列组合公式的应用
排列组合基础公式
组合数学为我们提供了计数的系统方法。掌握这些公式不仅能直接解决计数问题,还能帮助我们分析算法的时间复杂度。
基本公式:
- 排列数:$P(n, k) = \frac{n!}{(n-k)!}$
- 组合数:$C(n, k) = \binom{n}{k} = \frac{n!}{k!(n-k)!}$
组合数的性质:
- 对称性:$\binom{n}{k} = \binom{n}{n-k}$
- 帕斯卡恒等式:$\binom{n}{k} = \binom{n-1}{k-1} + \binom{n-1}{k}$
- 范德蒙德恒等式:$\sum_{k=0}^r \binom{m}{k}\binom{n}{r-k} = \binom{m+n}{r}$
计算组合数的实现:
def factorial(n):
"""计算阶乘"""
if n <= 1:
return 1
result = 1
for i in range(2, n + 1):
result *= i
return result
def combination(n, k):
"""计算组合数 C(n, k)"""
if k > n or k < 0:
return 0
if k == 0 or k == n:
return 1
# 利用对称性优化
k = min(k, n - k)
# 避免溢出的计算方法
result = 1
for i in range(k):
result = result * (n - i) // (i + 1)
return result
def combination_dp(n, k):
"""动态规划计算组合数,基于帕斯卡三角形"""
dp = [[0] * (k + 1) for _ in range(n + 1)]
for i in range(n + 1):
dp[i][0] = 1
for j in range(1, min(i, k) + 1):
dp[i][j] = dp[i-1][j-1] + dp[i-1][j]
return dp[n][k]
下一个排列算法
下一个排列是组合数学中的经典问题,要求找到当前排列在字典序中的下一个排列。
算法思路:
- 从右往左找第一个升序对 (i, i+1),即 nums[i] < nums[i+1]
- 从右往左找第一个大于 nums[i] 的元素 nums[j]
- 交换 nums[i] 和 nums[j]
- 反转 i+1 到末尾的所有元素
def nextPermutation(nums):
"""
#31: 下一个排列
修改原数组,将其变为下一个排列
"""
n = len(nums)
# 步骤1:找到第一个升序对
i = n - 2
while i >= 0 and nums[i] >= nums[i + 1]:
i -= 1
if i >= 0: # 如果不是最后一个排列
# 步骤2:找到第一个大于nums[i]的元素
j = n - 1
while j > i and nums[j] <= nums[i]:
j -= 1
# 步骤3:交换
nums[i], nums[j] = nums[j], nums[i]
# 步骤4:反转i+1到末尾
left, right = i + 1, n - 1
while left < right:
nums[left], nums[right] = nums[right], nums[left]
left += 1
right -= 1
康托展开与逆康托展开
康托展开将排列映射到自然数,可以在排列和序号之间建立一一对应关系。
def cantor_encode(perm):
"""
康托展开:将排列转换为序号
序号 = Σ(smaller[i] * (n-i-1)!)
其中 smaller[i] 是 perm[i] 右边比它小的元素个数
"""
n = len(perm)
result = 0
factorial = [1] * n
# 预计算阶乘
for i in range(1, n):
factorial[i] = factorial[i-1] * i
for i in range(n):
smaller = 0
for j in range(i + 1, n):
if perm[j] < perm[i]:
smaller += 1
result += smaller * factorial[n - i - 1]
return result
def cantor_decode(n, k):
"""
逆康托展开:将序号转换为排列
"""
nums = list(range(1, n + 1))
factorial = [1] * n
for i in range(1, n):
factorial[i] = factorial[i-1] * i
result = []
k -= 1 # 转换为0索引
for i in range(n, 0, -1):
index = k // factorial[i-1]
result.append(nums[index])
nums.pop(index)
k %= factorial[i-1]
return result
卡特兰数的应用
卡特兰数是组合数学中的重要数列,在许多计数问题中出现:
$C_n = \frac{1}{n+1}\binom{2n}{n} = \frac{(2n)!}{(n+1)!n!}$
递推关系:$C_{n+1} = \sum_{i=0}^n C_i C_{n-i}$
卡特兰数的应用场景:
- n对括号的合法组合数
- n+1个节点的不同二叉搜索树数量
- n×n网格从左下到右上不越过对角线的路径数
- n个元素的出栈序列数
def catalan_number(n):
"""计算第n个卡特兰数"""
if n <= 1:
return 1
# 方法1:使用公式
result = 1
for i in range(n):
result = result * (2 * n - i) // (i + 1)
return result // (n + 1)
def catalan_dp(n):
"""动态规划计算卡特兰数"""
dp = [0] * (n + 1)
dp[0] = dp[1] = 1
for i in range(2, n + 1):
for j in range(i):
dp[i] += dp[j] * dp[i - 1 - j]
return dp[n]
实例分析:#60 排列序列
题目:给出集合 [1,2,3,...,n],返回所有排列按字典序排序后的第 k 个排列。
def getPermutation(n, k):
"""
使用康托展开的思想:
第k个排列可以通过确定每一位的数字来构造
"""
nums = list(range(1, n + 1))
factorial = [1] * n
# 计算阶乘
for i in range(1, n):
factorial[i] = factorial[i-1] * i
k -= 1 # 转换为0索引
result = []
for i in range(n, 0, -1):
# 当前位置应该选择第index个数字
index = k // factorial[i-1]
result.append(str(nums[index]))
nums.pop(index)
k %= factorial[i-1]
return ''.join(result)
14.4 几何问题:凸包、最近点对
点、线、面的基本计算
几何问题的基础是向量运算和解析几何:
import math
class Point:
def __init__(self, x, y):
self.x = x
self.y = y
def distance_to(self, other):
"""两点间距离"""
return ((self.x - other.x)**2 + (self.y - other.y)**2)**0.5
def __sub__(self, other):
"""向量减法"""
return Point(self.x - other.x, self.y - other.y)
def dot_product(p1, p2):
"""点积:a·b = |a||b|cos(θ)"""
return p1.x * p2.x + p1.y * p2.y
def cross_product(p1, p2):
"""叉积:a×b = |a||b|sin(θ)"""
return p1.x * p2.y - p1.y * p2.x
def orientation(p, q, r):
"""
判断三点的方向关系:
返回值 > 0: 逆时针
返回值 = 0: 共线
返回值 < 0: 顺时针
"""
return cross_product(q - p, r - q)
def point_on_segment(p, q, r):
"""判断点q是否在线段pr上"""
return (min(p.x, r.x) <= q.x <= max(p.x, r.x) and
min(p.y, r.y) <= q.y <= max(p.y, r.y) and
orientation(p, q, r) == 0)
def segments_intersect(p1, q1, p2, q2):
"""判断两线段是否相交"""
o1 = orientation(p1, q1, p2)
o2 = orientation(p1, q1, q2)
o3 = orientation(p2, q2, p1)
o4 = orientation(p2, q2, q1)
# 一般情况:两线段跨立
if o1 * o2 < 0 and o3 * o4 < 0:
return True
# 特殊情况:共线且重叠
if o1 == 0 and point_on_segment(p1, p2, q1):
return True
if o2 == 0 and point_on_segment(p1, q2, q1):
return True
if o3 == 0 and point_on_segment(p2, p1, q2):
return True
if o4 == 0 and point_on_segment(p2, q1, q2):
return True
return False
叉积与几何判定
叉积是二维几何中最重要的工具,可以用于:
- 判断点的位置关系
- 计算多边形面积
- 判断凸性
def polygon_area(points):
"""
计算多边形面积(可以是凸多边形或凹多边形)
使用鞋带公式(Shoelace formula)
"""
n = len(points)
area = 0
for i in range(n):
j = (i + 1) % n
area += points[i][0] * points[j][1]
area -= points[j][0] * points[i][1]
return abs(area) / 2
def is_convex_polygon(points):
"""判断多边形是否为凸多边形"""
n = len(points)
if n < 3:
return False
sign = 0
for i in range(n):
p1 = points[i]
p2 = points[(i + 1) % n]
p3 = points[(i + 2) % n]
cross = cross_product(
Point(p2[0] - p1[0], p2[1] - p1[1]),
Point(p3[0] - p2[0], p3[1] - p2[1])
)
if cross != 0:
if sign == 0:
sign = 1 if cross > 0 else -1
elif sign * cross < 0:
return False
return True
凸包算法
凸包是包含所有给定点的最小凸多边形。Graham扫描是经典的凸包算法:
def convex_hull_graham(points):
"""
Graham扫描算法求凸包
时间复杂度:O(n log n)
"""
n = len(points)
if n < 3:
return points
# 找到最下方的点(y坐标最小,相同则x坐标最小)
start = min(points, key=lambda p: (p[1], p[0]))
# 按极角排序
def polar_angle(p):
dx = p[0] - start[0]
dy = p[1] - start[1]
return (math.atan2(dy, dx), dx*dx + dy*dy)
sorted_points = sorted(points, key=polar_angle)
# 构建凸包
hull = []
for p in sorted_points:
# 移除非左转的点
while len(hull) >= 2:
o = orientation(
Point(*hull[-2]),
Point(*hull[-1]),
Point(*p)
)
if o <= 0: # 不是逆时针
hull.pop()
else:
break
hull.append(p)
return hull
def convex_hull_jarvis(points):
"""
Jarvis步进法(礼品包装算法)
时间复杂度:O(nh),h是凸包上的点数
"""
n = len(points)
if n < 3:
return points
# 找到最左边的点
l = min(range(n), key=lambda i: points[i][0])
hull = []
p = l
while True:
hull.append(points[p])
# 找到最逆时针的点
q = (p + 1) % n
for i in range(n):
o = orientation(
Point(*points[p]),
Point(*points[i]),
Point(*points[q])
)
if o > 0: # i在pq的左边
q = i
p = q
if p == l: # 回到起点
break
return hull
扫描线算法
扫描线是处理几何问题的重要技术,特别适合处理区间相关问题:
def rectangle_area_union(rectangles):
"""
计算多个矩形的并集面积
使用扫描线算法
"""
events = []
# 创建事件:矩形的左右边界
for x1, y1, x2, y2 in rectangles:
events.append((x1, y1, y2, 1)) # 左边界,开始
events.append((x2, y1, y2, -1)) # 右边界,结束
events.sort()
def calculate_y_coverage(intervals):
"""计算y轴上的覆盖长度"""
if not intervals:
return 0
intervals.sort()
total = 0
start, end = intervals[0]
for s, e in intervals[1:]:
if s <= end:
end = max(end, e)
else:
total += end - start
start, end = s, e
total += end - start
return total
total_area = 0
prev_x = 0
active_intervals = []
for x, y1, y2, typ in events:
# 计算之前的面积
if active_intervals and x > prev_x:
y_coverage = calculate_y_coverage(active_intervals)
total_area += y_coverage * (x - prev_x)
# 更新活动区间
if typ == 1:
active_intervals.append((y1, y2))
else:
active_intervals.remove((y1, y2))
prev_x = x
return total_area
实例分析:#149 直线上最多的点数
题目:给定平面上的n个点,求最多有多少个点在同一条直线上。
def maxPoints(points):
"""
关键:用斜率来表示直线,但要处理精度问题
技巧:用最简分数表示斜率,避免浮点数误差
"""
if len(points) <= 2:
return len(points)
def gcd(a, b):
while b:
a, b = b, a % b
return a
max_count = 0
for i in range(len(points)):
slopes = {}
duplicate = 0
local_max = 0
for j in range(i + 1, len(points)):
dx = points[j][0] - points[i][0]
dy = points[j][1] - points[i][1]
if dx == 0 and dy == 0:
# 重复点
duplicate += 1
continue
# 计算最简分数形式的斜率
g = gcd(dx, dy)
dx, dy = dx // g, dy // g
# 统一符号:保证dx为正或(dx=0时dy为正)
if dx < 0:
dx, dy = -dx, -dy
elif dx == 0:
dy = abs(dy)
slope = (dx, dy)
slopes[slope] = slopes.get(slope, 0) + 1
local_max = max(local_max, slopes[slope])
max_count = max(max_count, local_max + duplicate + 1)
return max_count
实例分析:#223 矩形面积
题目:给定两个矩形的坐标,计算它们覆盖的总面积。
def computeArea(ax1, ay1, ax2, ay2, bx1, by1, bx2, by2):
"""
计算两个矩形覆盖的总面积
总面积 = 矩形1面积 + 矩形2面积 - 重叠面积
"""
# 两个矩形的面积
area1 = (ax2 - ax1) * (ay2 - ay1)
area2 = (bx2 - bx1) * (by2 - by1)
# 计算重叠部分
# 重叠矩形的左下角
overlap_x1 = max(ax1, bx1)
overlap_y1 = max(ay1, by1)
# 重叠矩形的右上角
overlap_x2 = min(ax2, bx2)
overlap_y2 = min(ay2, by2)
# 计算重叠面积
overlap_area = 0
if overlap_x2 > overlap_x1 and overlap_y2 > overlap_y1:
overlap_area = (overlap_x2 - overlap_x1) * (overlap_y2 - overlap_y1)
return area1 + area2 - overlap_area
本章小结
数学与位运算技巧是算法优化的利器。通过本章的学习,我们掌握了:
位运算核心技巧
- 异或运算:利用自反性解决配对问题
- 位掩码:用二进制位表示集合状态,实现高效的集合操作
- 位操作技巧:清除最低位、判断2的幂等常用操作
数论基础工具
- 欧几里得算法:高效计算最大公约数
- 模运算性质:防止溢出,处理循环
- 快速幂:对数时间计算幂运算
组合数学方法
- 排列组合公式:解决计数问题的基础
- 康托展开:排列与序号的双向映射
- 卡特兰数:识别经典的递归计数模式
几何算法精华
- 叉积应用:判断方向、计算面积
- 凸包算法:Graham扫描的单调栈思想
- 扫描线技术:处理区间和矩形问题
关键公式汇总:
- 异或性质:$a \oplus a = 0$,$a \oplus 0 = a$
- 组合数递推:$\binom{n}{k} = \binom{n-1}{k-1} + \binom{n-1}{k}$
- 卡特兰数:$C_n = \frac{1}{n+1}\binom{2n}{n}$
- 叉积几何意义:$\vec{a} \times \vec{b} = |a||b|\sin\theta$
常见陷阱与错误
位运算优先级陷阱
# 错误:位运算优先级低于比较运算符
if n & 1 == 0: # 实际是 n & (1 == 0)
pass
# 正确:加括号明确优先级
if (n & 1) == 0:
pass
整数溢出处理
# Python 自动处理大整数,但其他语言需要注意
# C++/Java 中计算组合数容易溢出
# 避免溢出的技巧:边乘边除
def combination_safe(n, k):
result = 1
for i in range(k):
result = result * (n - i) // (i + 1) # 先乘后除
return result
浮点数精度问题
# 错误:直接比较浮点数
if slope1 == slope2: # 可能因精度问题失败
pass
# 正确:使用整数表示(如最简分数)
slope1 = (dx1 // gcd1, dy1 // gcd1)
slope2 = (dx2 // gcd2, dy2 // gcd2)
if slope1 == slope2:
pass
# 或使用误差容忍
EPSILON = 1e-9
if abs(slope1 - slope2) < EPSILON:
pass
边界条件处理
# 负数的位运算
# Python: -1 >> 1 = -1 (算术右移)
# 需要特别处理符号位
# 模运算的负数处理
def mod_safe(a, m):
return ((a % m) + m) % m # 确保结果非负
# 几何算法的特殊点
# 重复点、共线点、退化情况都需要特别处理
算法选择误区
- 过度优化:不是所有问题都需要位运算,可读性也很重要
- 精度 vs 效率:几何问题中,整数运算比浮点数更可靠
- 空间换时间:预计算(如阶乘表)可以显著提升性能
记住:数学是算法的灵魂。当你遇到看似复杂的问题时,不妨先寻找其中的数学规律,往往能找到简洁优雅的解法。