【问题标题】:How to align two lists of numbers如何对齐两个数字列表
【发布时间】:2021-07-29 20:03:43
【问题描述】:

我有两个排序的数字列表AB,其中B 至少与A 一样长。说:

A = [1.1, 2.3, 5.6, 5.7, 10.1]
B = [0, 1.9, 2.4, 2.7, 8.4, 9.1, 10.7, 11.8]

我想将A 中的每个号码与B 中的不同号码相关联,但保留顺序。对于任何此类映射,我们将总距离定义为映射数字之间的平方距离之和。

例如:

如果我们将 1.1 映射到 0 0 那么 2.3 可以映射到从 1.9 开始的任何数字。但是如果我们将 1.1 映射到 2.7,那么 2.3 只能映射到 B 中从 8.4 开始的数字。

假设我们映射 1.1->0、2.3->1.9、5.6->8.4、5.7->9.1、10.1->10.7。这是一个有效的映射,距离为 (1.1^2+0.4^2+2.8^2+3.4^2+0.6^2)。

另一个展示贪婪方法的例子是行不通的:

 A = [1, 2]
 B = [0, 1, 10000]

如果我们映射 1->1,那么我们必须映射 2->10000,这很糟糕。

任务是找到总距离最小的有效映射。

很难做到吗?当列表长度为几千时,我对一种快速的方法感兴趣。

【问题讨论】:

  • 不,这并不难。蛮力将适用于短名单。动态编程适用于更大的列表。
  • @user3386109 你能为更大的列表提供解决方案吗?
  • @user3386109 是的,广泛地说。
  • 如果您将 1.1 分配给 0 并将 2.3 分配给 2.4,那么您将面临子问题 A = [5.6,5.7,10.1] B=[2.7,8.4,...] 如果您将 1.1 分配给 1.9 并将 2.3 分配给 2.4,那么您将面临相同的子问题.因此,您可以看到子问题由 A 和 B 中剩余的元素数量定义。您可以通过记忆子问题的答案来应用动态规划。
  • 看看this article(减去移位估计)。

标签: python algorithm


【解决方案1】:

这是一个O(n) 解决方案! (这是最初的尝试,固定版本见下文。)

思路如下。我们首先解决每个其他元素的问题,将其转化为非常接近的解决方案,然后使用动态规划找到真正的解决方案。这是先解决一半大小的问题,然后是O(n) 工作。使用x + x/2 + x/4 + ... = 2x 事实证明这是O(n) 工作。

这非常非常需要排序列表。并且做一个 5 跨的乐队是矫枉过正的,它看起来很像一个 3 跨的乐队总是给出正确的答案,但我没有足够的信心去这样做。

def improve_matching (list1, list2, matching):
    # We do DP forward, trying a band that is 5 across, building up our
    # answer as a linked list.  If our answer changed by no more than 1
    # anywhere, we are done.  Else we recursively improve again.
    best_j_last = -1
    last = {-1: (0.0, None)}
    for i in range(len(list1)):
        best_j = None
        best_cost = None
        this = {}
        for delta in (-2, 2, -1, 1, 0):
            j = matching[i] + delta
            # Bounds sanity checks.
            if j < 0:
                continue
            elif len(list2) <= j:
                continue

            j_prev = best_j_last
            if j <= j_prev:
                if j-1 in last:
                    j_prev = j-1
                else:
                    # Can't push back this far.
                    continue

            cost = last[j_prev][0] + (list1[i] - list2[j])**2
            this[j] = (cost, [j, last[j_prev][1]])
            if (best_j is None) or cost <= best_cost:
                best_j = j
                best_cost = cost

        best_j_last = best_j
        last = this

    (final_cost, linked_list) = last[best_j_last]
    matching_rev = []
    while linked_list is not None:
        matching_rev.append( linked_list[0])
        linked_list = linked_list[1]
    matching_new = [x for x in reversed(matching_rev)]
    for i in range(len(matching_new)):
        if 1 < abs(matching[i] - matching_new[i]):
            print "Improving further" # Does this ever happen?
            return improve_matching(list1, list2, matching_new)

    return matching_new

def match_lists (list1, list2):
    if 0 == len(list1):
        return []
    elif 1 == len(list1):
        best_j = 0
        best_cost = (list1[0] - list2[0])**2
        for j in range(1, len(list2)):
            cost = (list1[0] - list2[j])**2
            if cost < best_cost:
                best_cost = cost
                best_j = j
        return [best_j]
    elif 1 < len(list1):
        # Solve a smaller problem first.
        list1_smaller = [list1[2*i] for i in range((len(list1)+1)//2)]
        list2_smaller = [list2[2*i] for i in range((len(list2)+1)//2)]
        matching_smaller = match_lists(list1_smaller, list2_smaller)

        # Start with that matching.
        matching = [None] * len(list1)
        for i in range(len(matching_smaller)):
            matching[2*i] = 2*matching_smaller[i]

        # Fill in the holes between
        for i in range(len(matching) - 1):
            if matching[i] is None:
                best_j = matching[i-1] + 1
                best_cost = (list1[i] - list2[best_j])**2
                for j in range(best_j+1, matching[i+1]):
                    cost = (list1[i] - list2[j])**2
                    if cost < best_cost:
                        best_cost = cost
                        best_j = j
                matching[i] = best_j

        # And fill in the last one if needed
        if matching[-1] is None:
            if matching[-2] + 1 == len(list2):
                # This will be an invalid matching, but improve will fix that.
                matching[-1] = matching[-2]
            else:
                best_j = matching[-2] + 1
                best_cost = (list1[-2] - list2[best_j])**2
                for j in range(best_j+1, len(list2)):
                    cost = (list1[-1] - list2[j])**2
                    if cost < best_cost:
                        best_cost = cost
                        best_j = j
                matching[-1] = best_j

        # And now improve.
        return improve_matching(list1, list2, matching)

def best_matching (list1, list2):
    matching = match_lists(list1, list2)
    cost = 0.0
    result = []
    for i in range(len(matching)):
        pair = (list1[i], list2[matching[i]])
        result.append(pair)
        cost = cost + (pair[0] - pair[1])**2
    return (cost, result)

更新

上面有一个错误。可以使用match_lists([1, 3], [0, 0, 0, 0, 0, 1, 3]) 进行演示。但是下面的解决方案也是O(n),我相信没有错误。不同之处在于,我不是寻找固定宽度的带,而是寻找由先前匹配动态确定的宽度。由于在任何给定位置最多只能有 5 个条目可以匹配,因此对于该数组和几何递减的递归调用,它再次结束 O(n)。但相同值的长时间延伸不会导致问题。

def match_lists (list1, list2):
    prev_matching = []

    if 0 == len(list1):
        # Trivial match
        return prev_matching
    elif 1 < len(list1):
        # Solve a smaller problem first.
        list1_smaller = [list1[2*i] for i in range((len(list1)+1)//2)]
        list2_smaller = [list2[2*i] for i in range((len(list2)+1)//2)]
        prev_matching = match_lists(list1_smaller, list2_smaller)

    best_j_last = -1
    last = {-1: (0.0, None)}
    for i in range(len(list1)):
        lowest_j = 0
        highest_j = len(list2) - 1
        if 3 < i:
            lowest_j = 2 * prev_matching[i//2 - 2]
        if i + 4 < len(list1):
            highest_j = 2 * prev_matching[i//2 + 2]

        if best_j_last == highest_j:
            # Have to push it back.
            best_j_last = best_j_last - 1

        best_cost = last[best_j_last][0] + (list1[i] - list2[highest_j])**2
        best_j = highest_j
        this = {best_j: (best_cost, [best_j, last[best_j_last][1]])}

        # Now try the others.
        for j in range(lowest_j, highest_j):
            prev_j = best_j_last
            if j <= prev_j:
                prev_j = j - 1

            if prev_j not in last:
                continue
            else:
                cost = last[prev_j][0] + (list1[i] - list2[j])**2
                this[j] = (cost, [j, last[prev_j][1]])
                if cost < best_cost:
                    best_cost = cost
                    best_j = j

        last = this
        best_j_last = best_j

    (final_cost, linked_list) = last[best_j_last]
    matching_rev = []
    while linked_list is not None:
        matching_rev.append( linked_list[0])
        linked_list = linked_list[1]
    matching_new = [x for x in reversed(matching_rev)]

    return matching_new

def best_matching (list1, list2):
    matching = match_lists(list1, list2)
    cost = 0.0
    result = []
    for i in range(len(matching)):
        pair = (list1[i], list2[matching[i]])
        result.append(pair)
        cost = cost + (pair[0] - pair[1])**2
    return (cost, result)

注意

我被要求解释为什么会这样。

这是我的启发式理解。在算法中,我们解决了一半的问题。然后我们必须解决全部问题。

问题是完整问题的最优解可以强制从最优解到半问题多远?我们通过使list2 中不在半问题中的每个元素尽可能大,并将list1 中不在半问题中的每个元素尽可能小,将其推到右侧。但是如果我们把半问题的那些推到右边,然后把重复的元素放在它们是模边界效应的地方,我们就得到了半问题的 2 个最优解,而且没有什么比下一个元素向右移动更多的了是在一半的问题。类似的推理也适用于试图迫使解决方案离开。

现在让我们讨论这些边界效应。这些边界效应最后是 1 个元素。所以当我们试图把一个元素推到最后时,我们不能总是这样。通过查看 2 个元素而不是 1 个元素,我们也添加了足够的回旋余地来解决这一问题。

因此,必须有一个最佳解决方案,该解决方案非常接近以明显方式加倍的一半问题。可能还有其他人,但至少有一个。并且 DP 步骤会找到它。

我需要做一些工作才能将这种直觉转化为正式的证明,但我相信这是可以做到的。

【讨论】:

  • 这是一个有趣的算法。我期待更多地了解它。
  • @Anush 思路如下。假设我们有“其他所有元素”的最佳解决方案。当我们尽可能地添加介于两者之间的元素时,似乎最好的解决方案可以移动 1 个元素,但仅此而已。不过我没有正式的证明。但我也无法创建一个不正确的数组。
  • 找到最佳子结构的非常有趣的想法。不确定当子问题的最优解不唯一时会发生什么。
  • 我刚刚有一个实例,其中记录了 306 行“进一步改进”。我对 A 使用 random.uniform(0, 100000-1) ,对 B 使用 (900000, 1000000-1) ,其中 A 为 500,B 为 10000 个元素。在这样的例子中,大概的复杂度是多少?
  • @GZ0 想一想如果 B 多次具有相同的值会发生什么,我发现了一个错误。我认为它是可以修复的。
【解决方案2】:

这是一个递归解决方案。选取a的中间元素;将其映射到b 的每个可能元素(在每一端留出足够的空间以容纳a 的左右部分)。对于每个这样的映射,计算单元素成本;然后在ab 的左右片段上递归。

这是代码;我将把记忆留作学生的练习。

test_case = [
    [ [1, 2], [0, 1, 10] ],
    [ [1.1, 2.3, 5.6, 5.7, 10.1], [0, 1.9, 2.4, 2.7, 8.4, 9.1, 10.7, 11.8] ],
]

import math
indent = ""


def best_match(a, b):
    """
    Find the best match for elements in a mapping to b, preserving order
    """

    global indent
    indent += "  "
    # print(indent, "ENTER", a, b)

    best_cost = math.inf
    best_map = []

    if len(a) == 0:
        best_cost = 0
        best_map = []

    else:

        # Match the middle element of `a` to each eligible element of `b`
        a_midpt = len(a) // 2
        a_elem = a[a_midpt]
        l_margin = a_midpt
        r_margin = a_midpt + len(b) - len(a) 

        for b_pos in range(l_margin, r_margin+1):
            # For each match ...
            b_elem = b[b_pos]
            # print(indent, "TRACE", a_elem, b_elem)

            # ... compute the element cost ...
            mid_cost = (a_elem - b_elem)**2

            # ... and recur for similar alignments on left & right list fragments
            l_cost, l_map = best_match(a[:l_margin], b[:b_pos])
            r_cost, r_map = best_match(a[l_margin+1:], b[b_pos+1:])

            # Check total cost against best found; keep the best
            cand_cost = l_cost + mid_cost + r_cost
            # print(indent, " COST", mid_cost, l_cost, r_cost)
            if cand_cost < best_cost:
                best_cost = cand_cost
                best_map = l_map[:] + [(a_elem, b_elem)]
                best_map.extend(r_map[:])

    # print(indent, "LEAVE", best_cost, best_map)
    return best_cost, best_map


for a, b in test_case:
    print('\n', a, b)
    print(best_match(a, b))

输出:

 a = [1, 2] 
 b = [0, 1, 10]
2 [(1, 0), (2, 1)]

 a = [1.1, 2.3, 5.6, 5.7, 10.1] 
 b = [0, 1.9, 2.4, 2.7, 8.4, 9.1, 10.7, 11.8]
16.709999999999997 [(1.1, 1.9), (2.3, 2.4), (5.6, 2.7), (5.7, 8.4), (10.1, 10.7)]

【讨论】:

  • 按照 OP 的建议,这个实现如何与数千个元素的列表公平(因为 Python 的默认递归限制较低,我猜是有原因的)? (顺便说一下,投了赞成票,但也尝试了自下而上的方法。)
  • 算法在每一步将列表减半;大多数情况下的递归深度将是len(b) - len(a),随着算法的进展,记忆会缩短。 log(len(a)) 是另一个因素,但一般会小于长度差。
  • 我相信 Python 的默认递归限制是 1000。(我的第一个 draft 提供了一个稍微不同的,虽然不是意外的递归。)
  • 当你遇到一个有界的大问题时,你可以增加递归深度。
  • 啊;好点子。没有人说二阶导数会很好玩。
【解决方案3】:

对于咯咯笑和笑声,希望这是一个比其他任何一个工作更快的解决方案。这个想法很简单。首先,我们从左到右进行贪心匹配。然后从右到左进行贪婪匹配。这给了我们每个元素可以去哪里的界限。然后我们可以从左到右做一个 DP 解决方案,只看可能的值。

如果贪婪方法一致,这将需要线性时间。如果贪心方法相距甚远,这可能需要二次时间。但希望贪婪方法能产生相当接近的结果,从而产生接近线性的性能。

def match_lists(list1, list2):
    # First we try a greedy matching from left to right.
    # This gives us, for each element, the last place it could
    # be forced to match. (It could match later, for instance
    # in a run of equal values in list2.)
    match_last = []
    j = 0
    for i in range(len(list1)):
        while True:
            if len(list2) - j <= len(list1) - i:
                # We ran out of room.
                break
            elif abs(list2[j+1] - list1[i]) <= abs(list2[j] - list1[i]):
                # Take the better value
                j = j + 1
            else:
                break
        match_last.append(j)
        j = j + 1

    # Next we try a greedy matching from right to left.
    # This gives us, for each element, the first place it could be
    # forced to match.
    # We build it in reverse order, then reverse.
    match_first_rev = []
    j = len(list2) - 1
    for i in range(len(list1) - 1, -1, -1):
        while True:
            if j <= i:
                # We ran out of room
                break
            elif abs(list2[j-1] - list1[i]) <= abs(list2[j] - list1[i]):
                # Take the better value
                j = j - 1
            else:
                break
        match_first_rev.append(j)
        j = j - 1
    match_first = [x for x in reversed(match_first_rev)]

    # And now we do DP forward, building up our answer as a linked list.
    best_j_last = -1
    last = {-1: (0.0, None)}
    for i in range(len(list1)):
        # We initialize with the last position we could choose.
        best_j = match_last[i]
        best_cost = last[best_j_last][0] + (list1[i] - list2[best_j])**2
        this = {best_j: (best_cost, [best_j, last[best_j_last][1]])}

        # Now try the rest of the range of possibilities
        for j in range(match_first[i], match_last[i]):
            j_prev = best_j_last
            if j <= j_prev:
                j_prev = j - 1 # Push back to the last place we could match
            cost = last[j_prev][0] + (list1[i] - list2[j])**2
            this[j] = (cost, [j, last[j_prev][1]])
            if cost < best_cost:
                best_cost = cost
                best_j = j
        last = this
        best_j_last = best_j

    (final_cost, linked_list) = last[best_j_last]
    matching_rev = []
    while linked_list is not None:
        matching_rev.append(
                (list1[len(matching_rev)], list2[linked_list[0]]))
        linked_list = linked_list[1]
    matching = [x for x in reversed(matching_rev)]
    return (final_cost, matching)

print(match_lists([1.1, 2.3, 5.6, 5.7, 10.1], [0, 1.9, 2.4, 2.7, 8.4, 9.1, 10.7, 11.8]))

【讨论】:

  • 我想知道是否有办法保证 O(n log n) 最坏情况。
  • @גלעדברקן 我怀疑不是。我之前尝试过基于递归三元搜索来寻找放置中间节点的最佳位置,但失败了。然而,对于大小为 100,000 和 100,500 的随机数数组,这可以在一分钟内工作。一般来说,如果数组的大小为nn + O(sqrt(n)),则运行时间为O(n sqrt(n))。如果您使间隙大于或小于该值,则运行时间会提高。这应该足够了。
  • @גלעדברקן 我的实现大量使用了 A 和 B 是排序列表的假设。您使用的计时代码会生成未排序的列表。如果您可以重现明显但带有排序的列表,我会很感兴趣。也就是说,我确实在 B 中发现了一个带有重复值的小错误,我已修复。
  • 感谢您的澄清。对数字进行排序时,我没有发现差异。现在确实快了很多。
  • 在其他新闻中,我可能已经找到了O(n) 解决方案!我不确定它是否有效。
【解决方案4】:

Python 对递归不是很友好,因此尝试将其应用于包含数千个元素的列表可能不太公平。这是一种自下而上的方法,它利用来自A 的任何a 的最佳解决方案,因为我们将其潜在合作伙伴的索引从B 增加为非递减。 (适用于已排序和未排序的输入。)

def f(A, B):
  m = [[(float('inf'), -1) for b in B] for a in A]

  for i in xrange(len(A)):
    for j in xrange(i, len(B) - len(A) + i + 1):
      d = (A[i] - B[j]) ** 2

      if i == 0:
        if j == i:
          m[i][j] = (d, j)
        elif d < m[i][j-1][0]:
          m[i][j] = (d, j)
        else:
          m[i][j] = m[i][j-1]
      # i > 0
      else:
        candidate = d + m[i-1][j-1][0]
        if j == i:
          m[i][j] = (candidate, j)
        else:
          if candidate < m[i][j-1][0]:
            m[i][j] = (candidate, j)
          else:
            m[i][j] = m[i][j-1]

  result = m[len(A)-1][len(B)-1][0]
  # Backtrack
  lst = [None for a in A]
  j = len(B) - 1
  for i in xrange(len(A)-1, -1, -1):
    j = m[i][j][1]
    lst[i] = j
    j = j - 1
  return (result, [(A[i], B[j]) for i, j in enumerate(lst)])

A = [1, 2]
B = [0, 1, 10000]
print f(A, B)
print ""
A = [1.1, 2.3, 5.6, 5.7, 10.1]
B = [0, 1.9, 2.4, 2.7, 8.4, 9.1, 10.7, 11.8]
print f(A, B)

输出:

(2, [(1, 0), (2, 1)])

(16.709999999999997, [(1.1, 1.9), (2.3, 2.4), (5.6, 2.7), (5.7, 8.4), (10.1, 10.7)])

更新

这是一个O(|B|) 空间实现。我不确定这是否仍然提供一种回溯获取映射的方法,但我正在努力。

def f(A, B):
  m = [(float('inf'), -1) for b in B]
  m1 = [(float('inf'), -1) for b in B] # m[i-1]

  for i in xrange(len(A)):
    for j in xrange(i, len(B) - len(A) + i + 1):
      d = (A[i] - B[j]) ** 2

      if i == 0:
        if j == i:
          m[j] = (d, j)
        elif d < m[j-1][0]:
          m[j] = (d, j)
        else:
          m[j] = m[j-1]
      # i > 0
      else:
        candidate = d + m1[j-1][0]
        if j == i:
          m[j] = (candidate, j)
        else:
          if candidate < m[j-1][0]:
            m[j] = (candidate, j)
          else:
            m[j] = m[j-1]

    m1 = m
    m = m[:len(B) - len(A) + i + 1] + [(float('inf'), -1)] * (len(A) - i - 1)

  result = m1[len(B)-1][0]
  # Backtrack
  # This doesn't work as is
  # to get the mapping
  lst = [None for a in A]
  j = len(B) - 1
  for i in xrange(len(A)-1, -1, -1):
    j = m1[j][1]
    lst[i] = j
    j = j - 1
  return (result, [(A[i], B[j]) for i, j in enumerate(lst)])

A = [1, 2]
B = [0, 1, 10000]
print f(A, B)
print ""
A = [1.1, 2.3, 5.6, 5.7, 10.1]
B = [0, 1.9, 2.4, 2.7, 8.4, 9.1, 10.7, 11.8]
print f(A, B)

import random
import time

A = [random.uniform(0, 10000.5) for i in xrange(10000)]
B = [random.uniform(0, 10000.5) for i in xrange(15000)]

start = time.time()
print f(A, B)[0]
end = time.time()
print(end - start)

【讨论】:

  • 这看起来很有趣。我会尽快试用。
  • 我添加了另一种自下而上的方法,它使用贪婪来尽量避免在 DP 中进行尽可能多的工作。
  • cs.au.dk/~cstorm/students/MyHoa_Dec2010.pdf 的第 7 部分有帮助吗?
猜你喜欢
  • 1970-01-01
  • 2014-10-21
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2015-11-11
  • 1970-01-01
相关资源
最近更新 更多