Implement prune and branch for graph coloring. Already works better than previous solution.
This commit is contained in:
@@ -2,4 +2,11 @@
|
||||
|
||||
## Relation to coloring
|
||||
|
||||
A clique cover of a graph G may be seen as a graph coloring of the complement graph of G, the graph on the same vertex set that has edges between non-adjacent vertices of G. Like clique covers, graph colorings are partitions of the set of vertices, but into subsets with no adjacencies (independent sets) rather than cliques. A subset of vertices is a clique in G if and only if it is an independent set in the complement of G, so a partition of the vertices of G is a clique cover of G if and only if it is a coloring of the complement of G.
|
||||
A clique cover of a graph G may be seen as a graph coloring of the complement
|
||||
graph of G, the graph on the same vertex set that has edges between
|
||||
non-adjacent vertices of G. Like clique covers, graph colorings are partitions
|
||||
of the set of vertices, but into subsets with no adjacencies (independent sets)
|
||||
rather than cliques. A subset of vertices is a clique in G if and only if it is
|
||||
an independent set in the complement of G, so a partition of the vertices of G
|
||||
is a clique cover of G if and only if it is a coloring of the complement of G.
|
||||
|
||||
|
||||
@@ -1,238 +1,153 @@
|
||||
from collections import namedtuple
|
||||
from copy import deepcopy
|
||||
|
||||
colors_max = None
|
||||
|
||||
Graph = namedtuple("Graph", ['vertices', 'cliques'])
|
||||
Vertex = namedtuple("Vertex", ['id', 'adjacent_ids', 'max_clique', 'colors'])
|
||||
class Node(object):
|
||||
|
||||
def __init__(self, index):
|
||||
self.index = index
|
||||
self.neighbors = set()
|
||||
self.colors = set()
|
||||
self.color = None
|
||||
|
||||
def input_data_to_graph(input_data):
|
||||
def __str__(self):
|
||||
ns = len(self.neighbors)
|
||||
cs = self.colors
|
||||
return f"N({self.index}, {cs=}, {ns=}, {self.color})"
|
||||
|
||||
def __repr__(self):
|
||||
return self.__str__()
|
||||
|
||||
def set_only_color(self):
|
||||
assert(len(self.colors) == 1)
|
||||
self.color = self.colors.pop()
|
||||
for nb in list(self.neighbors):
|
||||
nb.colors.discard(self.color)
|
||||
nb.neighbors.remove(self)
|
||||
if len(nb.colors) == 1:
|
||||
nb.set_only_color()
|
||||
|
||||
def parse(input_data):
|
||||
# parse the input
|
||||
lines = input_data.split('\n')
|
||||
vertice_count, edge_count = map(int, lines[0].split())
|
||||
graph = Graph([Vertex(i, set(), set(), set())
|
||||
for i in range(vertice_count)], [])
|
||||
node_count, edge_count = map(int, lines[0].split())
|
||||
nodes = [Node(i) for i in range(node_count)]
|
||||
|
||||
for i in range(1, edge_count + 1):
|
||||
line = lines[i]
|
||||
v_1, v_2 = map(int, line.split())
|
||||
graph.vertices[v_1].adjacent_ids.add(v_2)
|
||||
graph.vertices[v_2].adjacent_ids.add(v_1)
|
||||
return graph
|
||||
n_1, n_2 = map(int, lines[i].split())
|
||||
nodes[n_1].neighbors.add(nodes[n_2])
|
||||
nodes[n_2].neighbors.add(nodes[n_1])
|
||||
return nodes
|
||||
|
||||
|
||||
def find_max_clique(vertex, graph):
|
||||
cliques = [set([vertex.id])]
|
||||
for v_id in vertex.adjacent_ids:
|
||||
v = graph.vertices[v_id]
|
||||
for c in list(cliques):
|
||||
# If the current vertex is adjacent to all
|
||||
# vertices in the clique it is part of that clique.
|
||||
# TODO: use issubset here
|
||||
if len(c) == len(c.intersection(v.adjacent_ids)):
|
||||
c = set(c)
|
||||
c.add(v.id)
|
||||
cliques.append(c)
|
||||
r = sorted(list(cliques), reverse=True)
|
||||
return sorted(r, key=len, reverse=True)[0]
|
||||
def branch(nodes, color):
|
||||
if not nodes:
|
||||
return nodes
|
||||
|
||||
# Find node with minimum number of colors to branch.
|
||||
min_node = None
|
||||
min_n_color = float("inf")
|
||||
next_nodes = []
|
||||
for n in nodes:
|
||||
n_color = len(n.colors)
|
||||
if n_color < min_n_color:
|
||||
if min_node:
|
||||
next_nodes.append(min_node)
|
||||
min_node = n
|
||||
min_n_color = n_color
|
||||
else:
|
||||
next_nodes.append(n)
|
||||
|
||||
if min_n_color == 1:
|
||||
min_node.color = min_node.colors.pop()
|
||||
for nb in min_node.neighbors:
|
||||
nb.colors.discard(min_node.color)
|
||||
nb.neighbors.remove(min_node)
|
||||
return next_nodes
|
||||
|
||||
# This is where we actually have to iterate and branch.
|
||||
min_node.color = min_node.colors.pop()
|
||||
for nb in min_node.neighbors:
|
||||
nb.colors.discard(min_node.color)
|
||||
nb.neighbors.remove(min_node)
|
||||
return search(next_nodes, color)
|
||||
|
||||
|
||||
def preprocess_graph(graph):
|
||||
# print("Adding max clique for each vertex.")
|
||||
print(1)
|
||||
for v in graph.vertices:
|
||||
assert(not v.max_clique)
|
||||
v.max_clique.update(find_max_clique(v, graph))
|
||||
vertices = sorted(graph.vertices,
|
||||
key=lambda v: len(v.max_clique), reverse=True)
|
||||
vertices_left = {v.id for v in graph.vertices}
|
||||
|
||||
# print("Computing max cliques.")
|
||||
print(2)
|
||||
for v in vertices:
|
||||
for v_id in v.max_clique:
|
||||
if v_id in vertices_left:
|
||||
graph.cliques.append(v.max_clique)
|
||||
vertices_left = vertices_left - v.max_clique
|
||||
if not vertices_left:
|
||||
def prune(nodes, color):
|
||||
node = None
|
||||
for n in nodes:
|
||||
if not n.colors:
|
||||
node = n
|
||||
break
|
||||
assert(not vertices_left)
|
||||
return graph
|
||||
|
||||
while node:
|
||||
assert(node.color is None)
|
||||
if colors_max is not None and color < colors_max:
|
||||
raise ValueError("No enough colors left.")
|
||||
node.color = color
|
||||
next_node = None
|
||||
next_nodes = []
|
||||
for n in nodes:
|
||||
if n is node:
|
||||
continue
|
||||
|
||||
if n not in node.neighbors:
|
||||
n.colors.add(color)
|
||||
else:
|
||||
n.neighbors.remove(node)
|
||||
|
||||
if next_node is None and not n.colors:
|
||||
next_node = n
|
||||
|
||||
next_nodes.append(n)
|
||||
|
||||
color += 1
|
||||
nodes = next_nodes
|
||||
node = next_node
|
||||
|
||||
return nodes, color
|
||||
|
||||
|
||||
def search(nodes, color):
|
||||
while nodes:
|
||||
nodes, color = prune(nodes, color)
|
||||
nodes = branch(nodes, color)
|
||||
return nodes
|
||||
|
||||
|
||||
def solve_it(input_data):
|
||||
graph = input_data_to_graph(input_data)
|
||||
if len(graph.vertices) == 50:
|
||||
return solve_it_smart(graph, 6)
|
||||
elif len(graph.vertices) == 500:
|
||||
return solve_it_smart(graph, 16)
|
||||
return solve_it_naiv(graph)
|
||||
global colors_max
|
||||
nodes = parse(input_data)
|
||||
# colors_max = 6
|
||||
nodes.sort(key=lambda n: len(n.neighbors), reverse=True)
|
||||
color = 0
|
||||
search(list(nodes), color)
|
||||
return to_output(nodes, input_data)
|
||||
|
||||
|
||||
def solve_it_smart(graph, num_colors):
|
||||
def to_output(nodes, input_data):
|
||||
nodes.sort(key=lambda n: n.index)
|
||||
test_nodes = parse(input_data)
|
||||
colors = set()
|
||||
|
||||
def compute_cliques():
|
||||
cliques = []
|
||||
for v in vertices:
|
||||
new_cliques = []
|
||||
for c in cliques:
|
||||
if c.issubset(v.adjacent_ids):
|
||||
new_c = c.copy()
|
||||
new_c.add(v.id)
|
||||
new_cliques.append(new_c)
|
||||
new_cliques.append(set([v.id]))
|
||||
cliques += new_cliques
|
||||
new_cliques = []
|
||||
return sorted(cliques, key=len)
|
||||
|
||||
def create_initial_colors():
|
||||
colors = [[] for _ in range(num_vertices)]
|
||||
max_clique = cliques[-1]
|
||||
for i, v_id in enumerate(max_clique):
|
||||
colors[v_id].append(i)
|
||||
for v_id in remaining_indices:
|
||||
colors[v_id] = [i for i in range(num_colors)]
|
||||
return colors
|
||||
|
||||
def prune(colors, changed_indices):
|
||||
for v_id in list(changed_indices):
|
||||
changed_indices = []
|
||||
if len(colors[v_id]) == 1:
|
||||
c = colors[v_id][0]
|
||||
for n_id in vertices[v_id].adjacent_ids:
|
||||
if c in colors[n_id]:
|
||||
colors[n_id].remove(c)
|
||||
changed_indices.append(n_id)
|
||||
if not colors[n_id]:
|
||||
return False
|
||||
return True
|
||||
|
||||
def sort_remaining_indices(remaining_indices):
|
||||
r = []
|
||||
for clique in cliques[::-1]:
|
||||
for i in clique:
|
||||
if i in list(remaining_indices):
|
||||
r.append(i)
|
||||
remaining_indices.remove(i)
|
||||
if not remaining_indices:
|
||||
return r
|
||||
|
||||
vertices = graph.vertices
|
||||
num_vertices = len(vertices)
|
||||
cliques = compute_cliques()
|
||||
max_clique = cliques[-1]
|
||||
remaining_indices = [i for i in range(num_vertices)
|
||||
if i not in max_clique]
|
||||
colors = create_initial_colors()
|
||||
assert(prune(colors, list(max_clique)))
|
||||
remaining_indices = sort_remaining_indices(remaining_indices)
|
||||
|
||||
def search(vertex_ids, colors):
|
||||
if not vertex_ids:
|
||||
return colors
|
||||
|
||||
current_id = -1
|
||||
min_colors = 1000
|
||||
for next_id in vertex_ids:
|
||||
if len(colors[next_id]) < min_colors:
|
||||
min_colors = len(colors[next_id])
|
||||
current_id = next_id
|
||||
vertex_ids.remove(current_id)
|
||||
|
||||
for color in colors[current_id]:
|
||||
new_colors = deepcopy(colors)
|
||||
new_colors[current_id] = [color]
|
||||
if prune(new_colors, [current_id]):
|
||||
r = search(list(vertex_ids), new_colors)
|
||||
if r:
|
||||
return r
|
||||
return False
|
||||
|
||||
colors = search(remaining_indices, colors)
|
||||
for vertex in vertices:
|
||||
cs = colors[vertex.id]
|
||||
assert(len(cs) == 1)
|
||||
vertex.colors.clear()
|
||||
vertex.colors.update((set(cs)))
|
||||
|
||||
return graph_to_result(graph)
|
||||
assert(len(nodes) == len(test_nodes))
|
||||
for i in range(len(test_nodes)):
|
||||
node = nodes[i]
|
||||
test_node = test_nodes[i]
|
||||
assert(test_node.index == node.index)
|
||||
# This works even if we got rid of the neighbors in the algorithm.
|
||||
for neighbor in test_node.neighbors:
|
||||
neighbor = nodes[neighbor.index]
|
||||
assert(node.color != neighbor.color)
|
||||
colors.add(node.color)
|
||||
obj = len(colors)
|
||||
opt = 0
|
||||
colors = " ".join([str(n.color) for n in nodes])
|
||||
return f"{obj} {opt}\n{colors}"
|
||||
|
||||
|
||||
def solve_it_brute_force(graph, max_colors=None):
|
||||
if not max_colors:
|
||||
max_colors = len(graph.vertices)
|
||||
|
||||
def is_color_allowed(color, vertex):
|
||||
for v_id in vertex.adjacent_ids:
|
||||
if color in graph.vertices[v_id].colors:
|
||||
return False
|
||||
return True
|
||||
|
||||
def search(vertex_ids):
|
||||
if not vertex_ids:
|
||||
return True
|
||||
current_id = vertex_ids[0]
|
||||
vertex_ids = vertex_ids[1:]
|
||||
vertex = graph.vertices[current_id]
|
||||
for color in range(max_colors):
|
||||
if is_color_allowed(color, vertex):
|
||||
vertex.colors.add(color)
|
||||
if search(vertex_ids):
|
||||
return True
|
||||
vertex.colors.pop()
|
||||
return False
|
||||
|
||||
vertex_ids = map(lambda v: v.id, sorted(graph.vertices,
|
||||
key=lambda v: len(v.adjacent_ids),
|
||||
reverse=True))
|
||||
search(list(vertex_ids))
|
||||
|
||||
assert(is_graph_valid(graph))
|
||||
return graph_to_result(graph)
|
||||
|
||||
|
||||
def is_graph_valid(graph):
|
||||
for v in graph.vertices:
|
||||
if len(v.colors) != 1:
|
||||
return False
|
||||
c = list(v.colors)[0]
|
||||
for n_id in v.adjacent_ids:
|
||||
n = graph.vertices[n_id]
|
||||
if c in n.colors:
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
def solve_it_naiv(graph):
|
||||
num_vertices = len(graph.vertices)
|
||||
|
||||
def is_color_used(color, vertex, graph):
|
||||
for v_id in vertex.adjacent_ids:
|
||||
if color in graph.vertices[v_id].colors:
|
||||
return True
|
||||
return False
|
||||
|
||||
for v in sorted(graph.vertices,
|
||||
key=lambda v: len(v.adjacent_ids), reverse=True):
|
||||
for color in range(num_vertices):
|
||||
if not is_color_used(color, v, graph):
|
||||
v.colors.add(color)
|
||||
break
|
||||
|
||||
assert(is_graph_valid(graph))
|
||||
return graph_to_result(graph)
|
||||
|
||||
|
||||
def graph_to_result(graph):
|
||||
num_colors = 0
|
||||
xs = []
|
||||
for v in graph.vertices:
|
||||
assert(len(v.colors) == 1)
|
||||
c = v.colors.pop()
|
||||
if c > num_colors:
|
||||
num_colors = c
|
||||
xs.append(str(c))
|
||||
|
||||
output_data = str(num_colors + 1) + ' ' + '0' + '\n'
|
||||
output_data += ' '.join(map(str, xs))
|
||||
|
||||
return output_data
|
||||
if __name__ == "__main__":
|
||||
file_location = "data/gc_50_3"
|
||||
with open(file_location, 'r') as input_data_file:
|
||||
input_data = input_data_file.read()
|
||||
print(solve_it(input_data))
|
||||
|
||||
@@ -7,31 +7,6 @@ import coloring
|
||||
def solve_it(input_data):
|
||||
return coloring.solve_it(input_data)
|
||||
|
||||
# Modify this code to run your optimization algorithm
|
||||
|
||||
# parse the input
|
||||
lines = input_data.split('\n')
|
||||
|
||||
first_line = lines[0].split()
|
||||
node_count = int(first_line[0])
|
||||
edge_count = int(first_line[1])
|
||||
|
||||
edges = []
|
||||
for i in range(1, edge_count + 1):
|
||||
line = lines[i]
|
||||
parts = line.split()
|
||||
edges.append((int(parts[0]), int(parts[1])))
|
||||
|
||||
# build a trivial solution
|
||||
# every node has its own color
|
||||
solution = range(0, node_count)
|
||||
|
||||
# prepare the solution in the specified output format
|
||||
output_data = str(node_count) + ' ' + str(0) + '\n'
|
||||
output_data += ' '.join(map(str, solution))
|
||||
|
||||
return output_data
|
||||
|
||||
|
||||
import sys
|
||||
|
||||
|
||||
@@ -214,7 +214,7 @@ def output(input_file, solver_file):
|
||||
|
||||
solution = ''
|
||||
|
||||
start = time.clock()
|
||||
start = time.process_time()
|
||||
try:
|
||||
solution = pkg.solve_it(load_input_data(input_file))
|
||||
except Exception as e:
|
||||
@@ -224,7 +224,7 @@ def output(input_file, solver_file):
|
||||
print(str(e))
|
||||
print('')
|
||||
return 'Local Exception =('
|
||||
end = time.clock()
|
||||
end = time.process_time()
|
||||
|
||||
if not (isinstance(solution, str) or isinstance(solution, unicode)):
|
||||
print('Warning: the solver did not return a string. The given object will be converted with the str() method.')
|
||||
|
||||
Reference in New Issue
Block a user