如何确定列表中的循环?

How to determine cycles in a list?

考虑一个整数索引列表,indicesindices 的所有元素都在列表 indices 的范围内。列表 indices 中的所有值都是唯一的。示例 [2, 0, 1, 4, 3, 5].

给定一个索引(例如 0),继续 indices[0](即上例中的 2)。重复可以无限期地完成。当移动 returns 到一个已经访问过的索引时,会发生一个循环。总是至少有一个循环。循环长度的总和等于列表的长度indices

我需要创建一个函数 cycles(indices) returns indices 中所有循环的列表。

对于上面的例子,预期的输出列表是[[2, 0, 1], [4, 3], [5]]

对于列表 [0, 1, 2, 3],循环是微不足道的,形式为 [[0], [1], [2], [3]]

注意:输出循环的顺序和索引的顺序无关紧要。例如:[[2, 0, 1], [4, 3], [5]] 等价于 [[3, 4], [0, 1, 2], [5]]

这是我到目前为止想出的代码:

def cycles(indices):
    cycles = []
    old_i = []
    i = 0
    while i in range(len(indices)):
        old_i.append(i)
        i = indices[i]
        if i in old_i:
            cycles.append(old_i)
            i = max(old_i) + 1
            old_i = []

    return cycles

我的代码的问题是,如果它已经通过了较小循环的元素,它不会重新检查较大循环的元素中是否有较小的循环。关于如何解决这个编码问题有什么建议吗?

请注意,我刚刚开始学习如何编程,并且正在学习入门在线课程,因此我希望能提供简单的解决方案。

一个粗略的解决方案,使用集合来跟踪我们已经访问过的索引:

def cycles(indices):

    def get_loop(indices, idx):             # this funtion returns a loop, starting from index `idx`
        seen, loop = set(), []
        while True:
            v = indices[idx]                # get index on position `idx`
            if v not in seen:               # have we seen index `v` already?
                loop.append(idx)            # no, add it as part of the loop
                seen.add(v)                 #   update set `seen` with index `v`
                idx = v                     #   set current index `idx` with index `v`
            else:
                break                       # yes, that means we closed the loop
        return loop                         # return this loop

    rv, seen = [], set()
    for i in indices:                       # iterate over all indices
        if i not in seen:                   # is index `i` part of any loop we already have?
            loop = get_loop(indices, i)     # no, so call get_loop() with starting index `i`
            rv.append(loop)                 # we add this loop to the result list
            seen.update(loop)               # update set `seen` with all indices inside this loop

    return rv

print(cycles([2, 0, 1, 4, 3, 5]))
print(cycles([0, 1, 2, 3, 4, 5]))
print(cycles([12, 0, 8, 10, 9, 6, 5, 4, 13, 7, 17,14, 2,18, 16, 1, 11, 19, 3, 15]))

打印:

[[2, 1, 0], [4, 3], [5]]
[[0], [1], [2], [3], [4], [5]]
[[12, 2, 8, 13, 18, 3, 10, 17, 19, 15, 1, 0], [9, 7, 4], [6, 5], [14, 16, 11]]

如果索引已经是循环的一部分,您可以跳过它。

def find_cycles(indices):
    """Given a list of indices return a list of cycles."""

    visited = set()
    cycles = []

    for start_ind in indices:

        if start_ind in visited:
            continue

        path = [start_ind]
        next_ind = indices[start_ind]

        while start_ind != next_ind:
            path.append(next_ind)
            next_ind = indices[next_ind]
        else:
            cycles.append(path)
            visited |= set(path)
    return cycles

find_cycles([12, 0, 8, 10, 9, 6, 5, 4, 13, 7, 17,14, 2,18, 16, 1, 11, 19, 3, 15])
# [[12, 2, 8, 13, 18, 3, 10, 17, 19, 15, 1, 0], [9, 7, 4], [6, 5], [14, 16, 11]]

您可以跟踪您已经访问过的所有索引,并在下次忽略它们。此外,每个循环都有唯一的索引,无论您在循环中的何处开始(12 或 0,如果两者在同一循环中),您始终可以完成循环(如圆圈)。

def gen_cycles(indices):
    cycles = []
    visited = []
    for i in indices:
        if(i in visited):
            continue
        start = i;
        current_cycle = []
        current_cycle.append(start)
        visited.append(start)
        j = indices[start]
        while(j!=start):
            current_cycle.append(j)
            visited.append(j)
            j = indices[j]
        cycles.append(current_cycle)
    return cycles

print(gen_cycles([12, 0, 8, 10, 9, 6, 5, 4, 13, 7, 17,14, 2,18, 16, 1, 11, 19, 3, 15]))

Output: [[12, 2, 8, 13, 18, 3, 10, 17, 19, 15, 1, 0], [9, 7, 4], [6, 5], [14, 16, 11]]

让我们看看[2, 0, 1, 4, 3, 5]。在循环[2, 0, 1]中,数字的顺序并不重要,所以如果你从它的任何一点开始,你就会得到正确的循环。

对于大小为 N 的数组,一个简单的解决方案是: 1. 遍历所有索引 1..N-1 作为 starting_index 2. 获取从索引 starting_index 开始的循环 3. 如果我们已经看到这个循环,则丢弃它。否则记住了。

函数可能如下所示:

def get_cycles(array):
    cycles = []
    for starting_index in range(len(array)):
        cycle = cycle_at(array, starting_index)
        if not any(are_equal(existing, cycle) for existing in cycles):
            cycles.append(cycle)
    return cycles

如果您不熟悉 any,带有 any 的行大致如下:

def any(iterable):
  for x in iterable:
    if x:
      return True
  return False

我们希望循环 [0, 1, 2][1, 2, 0] 被认为是相等的。 我们可以编写一个函数来移动其中一个,直到它匹配第二个:

def are_equal(cycle1, cycle2):
    if len(cycle1) != len(cycle2):
        return False
    for i in range(len(cycle1)):
        # [1, 2, 3, 4] -> [2, 3, 4, 1]
        cycle1 = cycle1[1:] + [cycle1[0]]
        if cycle1 == cycle2:
            return True
    return False

或者我们可以使用这些循环的属性并得到:

def are_equal(cycle1, cycle2):
    return set(cycle1) == set(cycle2)

因为两个循环相等当且仅当它们包含相同的元素(顺序无关紧要——正如声明所说)。

然后我们需要实现一个函数,使循环从索引 i 开始。

def cycle_at(array, index):
    visited_indices = set()
    cycle = []
    while True:
        cycle.append(index)
        visited_indices.add(index)
        index = array[index]
        if index in visited_indices:
            break
    return cycle

我们需要记住我们访问了哪些索引,我们会将它们存储在一个集合中。 然后我们将 'follow the cycle' 并遍历数组,每次看到新索引时将 index 添加到 visited_indices。 如果在任何时候我们偶然发现了我们已经访问过的索引,我们将结束循环。

您可以通过不考虑循环中已经出现的索引作为起始索引来提高性能,因为这将消除对 are_equal 函数的需要,并且会阻止您找到相同的循环超过一次。

你可以试试这个。

def cycles(indices):

    sub =[]
    i=0
    result = []
    remaining = indices[:]

    while remaining:
        i = indices[i]
        if(i in sub):
            remaining = [ a for a in remaining if a not in sub]
            result.append(sub)
            sub = []
            if remaining:
                i=remaining[0]

        sub.append(i)
    return result

r = cycles([2, 0, 1, 4, 3, 5])
print(r)
#[[2, 1, 0], [4, 3], [5]]

r = cycles([0,1,2,3,4,5])
print(r)
#[[0], [1], [2], [3], [4], [5]]

这看起来是一个完美的图论问题。您可以将列表中的元素连接到它们的索引,并在结果图中查找连接的组件。 networkx 是最流行的 Python 网络分析包:

import networkx as nx

lst = [2, 0, 1, 4, 3, 5]

G = nx.Graph()
G.add_edges_from(enumerate(lst))
print(list(nx.connected_components(G)))
# [{0, 1, 2}, {3, 4}, {5}]

def find_cycles(indices):
    indices = indices.copy()
    cycles = list()
    for a in indices:
        if a is None: continue
        cycle = list()
        while a is not None:
            cycle.append(a)
            indices[a], a = None, indices[a]
        else: cycles.append(cycle[:-1])
    return cycles

print(find_cycles([12, 0, 8, 10, 9, 6, 5, 4, 13, 7, 17,14, 2,18, 16, 1, 11, 19, 3, 15]))
>>> [[12, 2, 8, 13, 18, 3, 10, 17, 19, 15, 1, 0], [9, 7, 4], [6, 5], [14, 16, 11]]

但如果你不需要之后的输入,你可以删除indices = indices.copy()