使用相对位置数据对列表进行排序

时间:2016-11-05 00:56:08

标签: python algorithm sorting language-agnostic

这更像是一个概念性的编程问题,所以请耐心等待:

假设您有电影中的场景列表,并且每个场景可能会或可能不会参考同一电影中的过去/未来场景。我正在尝试找到最有效的排序这些场景的算法。当然,可能没有足够的信息让场景完全排序。

这里有一些Python中的示例代码(几乎是伪代码)来澄清:

class Reference:
    def __init__(self, scene_id, relation):
        self.scene_id = scene_id
        self.relation = relation


class Scene:
    def __init__(self, scene_id, references):
        self.id = scene_id
        self.references = references

    def __repr__(self):
        return self.id


def relative_sort(scenes):
    return scenes # Algorithm in question


def main():
    s1 = Scene('s1', [
        Reference('s3', 'after')
    ])
    s2 = Scene('s2', [
        Reference('s1', 'before'),
        Reference('s4', 'after')
    ])
    s3 = Scene('s3', [
        Reference('s4', 'after')
    ])
    s4 = Scene('s4', [
        Reference('s2', 'before')
    ])

    print relative_sort([s1, s2, s3, s4])


if __name__ == '__main__':
    main()

目标是在这种情况下relative_sort返回[s4, s3, s2, s1]

如果它有用,我可以分享我对该算法的初步尝试;我对它的蛮力有点尴尬。另外,如果你想知道,我正在尝试解读电影“穆赫兰道”的情节。

仅供参考:Python标签仅在此处,因为我的伪代码是用Python编写的。

3 个答案:

答案 0 :(得分:1)

您要查找的算法是topological sort

  

在计算机科学领域,有向图的拓扑排序或拓扑排序是其顶点的线性排序,使得对于从顶点u到顶点v的每个有向边uv,u在排序中位于v之前。例如,图的顶点可以表示要执行的任务,并且边可以表示一个任务必须在另一个之前执行的约束;在这个应用程序中,拓扑排序只是任务的有效序列。

您可以使用图表库轻松计算,例如,networkx,它实现topological_sort。首先,我们导入库并列出场景之间的所有关系 - 即图中的所有有向边

>>> import networkx as nx
>>> relations = [
    (3, 1),  # 1 after 3
    (2, 1),  # 2 before 1
    (4, 2),  # 2 after 4
    (4, 3),  # 3 after 4
    (4, 2)   # 4 before 2
]

然后我们创建一个有向图:

>>> g = nx.DiGraph(relations)

然后我们进行拓扑排序:

>>> nx.topological_sort(g)
[4, 3, 2, 1]

答案 1 :(得分:0)

我已将修改后的代码包含在我的答案中,这解决了当前(小)问题,但没有更大的样本问题,我不确定它的扩展程度。如果您提供了您尝试解决的实际问题,我很乐意测试并优化此代码,直到它解决该问题,但如果没有测试数据,我将不再进一步优化此解决方案。

对于初学者,我们将引用跟踪为集合,而不是列表。

  • 重复对我们没有帮助(如果在“s2”之前为“s1”,在“s2”之前为“s1”,我们没有获得任何信息)
  • 这也让我们添加反向引用放弃(如果“s1”在“s2”之前,则“s2”在“s1”之后)。

我们计算最小和最大位置:

  • 根据我们之后的场景数量确定最低位置
  • 这可以很容易地扩展:如果我们在min_pos为2的两个场景之后,我们的min_pos是4(如果一个是2,其他必须是3)
  • 基于我们之前有多少事情的最大排名
  • 这可以类似地扩展:如果我们在max_pos为4的两个场景之前来到,我们的max_pos是2(如果一个是4,其他必须是3)
  • 如果您决定这样做,只需用代码替换pass中的tighten_bounds(self)以尝试收紧单个场景的边界(如果有效,则将anything_updated设置为true)。

魔法在get_possible_orders

  • 如果迭代它,则生成所有有效的排序
  • 如果您只想要一个有效的订购,则不需要花时间全部创建

代码:

class Reference:
    def __init__(self, scene_id, relation):
        self.scene_id = scene_id
        self.relation = relation

    def __repr__(self):
        return '"%s %s"' % (self.relation, self.scene_id)

    def __hash__(self):
        return hash(self.scene_id)

    def __eq__(self, other):
        return self.scene_id == other.scene_id and self.relation == other.relation


class Scene:
    def __init__(self, title, references):
        self.title = title
        self.references = references
        self.min_pos = 0
        self.max_pos = None

    def __repr__(self):
        return '%s (%s,%s)' % (self.title, self.min_pos, self.max_pos)

inverse_relation = {'before': 'after', 'after': 'before'}


def inverted_reference(scene, reference):
    return Reference(scene.title, inverse_relation[reference.relation])


def is_valid_addition(scenes_so_far, new_scene, scenes_to_go):
    previous_ids = {s.title for s in scenes_so_far}
    future_ids = {s.title for s in scenes_to_go}
    for ref in new_scene.references:
        if ref.relation == 'before' and ref.scene_id in previous_ids:
            return False
        elif ref.relation == 'after' and ref.scene_id in future_ids:
            return False
    return True


class Movie:
    def __init__(self, scene_list):
        self.num_scenes = len(scene_list)
        self.scene_dict = {scene.title: scene for scene in scene_list}
        self.set_max_positions()
        self.add_inverse_relations()
        self.bound_min_max_pos()
        self.can_tighten = True
        while self.can_tighten:
            self.tighten_bounds()

    def set_max_positions(self):
        for scene in self.scene_dict.values():
            scene.max_pos = self.num_scenes - 1

    def add_inverse_relations(self):
        for scene in self.scene_dict.values():
            for ref in scene.references:
                self.scene_dict[ref.scene_id].references.add(inverted_reference(scene, ref))

    def bound_min_max_pos(self):
        for scene in self.scene_dict.values():
            for ref in scene.references:
                if ref.relation == 'before':
                    scene.max_pos -= 1
                elif ref.relation == 'after':
                    scene.min_pos += 1

    def tighten_bounds(self):
        anything_updated = False
        for scene in self.scene_dict.values():
            pass
            # If bounds for any scene are tightened, set anything_updated back to true
        self.can_tighten = anything_updated

    def get_possible_orders(self, scenes_so_far):
        if len(scenes_so_far) == self.num_scenes:
            yield scenes_so_far
            raise StopIteration
        n = len(scenes_so_far)
        scenes_left = set(self.scene_dict.values()) - set(scenes_so_far)
        valid_next_scenes = set(s
                                for s in scenes_left
                                if s.min_pos <= n <= s.max_pos)
        # valid_next_scenes = sorted(valid_next_scenes, key=lambda s: s.min_pos * self.num_scenes + s.max_pos)
        for s in valid_next_scenes:
            if is_valid_addition(scenes_so_far, s, scenes_left - {s}):
                for valid_complete_sequence in self.get_possible_orders(scenes_so_far + (s,)):
                    yield valid_complete_sequence

    def get_possible_order(self):
        return self.get_possible_orders(tuple()).__next__()


def relative_sort(lst):
    try:
        return [s.title for s in Movie(lst).get_possible_order()]
    except StopIteration:
        return None


def main():
    s1 = Scene('s1', {Reference('s3', 'after')})
    s2 = Scene('s2', {
        Reference('s1', 'before'),
        Reference('s4', 'after')
    })
    s3 = Scene('s3', {
        Reference('s4', 'after')
    })
    s4 = Scene('s4', {
        Reference('s2', 'before')
    })

    print(relative_sort([s1, s2, s3, s4]))


if __name__ == '__main__':
    main()

答案 2 :(得分:0)

正如其他人所指出的,你需要拓扑排序。定向关系形成边缘的有向图的深度优先遍历就是您所需要的。访问后期订单。这与拓扑排序相反。所以要获得topo排序,只需反转结果。

我已将您的数据编码为一对列表,显示了之前已知的内容。这只是为了保持我的代码简短。您可以轻松遍历类列表以创建图形。

请注意,要使topo排序有意义,要排序的集必须满足partial order的定义。你的很好。时间事件的顺序约束自然满足定义。

请注意,使用循环创建图表是完全可能的。没有像这样的图表。此实现不会检测周期,但可以很容易地对其进行修改。

当然你可以使用一个库来获得topo排序,但那里的乐趣在哪里?

from collections import defaultdict

# Before -> After pairs dictating order. Repeats are okay. Cycles aren't.
# This is OP's data in a friendlier form.
OrderRelation = [('s3','s1'), ('s2','s1'), ('s4','s2'), ('s4','s3'), ('s4','s2')]

class OrderGraph:
  # nodes is an optional list of items for use when some aren't related at all
  def __init__(self, relation, nodes=[]):
    self.succ = defaultdict(set) # Successor map
    heads = set()
    for tail, head in relation:
      self.succ[tail].add(head)
      heads.add(head)
    # Sources are nodes that have no in-edges (tails - heads)
    self.sources = set(self.succ.keys()) - heads | set(nodes)

  # Recursive helper to traverse the graph and visit in post order
  def __traverse(self, start):
    if start in self.visited: return
    self.visited.add(start)
    for succ in self.succ[start]: self.__traverse(succ)
    self.sorted.append(start) # Append in post-order

  # Return a reverse post-order visit, which is a topo sort. Not thread safe.
  def topoSort(self):
    self.visited = set()
    self.sorted = []
    for source in self.sources: self.__traverse(source)
    self.sorted.reverse()
    return self.sorted

则...

>>> print OrderGraph(OrderRelation).topoSort()
['s4', 's2', 's3', 's1']

>>> print OrderGraph(OrderRelation, ['s1', 'unordered']).topoSort()
['s4', 's2', 's3', 'unordered', 's1']

第二个调用显示您可以选择传递要在单独列表中排序的值。您可能但在关系对中已经没有提及值。当然,订单对中未提及的那些可以自由地出现在输出中的任何位置。