【问题标题】:Find two pairs of pairs that sum to the same value找到两对总和相同的对
【发布时间】:2014-02-03 03:11:59
【问题描述】:

我有我使用的随机二维数组

import numpy as np
from itertools import combinations
n = 50
A = np.random.randint(2, size=(n,n))

我想确定矩阵是否有两对行对,它们总和为相同的行向量。我正在寻找一种快速的方法来做到这一点。我目前的方法只是尝试所有可能性。

for pair in  combinations(combinations(range(n), 2), 2):
    if (np.array_equal(A[pair[0][0]] + A[pair[0][1]], A[pair[1][0]] + A[pair[1][1]] )):
        print "Pair found", pair

适用于n = 100 的方法会非常棒。

【问题讨论】:

  • 根据下面的答案,您能否阐明您想要实现的目标是什么?从您的代码来看,您似乎是在 pairs of pairs 总和等于同一行的行之后,而不是“两对总和为相同值的行”。
  • @richsilv 是的,没错。我的意思是成对的行。
  • 对,请看下面我的回答。
  • 1+1=2?标量和是多少?
  • @cyborg 1+1=2...是的 :)

标签: python performance algorithm numpy


【解决方案1】:

这是一个纯粹的 numpy 解决方案;没有很长的时间,但我必须将 n 推到 500 才能看到我的光标在它完成之前闪烁一次。虽然它是内存密集型的,并且由于更大的 n 的内存需求而将失败。不管怎样,我的直觉是,找到这样一个向量的几率会随着 n 的增大而呈几何下降。

import numpy as np

n = 100
A = np.random.randint(2, size=(n,n)).astype(np.int8)

def base3(a):
    """
    pack the last axis of an array in base 3
    40 base 3 numbers per uint64
    """
    S = np.array_split(a, a.shape[-1]//40+1, axis=-1)
    R = np.zeros(shape=a.shape[:-1]+(len(S),), dtype = np.uint64)
    for i in xrange(len(S)):
        s = S[i]
        r = R[...,i]
        for j in xrange(s.shape[-1]):
            r *= 3
            r += s[...,j]
    return R

def unique_count(a):
    """returns counts of unique elements"""
    unique, inverse = np.unique(a, return_inverse=True)
    count = np.zeros(len(unique), np.int)
    np.add.at(count, inverse, 1)
    return unique, count

def voidview(arr):
    """view the last axis of an array as a void object. can be used as a faster form of lexsort"""
    return np.ascontiguousarray(arr).view(np.dtype((np.void, arr.dtype.itemsize * arr.shape[-1]))).reshape(arr.shape[:-1])

def has_pairs_of_pairs(A):
    #optional; convert rows to base 3
    A = base3(A)
    #precompute sums over a lower triangular set of all combinations
    rowsums = sum(A[I] for I in np.tril_indices(n,-1))
    #count the number of times each row occurs by sorting
    #note that this is not quite O(n log n), since the cost of handling each row is also a function of n
    unique, count = unique_count(voidview(rowsums))
    #print if any pairs of pairs exist;
    #computing their indices is left as an excercise for the reader
    return np.any(count>1)

from time import clock
t = clock()
for i in xrange(100):
    print has_pairs_of_pairs(A)
print clock()-t

编辑:包括 base-3 包装;现在 n=2000 是可行的,大约需要 2gb 的内存,以及几秒钟的处理时间

编辑:添加了一些时间;在我的 i7m 上,每次调用 n=100 只需要 5ms。

【讨论】:

  • 注意;它明显快于 n=500 的 findMatchSets 解决方案
  • +1 你打败了我:numpy 非常适合暴力破解!一旦遇到内存问题,您可以通过考虑每 40 列以 3 为底的数字并将其值存储在 uint64s 的数组中,将数组的列数减少 40 倍。你会将你的 500x500x500 数组变成only 500x500x13...
  • 我只是添加 .astype(np.int8);现在 n=1000 也是可行的,最大使用 ~1.5gb,即使它需要几秒钟。但是你是对的,如果向量始终是二进制的,那么尽可能紧密地打包位会得到更多回报。
  • bincount 需要非负整数;传递给 unique_count 的类型是一个 void 对象;唯一的要求是它是可排序的。此外,bincount 只有在有限的支持下才有效。即使您以某种方式将整行视为一个巨大的整数而不是 void 对象,bincount 也会窒息。
  • 注意;通过为每个行和计算一个不一定唯一但很短的散列,并首先检查该空间中的冲突,性能可能会进一步显着提高。这将产生算法上的最佳性能。
【解决方案2】:

根据您问题中的代码,并假设您实际上正在寻找总和等于相同行向量的行对,您可以执行以下操作:

def findMatchSets(A):
   B = A.transpose()
   pairs = tuple(combinations(range(len(A[0])), 2))
   matchSets = [[i for i in pairs if B[0][i[0]] + B[0][i[1]] == z] for z in range(3)]
   for c in range(1, len(A[0])):
      matchSets = [[i for i in block if B[c][i[0]] + B[c][i[1]] == z] for z in range(3) for block in matchSets]
      matchSets = [block for block in matchSets if len(block) > 1]
      if not matchSets:
         return []
   return matchSets

这基本上将矩阵分层为等价集,在考虑一列之后总和为相同的值,然后是两列,然后是三列,依此类推,直到它到达最后一列或没有等价集剩下一个以上的成员(即没有这样的一对)。这适用于 100x100 数组,主要是因为当 n 很大时,两对行求和到同一行向量的机会非常小(与 3^n 可能的向量总和相比,n*(n+1)/2 组合) .

更新

更新的代码允许根据要求搜索所有行的 n 大小子集对。根据原始问题,默认值为 n=2:

def findMatchSets(A, n=2):
   B = A.transpose()
   pairs = tuple(combinations(range(len(A[0])), n))
   matchSets = [[i for i in pairs if sum([B[0][i[j]] for j in range(n)]) == z] for z in range(n + 1)]
   for c in range(1, len(A[0])):
      matchSets = [[i for i in block if sum([B[c][i[j]] for j in range(n)]) == z] for z in range(n + 1) for block in matchSets]
      matchSets = [block for block in matchSets if len(block) > 1]
      if not matchSets:
      return []
   return matchSets

【讨论】:

  • 公平点,只需要确保它在返回之前再次检查等价集大小(对于大 n 几乎不是问题)。上面的代码已更正。
  • 谢谢。您认为这段代码能否推广到总和为相同行向量的成对的行?
【解决方案3】:

这是一种“惰性”方法,可扩展到 n=10000,使用“仅”4gb 内存,每次调用在 10 秒左右完成。最坏情况复杂度为 O(n^3),但对于随机数据,预期性能为 O(n^2)。乍一看,您似乎需要 O(n^3) 次操作;每行组合至少需要生产和检查一次。但是我们不需要查看整行。相反,我们可以对行对的比较执行早期退出策略,一旦明确它们对我们没有用处;对于随机数据,我们可能会在考虑一行中的所有列之前很久就得出这个结论。

import numpy as np

n = 10
#also works for non-square A
A = np.random.randint(2, size=(n*2,n)).astype(np.int8)
#force the inclusion of some hits, to keep our algorithm on its toes
##A[0] = A[1]


def base_pack_lazy(a, base, dtype=np.uint64):
    """
    pack the last axis of an array as minimal base representation
    lazily yields packed columns of the original matrix
    """
    a = np.ascontiguousarray( np.rollaxis(a, -1))
    init = np.zeros(a.shape[1:], dtype)
    packing = int(np.dtype(dtype).itemsize * 8 / (float(base) / 2))
    for columns in np.array_split(a, (len(a)-1)//packing+1):
        yield reduce(
            lambda acc,inc: acc*base+inc,
            columns,
            init)

def unique_count(a):
    """returns counts of unique elements"""
    unique, inverse = np.unique(a, return_inverse=True)
    count = np.zeros(len(unique), np.int)
    np.add.at(count, inverse, 1)        #note; this scatter operation requires numpy 1.8; use a sparse matrix otherwise!
    return unique, count, inverse

def has_identical_row_sums_lazy(A, combinations_index):
    """
    compute the existence of combinations of rows summing to the same vector,
    given an nxm matrix A and an index matrix specifying all combinations

    naively, we need to compute the sum of each row combination at least once, giving n^3 computations
    however, this isnt strictly required; we can lazily consider the columns, giving an early exit opportunity
    all nicely vectorized of course
    """

    multiplicity, combinations = combinations_index.shape
    #list of indices into combinations_index, denoting possibly interacting combinations
    active_combinations = np.arange(combinations, dtype=np.uint32)

    for packed_column in base_pack_lazy(A, base=multiplicity+1):       #loop over packed cols
        #compute rowsums only for a fixed number of columns at a time.
        #this is O(n^2) rather than O(n^3), and after considering the first column,
        #we can typically already exclude almost all rowpairs
        partial_rowsums = sum(packed_column[I[active_combinations]] for I in combinations_index)
        #find duplicates in this column
        unique, count, inverse = unique_count(partial_rowsums)
        #prune those pairs which we can exclude as having different sums, based on columns inspected thus far
        active_combinations = active_combinations[count[inverse] > 1]
        #early exit; no pairs
        if len(active_combinations)==0:
            return False
    return True

def has_identical_triple_row_sums(A):
    n = len(A)
    idx = np.array( [(i,j,k)
        for i in xrange(n)
            for j in xrange(n)
                for k in xrange(n)
                    if i<j and j<k], dtype=np.uint16)
    idx = np.ascontiguousarray( idx.T)
    return has_identical_row_sums_lazy(A, idx)

def has_identical_double_row_sums(A):
    n = len(A)
    idx = np.array(np.tril_indices(n,-1), dtype=np.int32)
    return has_identical_row_sums_lazy(A, idx)


from time import clock
t = clock()
for i in xrange(10):
    print has_identical_double_row_sums(A)
    print has_identical_triple_row_sums(A)
print clock()-t

如您在上面所问的,已扩展为包括对三组行总和的计算。对于 n=100,这仍然只需要大约 0.2s

编辑:一些清理;编辑2:更多清理

【讨论】:

  • 非常感谢!
  • 不客气;感谢您发布一个有趣的问题。请注意,按照所提出的思路,扩展到更高的组合学应该是微不足道的;但请注意在这种情况下使用的 n。
  • 顺便说一句,我很好奇;这是干什么用的?
  • 我正在写一些疯狂的模拟 :) np.add.at 在我的 numpy 版本中不起作用。这是不是很新鲜?
  • 是的;这只适用于最新的 numpy.矢量化分散操作,你不想错过!有一个 pre-1.8 hack 来实现同样的效果,但如果我是你,我更喜欢升级
【解决方案4】:

您当前的代码不会测试总和为相同值的行对。

假设这实际上是您想要的,最好坚持纯 numpy。这会生成总和相等的所有行的索引。

import numpy as np

n = 100
A = np.random.randint(2, size=(n,n))

rowsum = A.sum(axis=1)

unique, inverse = np.unique(rowsum, return_inverse = True)

count = np.zeros_like(unique)
np.add.at(count, inverse, 1)

for p in unique[count>1]:
    print p, np.nonzero(rowsum==p)[0]

【讨论】:

  • 你需要 A.sum(axis=-1),而nonzero() 是“浪费”时间,因为它基本上是在整个数组中循环每个唯一值。在这里使用列表更快。
  • 行、列;有什么不同 ;)。印刷品仅作说明用途;并不是说 OP 首先对这些指数感兴趣。无论哪种方式,如果 n=100 确实是挑战,我怀疑涉及 python 列表的东西会变得更快。但实际上,这个循环在 n -> inf 的性能上并没有线性扩展
  • 我的意思是“成对成对的行”。
  • 很好,如果有人愿意实际使用您的代码,似乎值得指出。无论如何,打印不是为了说明,在那里你实际上计算了共享相同值的行集。
  • @EelcoHoogendoorn,事实证明,使用列表而不是最后一个循环,n=10 的速度提高了 5 倍,n=100 的速度提高了 8 倍
【解决方案5】:

如果您需要做的就是确定是否存在这样的一对,您可以这样做:

exists_unique = np.unique(A.sum(axis=1)).size != A.shape[0]

【讨论】:

  • 我的意思是“成对成对的行”。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2019-02-03
  • 2012-10-03
相关资源
最近更新 更多