对数组进行排序以获得最大的成对匹配

时间:2011-02-06 06:46:06

标签: python arrays numpy

我有一个数组:

array([[ 4, 10],
       [ 4,  2],
       [ 0,  7],
       [ 5, 11],
       [ 6,  8],
       [ 3,  6],
       [ 9,  7],
       [ 2, 11],
       [ 9,  5],
       [ 8,  1]])

我想要一种方法来对值对进行排序,以便尽可能多的成对2元素集具有公共值。这是所需有序数组的示例:

array([[ 4, 10],
       [ 4,  2],
       [ 2, 11],
       [ 5, 11],
       [ 9,  5],
       [ 9,  7],
       [ 0,  7],  #note the gap here:
       [ 8,  1],
       [ 6,  8],
       [ 3,  6]])

这些数组的几个条件都是正确的。没有重复对(即:[1,0]或[0,1]将出现在数组的其他位置,如果[0,1]已经存在)。没有对具有相同的值(即:[1,1]将不存在)。没有对将有两个以上的匹配(喵:在整个数组中没有值存在两次以上。)但是一对可以有零匹配(注意在上面的数组中没有匹配的间隙)。 / p>

显然,我可以创建数组的每个排列,但这看起来很粗糙。我认为可能有某种方法可以切割平台并以合理的方式重新堆叠,以便在少量切割中进行分类。但在我走这条路之前,我想:1)确保没有numpycollections功能已经执行此操作。 2)知道使用numpy .sort()(或类似的)来做这件事没有棘手的天才方法。 3)确定这是否是一项常见任务,并且有算法可以执行此操作。 (“哦,那是Blumen-Funke算法!”)

以下是一些生成混洗测试数组并检查已排序数组的代码:

def shuffled(N=12, ans=1):
    '''returns is now the unsorted test array'''
    r = range(N)
    random.shuffle(r)
    l = []
    for i in range(N):
        l.append((r[i-1], r[i]))
    random.shuffle(l)
    return np.array(l)[ans+1:]

# step 2: ???

def test_ordered(a):
    '''checks if generated array has been sorted'''
    c0 = a[1:,0]==a[:-1,0]
    c1 = a[1:,0]==a[:-1,1]
    c2 = a[1:,1]==a[:-1,0]
    c3 = a[1:,1]==a[:-1,1]
    cond = c0+c1+c2+c3
    ans = sum(numpy.logical_not(cond))
    # when sorted this should return the same number input into
    # shuffled() as 'ans':
    return ans

(这是一个主观问题吗?我被警告说是。)

结果:

非常感谢你的帮助。 Sven的解决方案比Paul的解决速度快20%,并且很高兴,他们都在线性时间运行,Doug的答案并没有解决问题。在输入数据中,性能​​对“中断”或“间隙”的数量存在很高但也很大程度上线性的依赖性。见下图。 10,000幅度轴是N. 0.5轴是断裂的百分比。 z轴是以秒为单位的性能。

再次感谢!

enter image description here

4 个答案:

答案 0 :(得分:5)

您已经描述了一个图形,其中顶点是数字,边缘是您的对。

您的条件指定每个号码在列表中显示一次或两次。这意味着图表中的连接组件是线(或循环)。您可以使用此算法找到它们:

  • [行存在]如果可能,请选择度数为1的数字(即,它仅在列表中出现一次)。尽可能地跟随对的链,将它们添加到输出并从图中移除遍历的顶点。
  • [循环存在]如果没有度数为1的数字,则表示所有组件都是循环。选择任何顶点(它将具有2级)。像以前一样按照对,将它们添加到输出并删除遍历的顶点,但这次在达到原始数字时停止。
  • 重复,直到您用完了图表中的所有顶点。

您可以有效地运行此算法:维护一组1度的顶点和2度的另一个顶点。当您使用边缘(原始列表中的一对)时,适当地修改集合:从中删除1级的端点第一组,并将端点从2级设置移动到1级。或者,使用优先级队列。

你还需要对你的对进行有效的查找:从顶点到相邻顶点列表构建一个dict。

使用这些想法,您可以在线性时间内找到最佳排序(假设O(1)set和dict实现)。

这是一个有点笨重的实现。

import collections

def handle_vertex(v, vs):
  if v in vs[0]:
    vs[0].remove(v)
  elif v in vs[1]:
    vs[0].add(v)
    vs[1].remove(v)

def follow(edgemap, start, vs):
  """Follow a path in the graph, yielding the edges."""
  v0 = start
  last = None
  while True:
    # All vertices that we can go to next.
    next_v = [v for v in edgemap[v0] if v != last]
    if not next_v:
      # We've reached the end (we must have been a line).
      return
    assert len(next_v) == 1 or (v0 == start and len(next_v) == 2)
    next_v = next_v[0]
    # Remove the endpoints from the vertex-degree sets.
    handle_vertex(v0, vs)
    handle_vertex(next_v, vs)
    yield v0, next_v
    if next_v == start:
      # We've got back to the start (we must have been a cycle).
      return
    v0, last = next_v, v0

def pairsort(edges):
  edgemap = collections.defaultdict(list)
  original_edges = {}
  for a, b in edges:
    # Build the adjacency table.
    edgemap[a].append(b)
    edgemap[b].append(a)
    # Keep a map to remember the original order pairs appeared in
    # so we can output edges correctly no matter which way round
    # we store them.
    original_edges[a, b] = [a, b]
    original_edges[b, a] = [a, b]
  # Build sets of degree 1 and degree 2 vertices.
  vs = [set(), set()]
  for k, v in edgemap.iteritems():
    vs[len(v) - 1].add(k)
  # Find all components that are lines.
  while vs[0]:
    v0 = vs[0].pop()
    for e in follow(edgemap, v0, vs):
      yield original_edges[e]
  # Find all components that are cycles.
  while vs[1]:
    v0 = vs[1].pop()
    for e in follow(edgemap, v0, vs):
      yield original_edges[e]

input = [
    [ 4, 10],
    [ 4,  2],
    [ 0,  7],
    [ 5, 11],
    [ 6,  8],
    [ 3,  6],
    [ 9,  7],
    [ 2, 11],
    [ 9,  5],
    [ 8,  1]]

print list(pairsort(input))

答案 1 :(得分:2)

我不确定我理解你问题的每一个细节;但如果我这样做,那么这应该做你想要的。

基本思路非常简单:对于两个1D阵列,您可以循环遍历所有成对 将它们排成一行,保持一个静止,然后一次向前滚动第二个增量。如果您使用NumPy的 roll 功能,那么当您向前滚动时,从数组末端掉落的值只会被推回到前面,就像跑步机。

在设置之后,只需沿正确的轴对两个向量进行差分 并且总和为0。跟踪这些总和( tx ,下面)然后得到对应的索引 这些总和的最大值, NP.argmax(tx)

import numpy as NP

# create some data:
c1 = NP.random.randint(0, 10, 15)
c2 = NP.random.randint(0, 10, 15)
c12 = NP.concatenate((c1, c2)).reshape(15, 2)

tx = []    # to hold the indices of the various orderings

for i in range(15) :
    v = NP.diff(c12, axis=0)
    num_equal_neighbors = NP.sum( NP.sum(v==0, axis=0) )
    tx.append(num_equal_neighbors)
    c2 = NP.roll(c2, 1)
    c12[:,1] = c2

现在找到两个向量的哪个排序给出最“成对”匹配:

best_order = NP.argmax(tx)
因此,当排列两个1D阵列时,发生所需的排序 这样第二个数组就滚动了* best_order *个数量 (并保留第一个数组)

答案 2 :(得分:1)

这是使用不同数据结构在Paul Hankin's answer中描述的基本相同算法的更简单实现。它也以线性时间运行。

edges = [[4, 10], [4,  2], [0,  7], [5, 11], [6,  8],
         [3,  6], [9,  7], [2, 11], [9,  5], [8,  1]]

def add_edge(vertex, adj):
    edge = edges[adj[0]][:]
    ordered_edges.append(edge[:])
    edge.remove(vertex)
    new_vertex = edge[0]
    new_adj = adj_edges[new_vertex]
    new_adj.remove(adj[0])
    del adj[0]
    return new_vertex, new_adj

adj_edges = {}
for i, edge in enumerate(edges):
    for vertex in edge:
        adj_edges.setdefault(vertex, []).append(i)
ordered_edges = []
for vertex, adj in adj_edges.iteritems():
    while len(adj) == 1:
        vertex, adj = add_edge(vertex, adj)
for vertex, adj in adj_edges.iteritems():
    while adj:
        vertex, adj = add_edge(vertex, adj)

答案 3 :(得分:0)

另见 Longest path problem: NP对于一般图表是完整的,或者对于非循环的权重否定的最短路径 另外,请尝试显示的图表 Spanning tree: 最长的比较长也难。