从一道 leetcode 题到离散对数求解算法
我们先来看题面,点进去之前可以先思考一下:
Given an array of integers
nums
containingintegers where each integer is in the range inclusive, . There is only one repeated number in
nums
, return this repeated number.You must solve the problem without modifying the array and using only constant extra space.
算法题解答
如果不看第三行,那么任意nums
,也不能新开数组,那就只能考虑双指针解法了。
一个比较容易想到的做法是 binary search,既然值域在 num
中
这里使用反证法,记
为当“condition”为真时值为 ,否则为 。 假设
之间存在重复元素且 。因为重复元素只有一个,因此 这些元素只会存在 个或者 个。因此 ,两式相加可得 ,矛盾! 因此假设不成立,原结论正确,
之间没有重复元素。
有了上述的结论,我们可以断定当
以上过程递归进行,总时间复杂度为
def findDuplicate(self, nums: List[int]) -> int:
low, high = 1, len(nums) - 1
while low < high:
mid = (low + high) // 2
count = 0
for num in nums:
if num <= mid:
count += 1
if count > mid:
high = mid
else:
low = mid + 1
return low
Floyd's cycle-finding algorithm
但题面中还有一个 follow up:
Can you solve the problem in linear runtime complexity?
这便是 binary search 不能触及到的领域了。这里我选择直接放弃,然后知道了一个名为 Floyd's cycle-finding 的算法,它也有一个别名叫 tortoise and the hare algorithm:算法过程如下:
首先根据 nums
数组建图,数组的 index 是一个端点(这里 index 从 1 开始),而这个 index 对应的值则是这个端点指向的端点。例如以下数组:
[5,3,1,6,2,5]
对应了以下这个有向图:

首先 hare 和 tortoise 随机选一个点同时出发(为了演示方便,这里选择 4 号点),此时 hare 跑得快些,一次跑两步,tortoise 速度慢些,一次爬一步。于是便会有这样的情景(左 hare,右 tortoise):
4 4
5 6
3 5
5 2
3 3
这个时候 hare 与 tortoise 正式相遇了,这说明算法成功找到了一个环(不然 hare 只会永远在 tortoise 前面)。
然后算法的下一步为了找到环的“起始点”(也就是点 5。如果他们一开始就在环里那自然就是起点了)。将 tortoise 放回了起点 4,hare 保持原先位置 3 不动,随后它们每次走一步:
3 4
1 6
5 5
可以发现它们在环的“起始点” 5 处相遇了。可以证明,无论图的形状如何,它们始终都会在环的“起始点”处相遇:
设环的周长为
,尾巴(也就是图中 5->6->4 的长度为 )当他们第一次相遇时,hare 走了 步,tortoise 走了 步,显然我们有 。而当 tortoise 被移回起点时,由于此时它们速度一样,hare 和 tortoise 的距离就恒为 。当 tortoise 走了 步到了“起始点”时,hare 走了 步,由于 ,因此 hare 和 tortoise 必相遇,并且都在“起始点”。
算法的最后一步就是 hare 不动,tortoise 绕一圈测出周长
回到本题,由于重复的数字只有一个,所以肯定是“起始点”本身,因此我们只需要做算法的前两步即可 AC。时间复杂度
def findDuplicate(self, nums: List[int]) -> int:
slow = nums[0]
fast = nums[0]
while True:
slow = nums[slow]
fast = nums[nums[fast]]
if slow == fast:
break
fast = nums[0]
while slow != fast:
slow = nums[slow]
fast = nums[fast]
return fast
Brent's algorithm
Brent 在 Floyd 的探圈算法基础上进行了改进,使用了 2 的幂次步长power
来检测循环。并引入了lambda
(每次迭代自增,可以理解为周长mu
(自增,最后得出尾巴
- 直接在第一步找到了环的周长
。 - 每一次迭代只需要计算一次
,而不是 floyd 的三次。
举一个例子,如果图长这样(假设它们都从 1 开始):
1 -> 2 -> 3 -> 4 -> 5 -> 3
然后我们有:
- Frame 1: Tortoise at 1, Hare at 2,
power = 1
,lambda = 1
. - Frame 2: Tortoise at 2, Hare at 3,
power = 2
,lambda = 1
. - Frame 3: Tortoise at 2, Hare at 4,
power = 2
,lambda = 2
. - Frame 4: Tortoise at 4, Hare at 5,
power = 4
,lambda = 1
. - Frame 5: Tortoise at 4, Hare at 3,
power = 4
,lambda = 2
. - Frame 6: Tortoise at 4, Hare at 4,
power = 4
,lambda = 3
(cycle length detected). - Frame 7: Tortoise at 1, Hare at 1 (reset for finding position of length
lambda
). - Frame 8: Tortoise at 1, Hare at 2.
- Frame 9: Tortoise at 1, Hare at 3.
- Frame 10: Tortoise at 1, Hare at 4. (repeated
lambda
times) - Frame 11: Tortoise at 2, Hare at 5,
mu = 1
. - Frame 12: Tortoise at 3, Hare at 3,
mu = 2
. (cycle start found)
python 代码如下:
def brent(f, x0) -> (int, int):
"""Brent's cycle detection algorithm."""
# main phase: search successive powers of two
power = lam = 1
tortoise = x0
hare = f(x0) # f(x0) is the element/node next to x0.
# this assumes there is a cycle; otherwise this loop won't terminate
while tortoise != hare:
if power == lam: # time to start a new power of two?
tortoise = hare
power *= 2
lam = 0
hare = f(hare)
lam += 1
# Find the position of the first repetition of length λ
tortoise = hare = x0
for i in range(lam):
# range(lam) produces a list with the values 0, 1, ... , lam-1
hare = f(hare)
# The distance between the hare and tortoise is now λ.
# Next, the hare and tortoise move at same speed until they agree
mu = 0
while tortoise != hare:
tortoise = f(tortoise)
hare = f(hare)
mu += 1
return lam, mu
pollard_rho
由于这个建图很容易让人想起
但我们事先不知道
那么我们如何来遍历这里的
这个算法便称为 pollard_rho 素性检测,因为
from math import *
def f(x, c, n):
return (x * x + c) % n
def pollard_rho(n, max_attempts=10):
if n % 2 == 0:
return 2 # tackle even case
for attempt in range(max_attempts):
x = random.randint(1, n-1) # tortoise
y = x # hare
c = random.randint(1, n-1) # c of f(x)
g = 1 # GCD result
# Floyd's cycle-finding
while g == 1:
x = f(x, c, n) # tortoise takes 1 step
y = f(f(y, c, n), c, n) # hare takes 2 steps
g = gcd(abs(x - y), n) # calculate gcd
if 1 < g < n:
return g # non-trivial divisor
# retry
return n # max retries exceeded
pollard_rho using Brent
为了水一篇论文,Brent 也使用了自己的算法替换了 pollard_rho 中的 Floyd 的探圈方法,并用实验证明了他的探圈方法效率比 Floyd 提高了 36%,从而导致总共的运行效率提高了 24%。
这里就不贴代码了,一种可能的实现参见:
https://comeoncodeon.wordpress.com/2010/09/18/pollard-rho-brent-integer-factorization/
pollard_rho for DLP
就像将质因数分解问题联想到 DLP(离散对数问题)那样自然一样,除了在有限环
我们假设需要求解离散对数问题
根据欧拉定理,我们有:
就可以通过 exgcd 求出对应的
而对于如何找到这样的
这里我们使用这个公式的变种:
然后设初始值 tortoise 为
以下是
i x a b X A B
------------------------------
1 2 1 0 10 1 1
2 10 1 1 100 2 2
3 20 2 1 1000 3 3
4 100 2 2 425 8 6
5 200 3 2 436 16 14
6 1000 3 3 284 17 15
7 981 4 3 986 17 17
8 425 8 6 194 17 19
..............................
48 224 680 376 86 299 412
49 101 680 377 860 300 413
50 505 680 378 101 300 415
51 1010 681 378 1010 301 416
可以看到当
于是我们就在约
DLP 与 pohlig-hellman 算法结合
pohlig-hellman 算法也可以用于高效求解 DLP 问题,主要思路将
若知道 DLP 中
因此综上所述 DLP 这个问题取决于
参考资料
An Introduction to Mathematical Cryptography
https://www.youtube.com/watch?v=pKO9UjSeLew
https://www.ruanx.net/pohlig-hellman/
https://en.wikipedia.org/wiki/Pollard's_rho_algorithm_for_logarithms
https://en.wikipedia.org/wiki/Pollard's_rho_algorithm
https://en.wikipedia.org/wiki/Pohlig–Hellman_algorithm
https://en.wikipedia.org/wiki/Cycle_detection#Brent.27s_algorithm
https://comeoncodeon.wordpress.com/2010/09/18/pollard-rho-brent-integer-factorization/