我有一个图表,它有两个不同的节点类,A类节点和B类节点。
A类节点未连接到任何其他A节点,B类节点未连接到任何其他B节点,但B节点连接到A节点,反之亦然。一些B节点连接到许多A节点,大多数A节点连接到许多B节点。
我想从图中消除尽可能多的A节点。
我必须保留所有B节点,并且它们仍然必须连接到至少一个A节点(最好只连接一个A节点)。
当没有连接B节点的A节点时,我可以消除它。是否有任何算法可以找到最佳的,或至少接近最佳的解决方案,我可以删除哪些A节点?
答案 0 :(得分:2)
旧的,不正确的答案,但从这里开始
首先,您需要认识到自己拥有bipartite graph。也就是说,您可以将节点着色为红色和蓝色,这样就没有边缘将红色节点连接到红色节点,或者将蓝色节点连接到蓝色节点。
接下来,确认您正在尝试解决vertex cover problem。来自维基百科:
在图论的数学学科中,图的顶点覆盖(有时是节点覆盖)是一组顶点,使得图的每个边都入射到该集的至少一个顶点。寻找最小顶点覆盖的问题是计算机科学中的经典优化问题,并且是具有近似算法的NP难优化问题的典型示例。
由于您有一个特殊的图表,因此认为NP-hard不适用于您是合理的。这个想法将我们带到Kőnig's theorem,它将最大匹配问题与最小顶点覆盖问题联系起来。一旦你知道了这一点,就可以应用Hopcroft–Karp algorithm来解决 O(| E |√| V |)时间内的问题,不过你可能需要稍微摇晃一下确保您保留所有 B 节点。
新的,正确的答案
事实证明,这种跳汰是创建了一个“约束二分图顶点覆盖问题”,它询问我们是否存在使用少于 a A节点且小于<的顶点覆盖em> b B节点。问题是NP完全,所以这是不行的。起跳比我想象的要难!
但是使用少于最小节点数不是我们想要的约束。我们希望确保使用最少数量的A节点和最大B节点数。
上面的Kőnig定理是最大流问题的一个特例。从流量方面考虑问题,我们很快就会minimum-cost flow problems。
在这些问题中,我们给出了一个图表,其边缘具有指定的运输能力和单位成本。目标是找到将给定数量的供应从任意一组源节点移动到任意一组汇聚节点所需的最低成本。
事实证明,您的问题可以转换为最低成本流量问题。为此,让我们生成一个连接到所有 A 节点的源节点和一个连接到所有 B 节点的汇聚节点。
现在,让我们将使用 Source-&gt; A 边缘的成本等于1,并将所有其他边缘的成本设为零。此外,让我们使 Source-&gt; A 边缘的容量等于无穷大,所有其他边缘的容量等于1.
如下所示:
红色边缘的成本= 1,容量= Inf。蓝色边缘的Cost = 0,Capacity = 1。
现在,解决最小流量问题等同于使用尽可能少的红色边缘。任何未使用的红色边缘都会将0流分配给其对应的A节点,并且可以从图中删除该节点。相反,每个B节点只能将1个单元的流量传递给接收器,因此必须保留所有B节点才能解决问题。
由于我们已将您的问题重新编入此标准表单,因此我们可以利用现有工具来获得解决方案;即Google's Operation Research Tools。
这样做会对上图给出以下答案:
未使用红色边缘并使用黑色边缘。请注意,如果红色边缘从源出现,则它连接的A节点不会产生黑色边缘。另请注意,每个B节点至少有一个进入黑边。这满足了你提出的限制。
我们现在可以通过查找 Source-&gt; A 边缘而无需使用来检测要删除的A节点。
源代码
生成上述图和相关解决方案所需的源代码如下:
#!/usr/bin/env python3
#Documentation: https://developers.google.com/optimization/flow/mincostflow
#Install dependency: pip3 install ortools
from __future__ import print_function
from ortools.graph import pywrapgraph
import matplotlib.pyplot as plt
import networkx as nx
import random
import sys
def GenerateGraph(Acount,Bcount):
assert Acount>5
assert Bcount>5
G = nx.DiGraph() #Directed graph
source_node = Acount+Bcount
sink_node = source_node+1
for a in range(Acount):
for i in range(random.randint(0.2*Bcount,0.3*Bcount)): #Connect to 10-20% of the Bnodes
b = Acount+random.randint(0,Bcount-1) #In the half-open range [0,Bcount). Offset from A's indices
G.add_edge(source_node, a, capacity=99999, unit_cost=1, usage=1)
G.add_edge(a, b, capacity=1, unit_cost=0, usage=1)
G.add_edge(b, sink_node, capacity=1, unit_cost=0, usage=1)
G.node[a]['type'] = 'A'
G.node[b]['type'] = 'B'
G.node[source_node]['type'] = 'source'
G.node[sink_node]['type'] = 'sink'
G.node[source_node]['supply'] = Bcount
G.node[sink_node]['supply'] = -Bcount
return G
def VisualizeGraph(graph, color_type):
gcopy = graph.copy()
for p, d in graph.nodes(data=True):
if d['type']=='source':
source = p
if d['type']=='sink':
sink = p
Acount = len([1 for p,d in graph.nodes(data=True) if d['type']=='A'])
Bcount = len([1 for p,d in graph.nodes(data=True) if d['type']=='B'])
if color_type=='usage':
edge_color = ['black' if d['usage']>0 else 'red' for u,v,d in graph.edges(data=True)]
elif color_type=='unit_cost':
edge_color = ['red' if d['unit_cost']>0 else 'blue' for u,v,d in graph.edges(data=True)]
Ai = 0
Bi = 0
pos = dict()
for p,d in graph.nodes(data=True):
if d['type']=='source':
pos[p] = (0, Acount/2)
elif d['type']=='sink':
pos[p] = (3, Bcount/2)
elif d['type']=='A':
pos[p] = (1, Ai)
Ai += 1
elif d['type']=='B':
pos[p] = (2, Bi)
Bi += 1
nx.draw(graph, pos=pos, edge_color=edge_color, arrows=False)
plt.show()
def GenerateMinCostFlowProblemFromGraph(graph):
start_nodes = []
end_nodes = []
capacities = []
unit_costs = []
min_cost_flow = pywrapgraph.SimpleMinCostFlow()
for node,neighbor,data in graph.edges(data=True):
min_cost_flow.AddArcWithCapacityAndUnitCost(node, neighbor, data['capacity'], data['unit_cost'])
supply = len([1 for p,d in graph.nodes(data=True) if d['type']=='B'])
for p, d in graph.nodes(data=True):
if (d['type']=='source' or d['type']=='sink') and 'supply' in d:
min_cost_flow.SetNodeSupply(p, d['supply'])
return min_cost_flow
def ColorGraphEdgesByUsage(graph, min_cost_flow):
for i in range(min_cost_flow.NumArcs()):
graph[min_cost_flow.Tail(i)][min_cost_flow.Head(i)]['usage'] = min_cost_flow.Flow(i)
def main():
"""MinCostFlow simple interface example."""
# Define four parallel arrays: start_nodes, end_nodes, capacities, and unit costs
# between each pair. For instance, the arc from node 0 to node 1 has a
# capacity of 15 and a unit cost of 4.
Acount = 20
Bcount = 20
graph = GenerateGraph(Acount, Bcount)
VisualizeGraph(graph, 'unit_cost')
min_cost_flow = GenerateMinCostFlowProblemFromGraph(graph)
# Find the minimum cost flow between node 0 and node 4.
if min_cost_flow.Solve() != min_cost_flow.OPTIMAL:
print('Unable to find a solution! It is likely that one does not exist for this input.')
sys.exit(-1)
print('Minimum cost:', min_cost_flow.OptimalCost())
ColorGraphEdgesByUsage(graph, min_cost_flow)
VisualizeGraph(graph, 'usage')
if __name__ == '__main__':
main()
答案 1 :(得分:2)
尽管这是一个老问题,但我仍未正确回答。
与此类似的问题也在this post中得到了解答。
您在此处提出的问题确实是最小集覆盖问题,这是众所周知的NP难题。在Wikipedia中,最小集覆盖问题可以表示为:
给定一组元素{1,2,...,n}(称为Universe)和m个集合的集合S(其集合等于宇宙),集合覆盖问题是确定的最小子集合。 S的联合等于宇宙的S。例如,考虑宇宙U = {1,2,3,4,5}和集合S = {{1,2,3},{2,4},{3,4},{4, 5}}。显然,S的并集是U。但是,我们可以使用以下较少的集合来覆盖所有元素:{{1,2,3},{4,5}}。
在您的公式中,B个节点表示宇宙中的元素,A个节点表示A个节点之间的集合和边缘,而B个节点确定哪些元素(B个节点)属于每个集合(A个节点)。然后,最小集合覆盖率等于A节点的最小数量,以便它们连接到所有B节点。因此,在连接到每个B节点时可以从图中删除的A节点的最大数量是不属于最小集合覆盖的那些。
由于它是NP难解的,因此没有用于计算最优值的政治时间算法,但是一种简单的贪心算法可以有效地为最优值提供严格的近似解。来自Wikipedia:
有一种贪婪的集合覆盖多项式时间逼近算法,该算法根据一个规则选择集合:在每个阶段,选择包含最多未发现元素的集合。