但是这个算法最近又遇到了效率问题。
还是时间效率问题
最近文档比对测试的时候,同事传了两个一百多页的文档进行比对,然后反馈说总是比对失败,系统的同事反馈说是引擎出问题了。
那究竟是引擎哪里出问题了呢?于是又打开测试模型,观察日志输出,发现在页面配对的时候,就一直停在那里,没有继续往下输出。观察cpu和内存占用情况,有一个cpu100%,内存占用没有明显增加,可以大致判断是不是死循环了,或者有一个计算量非常大的东西正在跑。开始时,并没有意识到是配对算法本身的效率问题,毕竟这个算法前不久还专门优化过,空耗了大半天的时间。后来实在没辙了,就调戏页面匹配算法,结果还真就是这个算法的问题。
即使我们在算法的第二个进行了相当部分的剪枝,但是只要匹配的元素比较多,计算量还是非常大的,看上去就像是假死了一样。
算法优化版本V3
既然知道了问题,那就想办法解决。
在我们的场景中,两边元素其实基本上是可以匹配上的,不匹配的元素只是少数,那我们是否可以用分治的思路来解决呢?虽然总的元素比较多,但是如果可以划分成小块,那计算量将大幅下降。
结合到我们的场景,如果我们能预先将一些比较确定的元素先匹配上,那不确定的部分就很可能会被分成几个小区间,如下图:

左右两边相同颜色的部分就是两个混乱的区域(待匹配的区域),其他的区域就是已经匹配好的区域。混乱的区域是对应的,直接输入到我们的V2版算法中就行,那现在问题的关键就是找到对应的混乱区域。
V3版算法步骤与实现
找到混乱区域的算法步骤:
梳理清楚步骤之后,实现其实就不太难了:
def match(self, min_score=0.2, sort_min_score=0.01):
"""快速配对算法(类似贪心算法)
:return items: numpy.ndarray: [(idx1, idx2)]
"""
scores = self.scores
len1, len2 = scores.shape
if len1 < 1 or len2 < 1:
return np.array([])
if len1 == 1 and len2 == 1:
return np.array([[0, 0]])
is_T = False # 是否转置
if len1 > len2:
is_T = True
scores = scores.T # 转置之后,保证行数是比较少的
# 每行最大值的索引
max_index = np.argmax(scores, axis=1)
eq_index = max_index == np.sort(max_index)
len_index = len(max_index)
if all(eq_index) and len_index == len(set(max_index)):
# 顺序一致且每个值都不同
data = [[i, v] for i, v in enumerate(max_index) if scores[i, v] >= sort_min_score]
if is_T:
data = [[v, i] for i, v in data]
return np.array(data)
# 顺序不一致或者存在相同的值
eq_index = np.zeros((len_index,), dtype=bool)
min_arr, max_arr = self.get_min_max(max_index)
for i in range(len_index):
# 比前面的值都大,比后面的值都小
if i > 0:
if max_index[i] <= max_arr[i-1]:
eq_index[i] = True
continue
if i < len_index-1:
if max_index[i] >= min_arr[i+1]:
eq_index[i] = True
data = []
is_true = False
# self.min_score = min_score
for (i, v), flag in zip(enumerate(max_index), eq_index):
if not flag:
is_true = False
if scores[i, v] >= min_score:
data.append([i, v])
if v == scores.shape[1]: # 已经是最大的值
break
continue
if is_true:
continue
# 找到连续的True块
is_true = True
true_len = 0
for j in range(i, len_index):
if eq_index[j]:
true_len += 1
else:
break
# 在小区域[min_i:max_i, min_j:max_j]内进行匹配
min_j = 0 if len(data) == 0 else data[-1][1]+1
max_j = scores.shape[1] if i+true_len >= len_index else max_index[i+true_len]
min_i, max_i = i, i+true_len
self.scores = scores[min_i:max_i, min_j:max_j].copy()
tmp_data = self.match_old(min_score=min_score)
if debug:
print("tmp data: ", tmp_data)
for tmp_i, tmp_v in tmp_data:
data.append([tmp_i+min_i, tmp_v+min_j])
if is_T:
data = [[v, i] for i, v in data]
return np.array(data)后续
其实,如果仔细想,这个算法还是有漏洞的,例如临时匹配是否应该先将一些得分低的过滤掉呢?这个需要更多的测试。
总体上,对这整个算法的设计还是挺满意的,效果也很好,原来算法的时间复杂度应该是指数级的,现在更加接近线性级。