分治法是一種非常通用的算法設(shè)計(jì)技巧. 在很多實(shí)際問(wèn)題中, 相比直接求解, 分治法往往能顯著降低算法的計(jì)算復(fù)雜度. 常見(jiàn)的可以用分治法求解的問(wèn)題有: 排序, 矩陣乘法, 整數(shù)乘法, 離散傅里葉變換等. 分治法的一般思路如下:
- [Divide] 把原問(wèn)題拆分成同類型的子問(wèn)題.
- [Conquer] 用遞歸的方式求解子問(wèn)題.
- [Combine] 根據(jù)子問(wèn)題的解構(gòu)造原問(wèn)題的解.
分治法最關(guān)鍵的步驟是如何 低成本地 利用子問(wèn)題的解構(gòu)來(lái)造原問(wèn)題的解. 它包含兩個(gè)方面: 1. 可行性, 即, 可以用子問(wèn)題的解來(lái)構(gòu)造原問(wèn)題的解; 2. 高效性, 即構(gòu)造原問(wèn)題解的時(shí)間復(fù)雜度較低. 換句話說(shuō), 分治法需要比直接求解效率高. 分治法一般是通過(guò)遞歸求解子問(wèn)題, 其時(shí)間復(fù)雜度的分析需要用到如下定理.
Master Theorem[1]. 考慮遞歸式
. 當(dāng)
,
時(shí), 我們有
Counting Inversions[2]
在推薦場(chǎng)景中, 一個(gè)常用的方法是協(xié)同過(guò)濾(Collaborative Filtering), 即為相似的用戶推薦它們共同喜歡的事物. 以推薦歌曲為例, 我們把用戶和
對(duì)歌曲的偏好分別進(jìn)行排序, 然后計(jì)算有多少首歌在
和
的排序是"不同的". 最后根據(jù)這種不同來(lái)定義用戶
和
的相似性, 從而進(jìn)行歌曲推薦. 具體來(lái)說(shuō), 對(duì)用戶
對(duì)歌曲的偏好按
編號(hào). 用戶
對(duì)歌曲的偏好可以表示為
對(duì)任意的, 如果
, 我們稱
和
稱為 反序(inversion). 換句話說(shuō), 用戶A把歌曲
排在歌曲
的前面, 但是用戶B把歌曲
排在了歌曲
的前面.
問(wèn)題描述
給定無(wú)重復(fù)數(shù)字的整數(shù)序列, 計(jì)算其反序的數(shù)量.
算法設(shè)計(jì)
直接求解的思路是考慮所有的二元組并判斷它們是否反序. 這個(gè)算法的時(shí)間復(fù)雜度是
. 下面我們用分治法來(lái)降低計(jì)算復(fù)雜度.
注意到這個(gè)問(wèn)題實(shí)際上與排序非常類似. 通過(guò)對(duì)序列進(jìn)行排序的同時(shí)記錄不滿足"順序"的二元組數(shù)量, 即反序的數(shù)量. 令. 把序列分成兩部分:
用遞歸的方式對(duì)left和right進(jìn)行排序, 同時(shí)計(jì)算left和right中的反序數(shù)量. 當(dāng)序列的長(zhǎng)度為1時(shí), 返回0. 下一步是合并子問(wèn)題的解. 注意到left和right已經(jīng)是按照從小到大的順序進(jìn)行排列. 比較left和right的第一個(gè)元素并把較小的元素添加到結(jié)果中直到left或right為空, 最后再把剩余的序列添加到結(jié)果集. 在比較過(guò)程中我們需要記錄反序的數(shù)量. 當(dāng)right中的元素小于left中的元素時(shí), 反序的增量為"left中剩余元素的數(shù)量". 最終的結(jié)果包含三部分之和: left中反序的數(shù)量, rihgt中反序的數(shù)量和合并時(shí)反序的數(shù)量.
Python實(shí)現(xiàn)
整體的計(jì)算過(guò)程.
def sort_and_count(x):
if len(x) == 1:
return x, 0
k = len(x) // 2
left, count_left = sort_and_count(x[0: k])
right, count_right = sort_and_count(x[k:])
# 把子問(wèn)題的解拼接成原問(wèn)題的解
combined, count = merge_and_count(left, right)
return combined, count + count_left + count_right
歸并過(guò)程.
def merge_and_count(left, right):
""" 把left和right合并且計(jì)算inversion的數(shù)量
注意: left和right已經(jīng)排好序
"""
combined = []
count = 0
while len(left) and len(right):
if left[0] > right[0]: # 反序(左邊的編號(hào)小于右邊的編號(hào)是正序)
combined.append(right.pop(0))
count += len(left)
else: # 正序
combined.append(left.pop(0))
return combined + left + right, count
計(jì)算復(fù)雜度
容易分析歸并過(guò)程的時(shí)間復(fù)雜度是. 令
代表算法的時(shí)間復(fù)雜度, 我們有
根據(jù)Master Theorem, 我們得到.
Closest Pair[2]
Closest Pair是計(jì)算幾何里的一個(gè)基本問(wèn)題: 給定二維平面上的個(gè)點(diǎn), 找到距離最近的兩個(gè)點(diǎn). 通過(guò)計(jì)算任意兩點(diǎn)的距離可以在
找到距離最近的兩點(diǎn). 下面利用分治法可以把時(shí)間復(fù)雜度降低到
.
算法設(shè)計(jì)
如果所有點(diǎn)是一維的, 我們可以把它們排序, 然后計(jì)算所有相鄰兩點(diǎn)的最小距離. 排序耗時(shí), 計(jì)算相鄰點(diǎn)的最小距離耗時(shí)
, 因此算法的時(shí)間復(fù)雜度為
. 在二維情形, 我們的思路是類似的:
沿著
軸方向?qū)c(diǎn)集
進(jìn)行排序得到
.
把
按與
軸垂的方向均分成兩部分
和
:
-
遞歸地求解
和
中的closest pair(如下圖所示).
image 根據(jù)
和
的計(jì)算結(jié)果構(gòu)造原問(wèn)題的解(見(jiàn)下文).
合并(Combine)
設(shè),
分別是
和
中的closest pair. 如果
的closest pair在
或
中, 我們只需要從
和
選擇距離小的pair作為結(jié)果輸出. 否則
的closest pair其中一點(diǎn)在
中, 另一點(diǎn)在
中, 這時(shí)我們需要比較
和
中的點(diǎn). 這樣一來(lái), 合并的時(shí)間復(fù)雜度為
! 接下來(lái)我們要把合并的時(shí)間復(fù)雜度降低為
.
令, 其中
代表
,
兩點(diǎn)之間的距離. 設(shè)
代表
和
的分割線. 如果存在
,
使得
, 那么
和
在
軸方向距離
一定不超過(guò)
. 令
, 因此
. 如下圖所示,
中的點(diǎn)在藍(lán)色虛線之間.
[圖片上傳失敗...(image-facf8c-1586865336187)]
把中的點(diǎn)按
軸從小到大排序, 得到集合
, 其中
是一個(gè)二元組(代表它在平面中的位置). 我們有如下定理(稍后給出證明):
定理 如果存在
滿足
, 那么
.
這樣一來(lái), 我們可以在的時(shí)間內(nèi)找到所有距離不超過(guò)
的點(diǎn)對(duì), 并記錄距離最小的點(diǎn)對(duì)作為結(jié)果輸出(如果存在). 思路思路如下:
pairs_within_delta = [] # S中距離不超過(guò)delta的點(diǎn)的集合
for s in Sy:
for t in 15 points after s:
if d(s, t) < delta:
add (s,t) to pairs_within_delta
output the minimum distance pair in pairs_within_delta
求解子問(wèn)題和
之前, 首先把
根據(jù)
軸從小到大排序得到
, 這樣一來(lái)可以在
時(shí)間內(nèi)構(gòu)造
, 即依次過(guò)濾掉
中距離
超過(guò)
的點(diǎn). 在上述算法中, 外層循環(huán)次數(shù)是
, 內(nèi)層循環(huán)是常數(shù), 因此在合并步驟中構(gòu)造closest pair的時(shí)間復(fù)雜度最終降低為
.
Python實(shí)現(xiàn)
先把輸入點(diǎn)集分別按
軸和
軸排序, 得到
和
. 遞歸求解的過(guò)程參考函數(shù)
closest_pair_xy
.
def closest_pair(points):
""" 計(jì)算二維點(diǎn)集中的closest pair.
:param points: P = [(x1,y1), (x2,y2), ..., (xn, yn)]
:return: 兩個(gè)距離最近的點(diǎn)
"""
# 把P按x軸和y軸分別進(jìn)行排序, 得到Px和Py
# 注意: P, Px, Py 三個(gè)集合是相同的(僅僅排序不同)
Px = sorted(points, key=lambda item: item[0])
Py = sorted(points, key=lambda item: item[1])
return closest_pair_xy(Px, Py)
def closest_pair_xy(Px, Py):
""" 計(jì)算closest pair
:param Px: 把points按x軸升序排列
:param Py: 把points按y軸升序排列
:return: point1, point2
"""
if len(Px) <= 3:
return search_closest_pair(Px)
# 構(gòu)造子問(wèn)題的輸入: Qx, Rx, Qy, Ry
k = len(Px) // 2
Q, R = Px[0: k], Px[k:]
Qx, Qy = sorted(Q, key=lambda x: x[0]), sorted(Q, key=lambda x: x[1])
Rx, Ry = sorted(R, key=lambda x: x[0]), sorted(R, key=lambda x: x[1])
# 求解子問(wèn)題
q0, q1 = closest_pair_xy(Qx, Qy)
r0, r1 = closest_pair_xy(Rx, Ry)
# 合并子問(wèn)題的解
return combine_results_of_sub_problems(Py, Qx, q0, q1, r0, r1)
def search_closest_pair(points):
""" 用枚舉的方式尋找closest pair
:param points: [(x1,y1), (x2,y2), ...]
:return: closest pair
"""
n = len(points)
dist_min, i_min, j_min = math.inf, 0, 0
for i in range(n-1):
for j in range(i+1, n):
d = get_dist(points[i], points[j])
if d < dist_min:
dist_min, i_min, j_min = d, i, j
return points[i_min], points[j_min]
def get_dist(a, b):
""" 計(jì)算兩點(diǎn)之間的歐式距離
"""
return math.sqrt(math.pow(a[0]-b[0], 2) + math.pow(a[1]-b[1], 2))
設(shè)代表
closest_pair_xy
的計(jì)算時(shí)間. 根據(jù)前文分析, 合并子問(wèn)題的解并輸出原問(wèn)題的解的時(shí)間復(fù)雜度為, 因此我們有
根據(jù)Master Theorem, 我們有. 此外, 把
分別按
軸排序的時(shí)間復(fù)雜度為
, 因此算法整體的時(shí)間復(fù)雜度是
.
下面是合并過(guò)程的實(shí)現(xiàn).
def combine_results_of_sub_problems(Py, Qx, q0, q1, r0, r1):
"""
:param Py: P按y軸排序的結(jié)果
:param Qx: P在x=x0處被切分成Q和R. Qx是Q按x軸排序的結(jié)果
:param q0: (q0, q1)是Q中的closest pair
:param q1: 參考q0
:param r0: (r0, r1)是R中的closest pair
:param r1: 參考r0
:return: closest pair in P
"""
# 計(jì)算Sy
d = min(get_dist(q0, q1), get_dist(r0, r1))
Sy = get_sy(Py, Qx, d)
# 檢查是否存在距離更小的pair
s1, s2 = closest_pair_of_sy(Sy)
if s1 and s2 and get_dist(s1, s2) < d:
return s1, s2
elif get_dist(q0, q1) < get_dist(r0, r1):
return q0, q1
else:
return r0, r1
def get_sy(Py, Qx, d):
""" 根據(jù)Py計(jì)算Sy.
:param Py: P按y軸排序的結(jié)果
:param Qx: P在x=x0處被切分成Q和R. Qx是Q按x軸排序的結(jié)果
:param d: delta
:return: S
"""
x0 = Qx[-1][0] # Q最右邊點(diǎn)的x坐標(biāo)值
return [p for p in Py if p[0] - x0 < d]
def closest_pair_of_sy(Sy):
""" 計(jì)算集合Sy的closest pair
"""
n = len(Sy)
if n <= 1:
return None, None
dist_min, i_min, j_min = math.inf, 0, 0
for i in range(n-1):
for j in range(i + 1, i + 16):
if j == len(Sy):
break
d = get_dist(Sy[i], Sy[j])
if d < dist_min:
dist_min, i_min, j_min = d, i, j
return Sy[i_min], Sy[j_min]
定理證明
定理 如果存在
滿足
, 那么
.
根據(jù)前文的描述, 已知中的點(diǎn)在下圖藍(lán)色虛線之間. 把
中的點(diǎn)按
軸從小到大排序得到
, 其中
代表平面中的一個(gè)點(diǎn). 為了方便描述, 我們把下圖中藍(lán)色虛線之間用單位長(zhǎng)度為
的網(wǎng)格劃分.
[圖片上傳失敗...(image-dad441-1586865336187)]
假設(shè)存在使得
. 我們要證明
. 證明分為兩步:
-
和
必須在不同的網(wǎng)格中. 反證法. 假設(shè)
在同一個(gè)網(wǎng)格中(意味著
or
), 它們的距離
. 注意
是
和
中的最短距離, 因此矛盾.
-
. 反證法. 假設(shè)
. 如上圖所示
和
之間至少相差3行(網(wǎng)格). 因此
, 矛盾.