如何在Python中建立这张图?

时间:2019-04-03 22:42:30

标签: python graph tree

我正在练习Python,想要在python中创建一个图形,并带有一个起始节点和对子级的一些要求。每个节点的值将为3位数字(例如320、110),我想按以下顺序生成子代:

  • 从第一位数字中减去1
  • 1被添加到第一位
  • 从第二个数字中减去1
  • 1被添加到第二个数字
  • 从第三位数字中减去1
  • 1被添加到第三位

起始节点和目标节点的输入来自文本文件,它们可能在第三行包含禁止编号列表,这些编号是搜索算法无法访问的编号。

约束:

  • 您不能添加到数字9或从数字0减去;
  • 您无法采取行动将当前数字转换为以下数字之一 禁止的数字;
  • 您不能连续两次移动相同的数字。

请注意,由于数字有3位数字,因此从开始节点开始最多有6个可能的移动。 第一次移动后,由于移动的限制,尤其是由于限制3,分支因子最多为4。

我已经为我的图形实现了Node类,但是在实际构建图形时遇到了问题。

这是我对Node类所做的:

class Node(object):
    def __init__(self, data):
        self.data = data
        self.children = []
        self.parent = []

    def add_child(self, obj):
        self.children.append(obj)

    def add_parent(self, obj):
        self.parent.append(obj)

root = Node(320)

def get_root():
    print(root.data)

# some things I've tried
# p = Node(root.data-100)
# p.add_parent(root.data)
# root.add_child(p.data)
# set_root(320)
get_root()
# print(root.data)
# print(root.children)
# print(p.parent)
# p = Node(root.data-100)

我已经实现了一个BFS,该BFS在给出图形时会提供正确的路径输出,但是我无法创建实际的图形以在此BFS中使用。这是我的BFS:

visited = set()

def bfs(graph_to_search, start, end):
    queue = [[start]]
    # visited = set()

    while queue:
        # Gets the first path in the queue
        path = queue.pop(0)

        # Gets the last node in the path
        vertex = path[-1]

        # Checks if we got to the end
        if vertex == end:
            return path
        # We check if the current node is already in the visited nodes 
            set in order not to recheck it
        elif vertex not in visited:
            # enumerate all adjacent nodes, construct a new path 
                    and push it into the queue
            for current_neighbour in graph_to_search.get(vertex[]):
                new_path = list(path)
                new_path.append(current_neighbour)
                queue.append(new_path)

            # Mark the vertex as visited
            visited.add(vertex)

示例: 在起始节点为320,结束节点为110且没有禁止节点的情况下,此图上的BFS搜索如下所示:

enter image description here

enter image description here

任何帮助将不胜感激。谢谢。

1 个答案:

答案 0 :(得分:2)

首先要创建Node的模型和生成图形的方法,我们必须做一些假设:

  • 它是无向图
  • 节点之间的距离相等或不重要
  • Node将需要某种识别码
  • 邻居的产生是相对于当前Node的,因此功能应该是Node实例的一部分
  • 如果我们未指定限制,则Graph可能会无限生成,因此我们必须引入max_spread的概念

因此Node的代码如下:

from copy import copy

def check_three_digits(value_name, digits):
    assert len(digits) == 3, "The {} should be of precise length 3. Actual: {}".format(value_name, digits)
    assert digits.isdigit(), "The {} should consist of 3 digits. Actual {}".format(value_name, digits)


class Node:

    _node_count = 0

    def __init__(self, data: str):
        check_three_digits("data param", data)
        self._id = Node._node_count
        self._data = data
        self._neighbours = []
        Node._node_count += 1

    @property
    def id(self):
        return self._id

    @property
    def data(self):
        return copy(self._data)

    @property
    def neighbours(self):
        return copy(self._neighbours)

    def add_neighbour(self, neighbour):
        self._neighbours.append(neighbour)

    def _new_neighbour(self, data):
        new_neighbour = Node(data)
        new_neighbour.add_neighbour(self)
        return new_neighbour

    def generate_neighbours(self, forbidden_nodes_digits=[]):
        first_digit = self._data[0]
        second_digit = self._data[1]
        third_digit = self._data[2]

        first_digit_num = int(first_digit)
        second_digit_num = int(second_digit)
        third_digit_num = int(third_digit)
        sub_first_digit_num = first_digit_num - 1
        add_first_digit_num = first_digit_num + 1

        sub_second_digit_num = second_digit_num - 1
        add_second_digit_num = second_digit_num + 1

        sub_third_digit_num = third_digit_num - 1
        add_third_digit_num = third_digit_num + 1

        sub_first_digit_num = first_digit_num if sub_first_digit_num < 0 else sub_first_digit_num
        add_first_digit_num = first_digit_num if add_first_digit_num > 9 else add_first_digit_num

        sub_second_digit_num = second_digit_num if sub_second_digit_num < 0 else sub_second_digit_num
        add_second_digit_num = second_digit_num if add_second_digit_num > 9 else add_second_digit_num

        sub_third_digit_num = third_digit_num if sub_third_digit_num < 0 else sub_third_digit_num
        add_third_digit_num = third_digit_num if add_third_digit_num > 9 else add_third_digit_num


        for ndigits in [
            "{}{}{}".format(str(sub_first_digit_num), second_digit, third_digit),
            "{}{}{}".format(str(add_first_digit_num), second_digit, third_digit),
            "{}{}{}".format(first_digit, str(sub_second_digit_num), third_digit),
            "{}{}{}".format(first_digit, str(add_second_digit_num), third_digit),
            "{}{}{}".format(first_digit, second_digit, str(sub_third_digit_num)),
            "{}{}{}".format(first_digit, second_digit, str(add_third_digit_num)),
        ]:
            if ndigits in forbidden_nodes_digits:
                continue

            self._neighbours.append(self._new_neighbour(ndigits))


    def __repr__(self):
        return str(self)

    def __str__(self):
        return "Node({})".format(self._data)

为了生成图,我们有:

def generate_nodes(node, end_node_digits, forbidden_nodes_digits, visited_nodes=None, current_spread=0, max_spread=4):
    """
    Handles the generation of the graph.

    :node: the current node to generate neighbours for
    :end_node_digits: the digits at which to stop spreading further the graph from the current spread.
    :visited_nodes: Marks the nodes for which neighbours generation happened, to avoid repetition and infinite recursion.
    :current_spread: Marks the current level at which neighbours are being generated.
    :max_spread: Defined the max spread over which the graph should no longer generate neighbours for nodes.
    """

    # initialize the kwargs with None values
    if visited_nodes is None:
        visited_nodes = []

    # mark the current node as visited
    visited_nodes.append(node.id)

    # no reason to generate further since we hit the max spread limit
    if current_spread >= max_spread:
        return

    # generate the neighbours for the current node
    node.generate_neighbours(forbidden_nodes_digits)

    # if we generated the end node, fall back, no need to generate further
    if end_node_digits in [n.data for n in node.neighbours]:
        return

    # make sure to generate neighbours for the current node's neighbours as well
    for neighbour in node.neighbours:
        if neighbour.id in visited_nodes:
            continue

        generate_nodes(
            neighbour, end_node_digits, forbidden_nodes_digits,
            visited_nodes=visited_nodes, current_spread=current_spread + 1, max_spread=max_spread
        )

用于这种模型的广度优先搜索算法如下:

def bfs(node, end_node_digits, visited_nodes=None, path=None):
    """
    Looks for a specific digit sequence in the graph starting from a specific node.
    :node: the node to start search from.
    :end_node_digits: The digit sequence to look for.
    :visited_nodes: The nodes for which BFS was already performed. Used to avoid infinite recursion and cyclic traversal.
    :path: The search path that lead to this node.
    """

    # initialize the None kwargs
    if visited_nodes is None:
        visited_nodes = []

    if path is None:
        path = ""
    path += "({}, {}) ".format(node.id, node.data)

    # mark the current node as visited
    visited_nodes.append(node.id)

    # if we find the end node we can safely report back the match
    if node.data == end_node_digits:
        return path

    # if the current node doesn't match the end node then we look into the neighbours
    for neighbour in node.neighbours:

        # exclude the visited nodes (obviously excluding the node that generated these nodes)
        if neighbour.id in visited_nodes:
            continue

        # do a BFS in the subdivision of the graph
        result_path = bfs(neighbour, end_node_digits, visited_nodes, path)

        # if a match was found in the neighbour subdivision, report it back
        if result_path is not None:
            return result_path

    return None

我们可以通过以input.txt为例来举例说明所编写代码的功能:

320
221
330 420

__main__块,如:

if __name__ == '__main__':

    # retrieve the nodes from the input file
    start_node_digits = None
    end_node_digits = None
    forbidden_nodes_digits = []

    with open("input.txt", "r") as pf:
        start_node_digits = pf.readline().strip()
        end_node_digits = pf.readline().strip()
        forbidden_nodes_digits = pf.readline().split()

    forbidden_nodes_digits = [fnode.strip() for fnode in forbidden_nodes_digits]
    print("Start node digits: {}".format(start_node_digits))
    print("End node digits: {}".format(end_node_digits))
    print("Forbidden nodes digits: {}".format(forbidden_nodes_digits))

    # validate the input nodes data
    check_three_digits("start node", start_node_digits)
    check_three_digits("end node", end_node_digits)
    for fnode_digits in forbidden_nodes_digits:
        check_three_digits("forbidden node", fnode_digits)

    # create the first node and generate the graph
    first_node = Node(start_node_digits)
    print("Generate nodes for graph....")
    max_spread = 2
    generate_nodes(first_node, end_node_digits, forbidden_nodes_digits, max_spread=max_spread)

    # poerform a BFS for a sequence of digits
    print("BFS for {}".format(end_node_digits))
    match_path = bfs(first_node, end_node_digits)
    print("BFS search result: {}".format(match_path))

我们还可以使用以下功能来可视化图形:

import networkx as nx
import matplotlib.pyplot as plt

def _draw_node(graph, node, visited_nodes=None):

    # initialize kwargs with None values
    if visited_nodes is None:
        visited_nodes = []

    # mark node as visited
    visited_nodes.append(node.id)

    for neighbour in node.neighbours:
        if neighbour.id in visited_nodes:
            continue

        graph.add_node(neighbour.id)
        graph.add_edge(node.id, neighbour.id)
        nx.set_node_attributes(graph, {neighbour.id: {'data': neighbour.data}})

        _draw_node(graph, neighbour, visited_nodes)


def draw_graph(first_node, start_node_digits, end_node_digits, forbidden_nodes_digits, fig_scale, fig_scale_exponent=1.2):
    g = nx.Graph()

    # add first node to the draw figure
    g.add_node(first_node.id)
    nx.set_node_attributes(g, {first_node.id: {'data': first_node.data}})
    _draw_node(g, first_node)

    # prepare graph drawing
    labels = nx.get_node_attributes(g, 'data')
    fig = plt.figure(frameon=False)
    INCH_FACTOR = 5  # inches
    fig_scale = fig_scale ** fig_scale_exponent
    fig.set_size_inches(fig_scale * INCH_FACTOR, fig_scale * INCH_FACTOR)

    nodes_attributes = nx.get_node_attributes(g, 'data')

    color_map = []
    for n in g:
        ndata = nodes_attributes[n]
        if ndata == start_node_digits:
            color_map.append('yellow')
        elif ndata == end_node_digits:
            color_map.append('cyan')
        elif ndata in forbidden_nodes_digits:
            # just in case something slips
            color_map.append('red')
        else:
            color_map.append("#e5e5e5")

    # actually draw the graph and save it to a PNG.
    nx.draw_networkx(
        g, with_labels=True, labels=labels, node_size=600,
        node_color=color_map,
        # node_color='#e5e5e5',
        font_weight='bold', font_size="10",
        pos=nx.drawing.nx_agraph.graphviz_layout(g)
    )
    plt.savefig("graph.png", dpi=100)

可以在__main__块中调用,例如:

print("Draw graph...")
draw_graph(first_node, start_node_digits, end_node_digits, forbidden_nodes_digits, fig_scale=max_spread, fig_scale_exponent=1)

图形如下:

graph

其BFS结果将类似于:(0, 320) (1, 220) (10, 221)

现在我不确定这是否完全符合规范,但这应该是一个很好的起点。实现图的方法也有多种,有些人使用顶点和边的列表。

对于networkx的{​​{1}},您需要通过pip graphviz软件包进行安装;如果您使用的是Linux,则可能需要执行pygraphviz < / p>