将节点添加到一个未连接的图中,以完全连接图组件,并具有节点间距离约束。

7

我有一个图表,其中每个节点都有一个由(x,y)给出的空间位置,仅当每个节点之间的欧几里得距离小于或等于 sqrt(2)时,才连接节点之间的边缘。这是我的例子:

import networkx

G=nx.Graph()

G.add_node(1,pos=(1,1))
G.add_node(2,pos=(2,2))
G.add_node(3,pos=(1,2))

G.add_node(4,pos=(1,4))
G.add_node(5,pos=(2,5))

G.add_node(6,pos=(4,2))
G.add_node(7,pos=(5,2))
G.add_node(8,pos=(5,3))

# Connect component one
G.add_edge(1,2)
G.add_edge(1,3)
G.add_edge(2,3)

# Connect component two
G.add_edge(6,7)

# Connect component three
G.add_edge(6,8)
G.add_edge(7,8)
G.add_edge(4,5)

pos=nx.get_node_attributes(G,'pos')

nx.draw(G,pos)

我的问题是,如何确定最佳位置和附加节点的数量,以使图形组件相连,并确保任何附加节点始终在现有节点的sqrt(2)范围内?

输入图像描述


2
我认为“全连接”可能不是这里的正确词语,因为无论您向图形中添加多少个节点,在约束条件“sqrt(2)或更少”的情况下,您当前的图形永远不会完全连接(例如节点1和8永远不会直接连接)。我相信您想要得到的是一个图形,其中所有节点都可以从其他单词到达。我理解得对吗? - jylls
是的,那是正确的。谢谢,我会更新我的问题。我的意思是图表的组件是相互连接的。 - Anthony W
1
节点位置是否有任何限制?例如,可以安全地假设它们在网格上吗?对于每个连接组件的节点计数和连接组件的形状是否有特定的假设? - SultanOrazbayev
1
如果组件分离得很好,那么这就是国王晶格上的斯坦纳树问题。它在二维空间中可能不适用于NP完全问题,但这表明精确解决方案可能会很困难和/或昂贵。 - Davis Herring
1
可能不太理想的一种选择是沿着图形的周长生成节点(即从节点0开始,通过节点1、2、...、N_nodes),每隔sqrt(2)。在最坏的情况下,您将不得不在整个周长上创建节点,直到所有节点都连接起来。 - jylls
显示剩余2条评论
3个回答

2
由于顶点的最大长度可以为sqrt(2),我们可以假设我们的图需要存在于网格世界中,其中节点具有正整数x和y坐标,并且顶点只能连接附近的节点,受长度限制。
解决方案是逐个扩展组件,使用带来组件更接近的节点和顶点。该问题的连通图将添加节点9和10以及连接的顶点。请参见下面的第一张图片。

enter image description here

除了OP问题中提到的测试之外,似乎更有趣的测试是将节点4放置在位置(3,4)。在这种情况下,最优解是一个斯坦纳点(参见enter link description here)。
为了构建这样的小图解决方案,我们可以使用一种算法,添加一个节点和一条连接最近的一对节点之一的边缘。为了适应构建斯坦纳树,我们允许在对角线位置选择点的位置。也就是说,节点2和6具有相同的y坐标,但我们将对角线地创建节点9,以便我们可以建立到下一个最近邻居的桥梁。请参见下面的图片。

Connected graph on modified problem

为了避免建立额外的节点,应该逐步添加节点。由于我们已经给节点编号,因此可以通过选择最小的节点编号来进行操作。这会导致在需要跨越更多空间时生长更大的斯坦纳树。请参见下图,在原始图中添加节点9到12。

enter image description here

这个(且非常低效的)粗略算法如下所示。欧几里得距离函数是从Russell&Norvig(Stuart Russell和Peter Norvig的“人工智能现代方法”Github库)修改而来的。
import itertools
import networkx as nx
import numpy as np

# Assumes graph G created same or similar to code in OP's question

def closest_nodes(c1, c2, components):
    nodes_c1 = [(n, G.nodes[n]['pos']) for n in components[c1]]
    nodes_c2 = [(n,G.nodes[n]['pos']) for n in components[c2]]
    xnodes = itertools.product(nodes_c1, nodes_c2)
    closest = min(xnodes, key=lambda x: euclidean_distance(x[0], x[1]))
    return euclidean_distance(closest[0], closest[1]), closest

def next_closest_node(n1, c1, c2, components):
    other_nodes = []
    for i,v in enumerate(components):
        if i == c1 or i == c2:
            continue
        other_nodes.extend((n, G.nodes[n]['pos']) for n in v)
    if len(other_nodes) == 0:
        return None
    # print('next closest', n1, other_nodes)
    closest = min(other_nodes, key=lambda x: euclidean_distance(x, n1))
    return closest

def closest_comps(components):
    xcomps = itertools.combinations(range(len(components)), 2)
    closest = min(xcomps,
                  key=lambda x: closest_nodes(x[0], x[1], components)[0])
    return closest

def euclidean_distance(x, y):
    xc = x[1]
    yc = y[1]
    # print('e_d', x, y)
    return np.sqrt(sum((_x - _y) ** 2 for _x, _y in zip(xc, yc)))

def determine_pos(sel_comps, sel_nodes, components):
    next_closest = next_closest_node(sel_nodes[1][0], *sel_comps, components)
    # Grow from the smallest node number
    if sel_nodes[1][0][0] < sel_nodes[1][1][0]:
        from_node = sel_nodes[1][0][0]
        start_pos = sel_nodes[1][0][1]
        target_pos = sel_nodes[1][1][1]
    else:
        from_node = sel_nodes[1][1][0]
        start_pos = sel_nodes[1][1][1]
        target_pos = sel_nodes[1][0][1]
    diff_x = target_pos[0]-start_pos[0]
    diff_y = target_pos[1]-start_pos[1]
    new_x, new_y = start_pos[0], start_pos[1]
    if diff_x != 0:
        new_x += diff_x//abs(diff_x)
    elif next_closest is not None:
        diff_x = next_closest[1][0]-start_pos[0]
        if diff_x != 0:
            new_x += diff_x//abs(diff_x)
    if diff_y != 0:
        new_y += diff_y//abs(diff_y)
    elif next_closest is not None:
        diff_y = next_closest[1][1]-start_pos[1]
        if diff_y != 0:
            new_y += diff_y//abs(diff_y)
    return from_node, (new_x, new_y)

while not nx.is_connected(G):
    components = [c for c in
                  sorted(nx.connected_components(G), key=len, reverse=True)]
    sel_comps = closest_comps(components)
    sel_nodes = closest_nodes(*sel_comps, components)
    sel_dist = euclidean_distance(sel_nodes[1][0], sel_nodes[1][1])
    edge_needed = sel_dist <= np.sqrt(2)
    if edge_needed:
        G.add_edge(sel_nodes[1][0][0], sel_nodes[1][1][0])
    else:
        new_pos = determine_pos(sel_comps, sel_nodes, components)
        new_node = G.number_of_nodes() + 1
        G.add_node(new_node, pos=new_pos[1])
        G.add_edge(new_pos[0], new_node)
    pos = nx.get_node_attributes(G, 'pos')
    nx.draw(G, pos, with_labels=True)

感谢您为此付出的努力,非常感激。我在一个100个节点的图上尝试了您的解决方案,正如您所建议的那样,它需要一些时间才能完成。 - Anthony W
1
非常好。如果您喜欢这个答案,请考虑接受它作为答案。就效率而言,有些事情可以做到显著减少运行时间。例如,在二维图中,应该很容易识别出所有最接近的节点(那些最接近任何来自其他组件的节点)。这些最接近的节点将定义要构建的Steiner树;其他节点可以从图中丢弃,它们不会影响过程。这可以大大减少要评估的节点数。 - VirtualScooter

1

我尝试使用遗传算法解决上述问题。我最初的猜测是,两个额外的节点将连接所有三个不相连的组件。

import networkx as nx
import pygad
import math
from libpysal import weights
import numpy as np

no_comps_target = 1                    # a connected graph has 1 component
max_dist_between_nodes = math.sqrt(2)  # in km
num_pts = 2                            # number of additional nodes to add
num_genes = num_pts*2                  # number of genes required with the GA. A gene each for x and y coordinates.

# Generate coordinate np array of existing components
centroids = np.array([(v) for k, v in pos.items()])

# Create indcies for new nodes within the GA solution list
y_ix = [x for x in range(num_genes) if x % 2 != 0] 
x_ix = [x for x in range(num_genes) if x % 2 == 0] 

# Define fitness function
def my_fitness_func(solution, solution_idx):
    
    # Select coordinates of GA solution
    xs = np.array(solution[x_ix])
    ys = np.array(solution[y_ix])
    new_pts = np.column_stack((xs,ys))
   
    # Create one set for all coordinates
    all_pts = np.append(centroids, new_pts,axis=0)
        
    # Calculate weights using a distance band equal to the max distance between nodes
    w = weights.DistanceBand.from_array(all_pts, 
                                        threshold=max_dist_between_nodes, 
                                        silence_warnings=True)

    # Convert to a networkx obejct
    G = w.to_networkx()
    
    # Calculate the number of graph components for G
    # Target is 1 - fully connected graph
    no_comps_solution = nx.number_connected_components(G)
         
    
    # Calculate solution fitness
    fitness = 1.0 / np.abs(no_comps_target - no_comps_solution + 0.000001)
        
        return fitness
# Set constraints on possible solution locations
x_max = 5
x_min = 0
y_max = 5
y_min = 1

ga_instance = pygad.GA(
    num_generations=20,
    num_parents_mating=2,
    fitness_func=my_fitness_func,
    sol_per_pop=10,
    num_genes=num_genes,
    gene_type= int,
    gene_space= [{'low': x_min, 'high': x_max, 'step': 1}, 
                 {'low': y_min, 'high': y_max, 'step': 1}] * num_pts,
    mutation_num_genes=1,
    parent_selection_type = "sss",
    keep_parents =2,
    stop_criteria="saturate_10" # Stop if no progress after 10 generations
)

ga_instance.run()

# If final reached a maximum we should expect a fitness of 100,000.
solution, solution_fitness, solution_idx = ga_instance.best_solution()
print("Parameters of the best solution : {solution}".format(solution=solution))
print("Fitness value of the best solution = {solution_fitness}".format(solution_fitness=solution_fitness))

这提供了一个有效的解决方案:
Parameters of the best solution : [3 3 2 4]
Fitness value of the best solution = 1000000.0

我运行了多次,得到了多个有效的解决方案。这很有意义。另外,如何确定额外节点的最佳数量?特别是如果这个问题变得更加庞大。我仍然想知道是否有其他解决这个问题的方法。特别是如果它们的代码更少!


1
你的适应函数没有考虑到创建附加节点的数量。例如,通过创建节点(1,3),(3,3)和(2,4),也可以达到100,000的适应度。您回答中的解决方案应该具有更高的适应度。我分享您对此类算法用于更大问题空间的担忧。 - VirtualScooter
上述解决方案的问题在于健康函数中的节点数量是固定的。可以多次运行该函数,从一个节点开始,然后每次增加一个节点,直到找到所有组件都连接的解决方案。这将需要更多的处理,但应该会产生具有最小数量附加节点的连接组件图形。 - Anthony W

1
我相当确信这个问题是NP难问题。我所知道的最接近的问题是具有八向度量的几何Steiner树问题。
我有两个快速且简单的建议,但都是启发式的。
第一种方法:将问题制定为欧几里得斯坦纳树问题(https://en.wikipedia.org/wiki/Steiner_tree_problem#Euclidean_Steiner_tree),只考虑问题的节点,暂时忽略边缘。使用GeoSteiner解决问题: http://www.geosteiner.com/ 这应该可以快速解决包含10000个或更多节点的问题(如果需要解决更大的问题,可以在完全斯坦纳树生成后将问题写出来,并使用https://scipjack.zib.de/)。没有Python接口,但只需将问题写入纯文本文件,语法非常简单。然后,将其他节点放入由GeoSteiner提供的解决方案中,以满足\sqrt(2)条件。最后,您需要进行一些清理工作,以摆脱冗余的边缘,因为解决方案不会考虑到您原始问题中已经有的边缘。将迄今为止计算出的所有边缘和节点组成一个加权图,将所有原始边缘的权重设置为0,所有新添加的边缘的权重设置为1。在这个加权图上考虑一个图中的斯坦纳树问题(https://en.wikipedia.org/wiki/Steiner_tree_problem#Steiner_tree_in_graphs_and_variants),其中终端集对应于您的原始节点。使用SCIP-Jack解决这个斯坦纳树问题:https://scipjack.zib.de/
第二个想法:将您的问题直接视为图中的斯坦纳树问题,具体如下:每条原始边缘都被赋予权重0,将所有原始节点视为终端。在距离彼此不超过\sqrt(2)的地方添加额外的节点和边缘。例如,您可以在所有连接组件周围放置一个大矩形,并从每个节点以0度、45度、90度等角度递归地添加额外的8个节点,距离为\sqrt(2),并在图中的斯坦纳树问题中带有权重1的边缘,只要它们在矩形内部即可。如果其中一个节点与您的原始节点之一的距离在\sqrt(2)之内,则使用权重为1的边缘直接将它们连接起来。使用SCIP-Jack解决相应的图中斯坦纳树问题。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接