【问题标题】:Any alternative to a (very slow) deepcopy in a DFS?DFS 中(非常慢的)深拷贝的任何替代方法?
【发布时间】:2012-04-12 17:05:33
【问题描述】:

我有一个包含 81 个顶点的漂亮图(一个列表)(每个顶点都是 Vertex 类的一个实例)。每个顶点有 20 个邻居。每个顶点都有许多可能的值(范围从 1 到 9),给定问题的一些初始约束,平均为 4 或 5。我在此图上实现了一个简单的 DFS,它采用可能值较少的节点, foreach 值构建另一个只有一个可能值的“深度复制”图,最后递归地将“深度复制”图再次传递到 DFS。问题在于速度; cProfiling 我的代码我发现我的 Mac 用来解决这个问题的 641 秒中有 635 秒被 copy.deepcopy 使用。有没有解决这个问题的方法?这是我的 DFS:

def dfs(graph):
    global initial_time_counter

    if all(len(i.possible_values)==1 for i in graph):
        sys.exit("Done in: %s" % (time.time()-initial_time_counter))

    #find out the non-solved vertex with minimum possible values
    min_vertex=sorted(filter(lambda x: len(x.possible_values)>1,graph),
                      key=lambda x: len(x.possible_values))[0]

    for value in min_vertex.possible_values:

        sorted_graph_copy=sorted(copy.deepcopy(graph), key=lambda x: len(x.possible_values))
        min_vertex_copy=filter(lambda x: len(x.possible_values)>1,sorted_graph_copy)[0]
        sorted_graph_copy.remove(min_vertex_copy)

        if min_vertex_copy.try_value(value): #Can this vertex accept value -> value?
            min_vertex_copy.set_value(value) #Yes, set it.
            sorted_graph_copy.append(min_vertex_copy) #Append it to the graph.
            dfs(sorted_graph_copy) #Run the DFS again.
    return False

附:你们中最聪明的人可能已经理解这个问题通常被称为数独。请注意,我不是在寻找特定于数独的答案,请以抽象的方式分析问题。

[编辑]

同样的问题,用顶点的纯字符串表示来解决,需要

import sys,time

def srange():
    return [[x,y] for x in range(9) for y in range(9)]

def represent_sudoku(sudoku):
    print "\n".join(["|".join([str(elem) for elem in line]) for line in sudoku])

#Hard sudoku
sudoku=[[4, 0, 0, 0, 0, 0, 8, 0, 5], [0, 3, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 7, 0, 0, 0, 0, 0], [0, 2, 0, 0, 0, 0, 0, 6, 0], [0, 0, 0, 0, 8, 0, 4, 0, 0], [0, 0, 0, 0, 1, 0, 0, 0, 0], [0, 0, 0, 6, 0, 3, 0, 7, 0], [5, 0, 0, 2, 0, 0, 0, 0, 0], [1, 0, 4, 0, 0, 0, 0, 0, 0]]

represent_sudoku(sudoku)

def get_nbs(x,y,sudoku,also_incomplete=False):
    line_nbs=sum([elem for elem in sudoku[y] if ((elem!=[0] and len(elem)==1) or also_incomplete)],[])

    column_nbs=sum([sudoku[xline][x] for xline in range(9) if ((sudoku[xline][x]!=[0] and len(sudoku[xline][x])==1) or also_incomplete)],[])


    area_nbs=[[j for j in i[(x/3)*3:(x/3)*3+3] if ((j!=[0] and len(j)==1) or also_incomplete)] for i in sudoku[(y/3)*3:(y/3)*3+3]]

    area_nbs=sum(sum(area_nbs,[]),[])    

    if not also_incomplete:
        return list(set(line_nbs+column_nbs+area_nbs))

    return line_nbs+column_nbs+area_nbs

for x,y in srange():
    sudoku[y][x]=[sudoku[y][x]]

def base_cleanup(sudoku):
    while 1:
        something_changed=False
        for x,y in srange():
            if sudoku[y][x]==[0] or len(sudoku[y][x])>1:
                possible_values=range(1,10) if sudoku[y][x]==[0] else sudoku[y][x]
                sudoku[y][x]=list(set(possible_values)-set(get_nbs(x,y,sudoku)))
                if sudoku[y][x]==[]:
                    return False
                something_changed=True if possible_values!=sudoku[y][x] else False
            else:
                sudoku[y][x]=sudoku[y][x]
        if not something_changed:
            break
    return sudoku


def dfs(graph):
    global s

    if graph==False:
        return False

    if all(sum([[len(elem)==1 for elem in line] for line in graph],[])):
        represent_sudoku(graph)
        sys.exit("Done in: %s" % (time.time()-s))

    enumerated_filtered_sudoku=filter(lambda x: len(x[1])>1, enumerate(sum(graph,[])))
    sorted_enumerated_sudoku=sorted(enumerated_filtered_sudoku,key=lambda x: len(x[1]))
    min_vertex=sorted_enumerated_sudoku[0]

    possible_values=[value for value in min_vertex[1]]

    for value in possible_values:        
        graph_copy=[[elem for elem in line] for line in graph]

        y,x=elements_position[min_vertex[0]]

        if not any(value==i for i in get_nbs(x,y,graph_copy)):
            graph_copy[y][x]=[value]
            if base_cleanup(graph_copy)!=False:
                graph_copy=base_cleanup(graph_copy)
                if graph_copy:
                    dfs(graph_copy)

    return False

sudoku = base_cleanup(sudoku)

elements_position = {i:srange()[i] for i in range(81)}
s = time.time()

dfs(sudoku)

【问题讨论】:

  • 使用在 C 中实现了 .copy 方法的 python 图类?可能是(networkx.lanl.gov
  • 假设我想留在 cPython 边界内。
  • 您可以在 C 中创建自己的图形结构并使用 python 包装器在 python 中使用它并创建一个复制方法来复制连续的内存块......我可能会看看那个 networkX 包复制速度更快,但我不能保证,但我认为它提供了 graph.copy() 方法(iirc ...)
  • @luke14free - FWIW 几年前我用 Ruby 写了一个蛮力数独求解器,它从来没有超过一秒钟。我希望 Python 具有可比性或更快。 IIRC 每当它想要建立一个分支以回溯到时,它都会复制整个“板”的状态。我使用了一个相对简单的结构来表示“板”,单元格以及每个单元格共享的行、列和子块。所以复制很简单,而且是“手工”完成的。所以我确实认为还有很大的改进空间。
  • @gbulmer 我同意,我正在重写算法,以避免使用任何比列表/字典更复杂的数据结构,因为我希望有类似的性能。

标签: python algorithm deep-copy depth-first-search


【解决方案1】:

cPickle 比 deepcopy 快:

Line # Hits Time Per Hit % Time Line Contents

15                                           @profile
16                                           def test():
17       100       967139   9671.4     95.0      b = deepcopy(a)
18                                               #c = copy(a)
19                                               #d = ujson.loads(ujson.dumps(a))
20                                               #e = json.loads(json.dumps(a))
21                                               #f = pickle.loads(pickle.dumps(a, -1))
22       100        50422    504.2      5.0      g = cPickle.loads(cPickle.dumps(a, -1))

【讨论】:

  • 哇! cPickle 快得多!感谢您的回答:)
【解决方案2】:

Deepcopy 可能比简单地复制相同数量的数据慢很多,这可能是因为检测循环需要付出所有努力。如果您以一种避免循环的方式自己复制图形(很容易,因为您知道网络拓扑)而不是委托给 deepcopy,那么它可能会给您带来很大的加速。我在复制一个非常简单的数据结构元素方面(使用推导式)获得了 50% 的加速,如果复杂的数据结构能带来更大的节省,我也不会感到惊讶。

当然,如果您可以避免在每一步都制作完整的整个状态副本,则可以获得更大的加速。例如,由于您首先搜索深度,因此您可以切换方法来维护撤消堆栈:只需记录您从中选择的每个选项列表,并在您回溯时恢复它们。

【讨论】:

  • +1ed 但手动复制效果不佳,因为在 python 中创建实例也非常慢。无论如何,我将切换到一种完全不同的方法,我只会实例化对对象的引用,而不是将它们放入邻居中,无论如何感谢您的努力。
  • 有道理。如果那样的话,我希望放弃 deepcopy 会让你降到 200 左右。您应该尝试只复制发生变化的部分,但是您要实现它。
【解决方案3】:

您需要复制整个图表吗?通常,您只会在搜索的任何步骤中修改其中的一小部分,因此使用不可变数据结构并仅重建所需的数据可能更有效。这不适用于循环,但我怀疑你的图表是一个列表?

我在 clojure 中解决了一个类似的问题,它具有对不可变结构的原生支持,并设法以这种方式获得合理的效率。但我不知道任何用于 python 的不可变数据结构库(在某处有一个写时复制列表包 - 就足够了吗?)

[只是用一个简单的例子来澄清一下——你是对的,元组是不可变的,所以如果你有一棵由这样的元组组成的树:(1, (2, 3), (4, (5, 6))) 那么你可以通过创建一个像(1, (2, 99), (4, (5, 6))) 这样的新树只有两个元组 - 您可以复制 (4, (5, 6)) 而无需进行深层复制。 clojure 有什么,而我不知道 python 有什么,是遵循相同原则的更复杂的结构(如哈希表)。它使 dfs 变得非常容易,因为您根本不必担心“上游”更改值。]

ps 只有通过做你正在做的事情,并看到所涉及的成本,我才能理解 norvig 方法的优雅......

【讨论】:

  • 我会试试的,谢谢!顺便提一句。 python 具有不可变的元组 (1,2,3,) 并列出可变的 [1,2,3,]。
【解决方案4】:

我并不完全了解数独的细节,但是如果您使用图表的单个副本并为每个节点提供一个包含以下内容的类:

1) 相邻顶点列表 2) 一个“已访问”标志,可用于跟踪已查看和未查看的内容

?

您的深度优先搜索仍然可以是递归的,但您无需打包图表的修改子集,只需将节点标记为“已访问”并继续,直到无处可去。如果您的图表已连接,它似乎应该可以工作......

【讨论】:

  • 这正是我现在正在做的事情,问题是深度复制这样一个类(由节点列表组成)需要大量时间
猜你喜欢
  • 2017-07-04
  • 2017-03-18
  • 1970-01-01
  • 1970-01-01
  • 2011-04-26
  • 2012-04-12
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多