Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Zykov and Christofides Algorithms for Chromatic Number #491

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions doc/attr.xml
Original file line number Diff line number Diff line change
Expand Up @@ -1441,6 +1441,10 @@ gap> DigraphLoops(D);
<Cite Key="Law1976"/></Item>
<Item><C>byskov</C> - Byskov's Algorithm
<Cite Key="Bys2002"/></Item>
<Item><C>zykov</C> - Zykov's Algorithm
<Cite Key="Corneil1973"/></Item>
<Item><C>christofides</C> - Christofides's Algorithm
<Cite Key="Wang1974"/></Item>
</List>

<Example><![CDATA[
Expand Down
32 changes: 32 additions & 0 deletions doc/digraphs.bib
Original file line number Diff line number Diff line change
Expand Up @@ -183,3 +183,35 @@ @article{Bys2002
Journal = {BRICS Report Series},
Doi = {10.7146/brics.v9i45.21760}
}

@article{Corneil1973,
author = {Corneil, D. G. and Graham, B.},
title = {An Algorithm for Determining the Chromatic Number of a Graph},
journal = {SIAM Journal on Computing},
volume = {2},
number = {4},
pages = {311-318},
year = {1973},
doi = {10.1137/0202026},
URL = {https://doi.org/10.1137/0202026},
eprint = {https://doi.org/10.1137/0202026}
}

@article{Wang1974,
author= {Wang, Chung C.},
title = {An Algorithm for the Chromatic Number of a Graph},
year = {1974},
issue_date = {July 1974},
publisher = {Association for Computing Machinery},
address = {New York, NY, USA},
volume = {21},
number = {3},
issn = {0004-5411},
url = {https://doi.org/10.1145/321832.321837},
doi = {10.1145/321832.321837},
abstract = {Christofides' algorithm for finding the chromatic number of a graph is improved both in speed and memory space by using a depth-first search rule to search for a shortest path in a reduced subgraph tree.},
journal = {J. ACM},
month = jul,
pages = {385–391},
numpages = {7}
}
179 changes: 179 additions & 0 deletions gap/attr.gi
Original file line number Diff line number Diff line change
Expand Up @@ -369,6 +369,181 @@ function(D)
end
);

BindGlobal("DIGRAPHS_ChromaticNumberZykov",
function(D)
local nr, ZykovReduce, chrom;
nr := DigraphNrVertices(D);
# Recursive function call
ZykovReduce := function(D)
local nr, D_contract, adjacent, vertices, v, x, y, x_i, y_i, found, deg;
nr := DigraphNrVertices(D);
# Update upper bound if possible.
chrom := Minimum(nr, chrom);
# Leaf nodes are either complete graphs or q-cliques. The chromatic number
# is then the smallest q-clique found.
if not IsCompleteDigraph(D) and IsEmpty(CliquesFinder(D, fail, [], 1, [],
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you possibly add a comment to this line about what it is that the CliquesFinder function is looking for here?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll add some additional explanation for the CliqueFinder call.

[], false, chrom,
true)) then
# Get adjacency function
adjacent := DigraphAdjacencyFunction(D);
# Sort vertices by degree, so that higher degree vertices are picked first
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is already implemented in DigraphWelshPowellOrder maybe you could use that instead of rolling your own?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you for letting me know about DigraphWelshPowellOrder, I'll make the change to use that instead!

vertices := [1 .. nr];
deg := ShallowCopy(OutDegrees(D));
SortParallel(deg, vertices, {x, y} -> x > y);
# Choose two non-adjacent vertices x, y
# This is just done by ascending ordering.
found := false;
for x_i in [1 .. nr] do
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This seems like a lot of work to find two non-adjacent vertices, couldn't you just take the last node u in the list vertices (which has lowest degree) and take the any value not in OutNeighbours(D, u);? This has to exist because u is of minimum degree and D is not a complete graph.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The reasoning behind the approach is that the recursion will terminate either when it finds a clique the size of the current upper bound or we reach the complete graph. By prioritising high degree vertices, it is more likely we will reach a clique in fewer steps. I'd be happy to run a few experiments though to test if you think it is worth checking.

x := vertices[x_i];
for y_i in [x_i + 1 .. nr] do
y := vertices[y_i];
if not adjacent(x, y) then
found := true;
break;
fi;
od;
if found then
break;
fi;
od;
Assert(1, x <> y, "x and y must be different");
Assert(1, found, "No adjacent vertices");
# Colour the vertex contraction.
# A contraction of a graph effectively merges two non adjacent vertices
# into a single new vertex with the edges merged.
# We merge y into x, keeping x.
D_contract := DigraphMutableCopy(D);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can't you use DigraphContractEdge to do this step too?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looking through the manual I can't seem to find any reference to DigraphContractEdge, but I believe it would be possible to replace the step with DigraphQuotient by taking the partition the two adjacent vertices in one part, and then the remaining vertices are singleton parts. I need to look into the implementation of DigraphQuotient, as it seems more general that what is needed here and so could be slower.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see now there is an open pull request for DigraphContractEdge, so I'll use that once the required changes are made.

for v in vertices do
# Iterate over all vertices that
if v = x or v = y then
continue;
fi;
# Add any edge that involves y, but not already x to avoid duplication.
if adjacent(v, y) and not adjacent(v, x) then
DigraphAddEdge(D_contract, x, v);
DigraphAddEdge(D_contract, v, x);
fi;
od;
DigraphRemoveVertex(D_contract, y);
ZykovReduce(D_contract);
# Colour the edge addition
# This just adds symmetric edges between x and y;
DigraphAddEdge(D, [x, y]);
DigraphAddEdge(D, [y, x]);
ZykovReduce(D);
# Undo changes to the graph
DigraphRemoveEdge(D, [x, y]);
DigraphRemoveEdge(D, [y, x]);
fi;
end;
# Algorithm requires an undirected graph.
D := DigraphSymmetricClosure(DigraphMutableCopy(D));
# Use greedy colouring as an upper bound
chrom := RankOfTransformation(DigraphGreedyColouring(D), nr);
ZykovReduce(D);
return chrom;
end
);

BindGlobal("DIGRAPHS_ChromaticNumberChristofides",
function(D)
local nr, I, n, T, b, unprocessed, i, v_without_t, j, u, min_occurences,
cur_occurences, chrom, colouring, stack, vertices;

nr := DigraphNrVertices(D);
vertices := List(DigraphVertices(D));
# Initialise the required variables.
# Calculate all maximal independent sets of D.
I := DigraphMaximalIndependentSets(D);
# Convert each MIS into a BList
I := List(I, i -> BlistList(vertices, i));
# Upper bound for chromatic number.
chrom := nr;
# Set of vertices of D not in the current subgraph at level n.
T := ListWithIdenticalEntries(nr, false);
# Current search level of the subgraph tree.
n := 0;
# The maximal independent sets of V \ T at level n.
b := [ListWithIdenticalEntries(nr, false)];
# Number of unprocessed MIS's of V \ T from level 1 to n
unprocessed := ListWithIdenticalEntries(nr, 0);
# Would be jth colour class of the chromatic colouring of G.
colouring := List([1 .. nr], i -> BlistList(vertices, [i]));
# Stores current unprocessed MIS's of V \ T at level 1 to level n
stack := [];
# Now perform the search.
repeat
# Step 2
if n < chrom then
# Step 3
# If V = T then we've reached a null subgraph
if SizeBlist(T) = nr then
chrom := n;
SubtractBlist(T, b[n + 1]);
for i in [1 .. chrom] do
colouring[i] := b[i];
# TODO set colouring attribute
od;
else
# Step 4
# Compute the maximal independent sets of V \ T
v_without_t := DIGRAPHS_MaximalIndependentSetsSubtractedSet(I, T,
infinity);
# Step 5
# Pick u in V \ T such that u is in the fewest maximal independent sets.
u := -1;
min_occurences := infinity;
for i in vertices do
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This seems to have unnecessarily high complexity. Why not loop over everything in v_without_t one time to find a list whose i-th entry is the number of times it occurs in the an entry of v_without_t?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That is a more sensible approach, I'll make that change now

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I experimented with both this approach and the current approach, and it seemed to not have a noticeable difference. I've instead simplified the current approach, so that it should be clearer and neater.

# Skip elements of T.
if T[i] then
continue;
fi;
cur_occurences := 0;
for j in v_without_t do
if j[i] then
cur_occurences := cur_occurences + 1;
fi;
od;
if cur_occurences < min_occurences then
min_occurences := cur_occurences;
u := i;
fi;
od;
Assert(1, u <> -1, "Vertex must be picked");
# Remove maximal independent sets not containing u.
v_without_t := Filtered(v_without_t, x -> x[u]);
# Add these MISs to the stack
Append(stack, v_without_t);
# Search has moved one level deeper
n := n + 1;
unprocessed[n] := Length(v_without_t);
fi;
else
# if n >= g then T = T \ b[n]
# This exceeds the current best bound, so stop search.
SubtractBlist(T, b[n + 1]);
fi;
# Step 6
while n <> 0 do
# step 7
if unprocessed[n] = 0 then
n := n - 1;
SubtractBlist(T, b[n + 1]);
else
# Step 8
# take an element from the top of the stack
i := Remove(stack);
unprocessed[n] := unprocessed[n] - 1;
b[n + 1] := i;
UniteBlist(T, i);
break;
fi;
od;
until n = 0;
return chrom;
end
);

InstallMethod(ChromaticNumber, "for a digraph by out-neighbours",
[IsDigraphByOutNeighboursRep],
function(D)
Expand All @@ -392,6 +567,10 @@ function(D)
return DIGRAPHS_ChromaticNumberLawler(D);
elif ValueOption("byskov") <> fail then
return DIGRAPHS_ChromaticNumberByskov(D);
elif ValueOption("zykov") <> fail then
return DIGRAPHS_ChromaticNumberZykov(D);
elif ValueOption("christofides") <> fail then
return DIGRAPHS_ChromaticNumberChristofides(D);
fi;

# The chromatic number of <D> is at least 3 and at most nr
Expand Down
112 changes: 112 additions & 0 deletions tst/standard/attr.tst
Original file line number Diff line number Diff line change
Expand Up @@ -1662,6 +1662,118 @@ Error, the argument <D> must be a digraph with no loops,
gap> DIGRAPHS_UnderThreeColourable(EmptyDigraph(0));
0

# Test ChromaticNumber Zykov
gap> ChromaticNumber(NullDigraph(10) : zykov);
1
gap> ChromaticNumber(CompleteDigraph(10) : zykov);
10
gap> ChromaticNumber(CompleteBipartiteDigraph(5, 5) : zykov);
2
gap> ChromaticNumber(DigraphRemoveEdge(CompleteDigraph(10), [1, 2]) : zykov);
10
gap> ChromaticNumber(Digraph([[4, 8], [6, 10], [9], [2, 3, 9], [],
> [3], [4], [6], [], [5, 7]]) : zykov);
3
gap> ChromaticNumber(DigraphDisjointUnion(CompleteDigraph(1),
> Digraph([[2], [4], [1, 2], [3]])) : zykov);
3
gap> ChromaticNumber(DigraphDisjointUnion(CompleteDigraph(1),
> Digraph([[2], [4], [1, 2], [3], [1, 2, 3]])) : zykov);
4
gap> gr := Digraph([[2, 3, 4], [3], [], []]);
<immutable digraph with 4 vertices, 4 edges>
gap> ChromaticNumber(gr : zykov);
3
gap> ChromaticNumber(EmptyDigraph(0) : zykov);
0
gap> gr := CompleteDigraph(4);;
gap> gr := DigraphAddVertex(gr);;
gap> ChromaticNumber(gr : zykov);
4
gap> gr := Digraph([[2, 4, 7, 3], [3, 5, 8, 1], [1, 6, 9, 2],
> [5, 7, 1, 6], [6, 8, 2, 4], [4, 9, 3, 5], [8, 1, 4, 9], [9, 2, 5, 7],
> [7, 3, 6, 8]]);;
gap> ChromaticNumber(gr : zykov);
3
gap> gr := DigraphSymmetricClosure(ChainDigraph(5));
<immutable symmetric digraph with 5 vertices, 8 edges>
gap> ChromaticNumber(gr : zykov);
2
gap> gr := DigraphFromGraph6String("KmKk~K??G@_@");
<immutable symmetric digraph with 12 vertices, 42 edges>
gap> ChromaticNumber(gr : zykov);
4
gap> gr := CycleDigraph(7);
<immutable cycle digraph with 7 vertices>
gap> ChromaticNumber(gr : zykov);
3
gap> ChromaticNumber(gr : zykov);
3
gap> ChromaticNumber(gr : zykov);
3
gap> a := DigraphRemoveEdges(CompleteDigraph(50), [[1, 2], [2, 1]]);;
gap> b := DigraphAddVertex(a);;
gap> ChromaticNumber(a : zykov);
49
gap> ChromaticNumber(b : zykov);
49

# Test ChromaticNumber Christofides
gap> ChromaticNumber(NullDigraph(10) : christofides);
1
gap> ChromaticNumber(CompleteDigraph(10) : christofides);
10
gap> ChromaticNumber(CompleteBipartiteDigraph(5, 5) : christofides);
2
gap> ChromaticNumber(DigraphRemoveEdge(CompleteDigraph(10), [1, 2]) : christofides);
10
gap> ChromaticNumber(Digraph([[4, 8], [6, 10], [9], [2, 3, 9], [],
> [3], [4], [6], [], [5, 7]]) : christofides);
3
gap> ChromaticNumber(DigraphDisjointUnion(CompleteDigraph(1),
> Digraph([[2], [4], [1, 2], [3]])) : christofides);
3
gap> ChromaticNumber(DigraphDisjointUnion(CompleteDigraph(1),
> Digraph([[2], [4], [1, 2], [3], [1, 2, 3]])) : christofides);
4
gap> gr := Digraph([[2, 3, 4], [3], [], []]);
<immutable digraph with 4 vertices, 4 edges>
gap> ChromaticNumber(gr : christofides);
3
gap> ChromaticNumber(EmptyDigraph(0) : christofides);
0
gap> gr := CompleteDigraph(4);;
gap> gr := DigraphAddVertex(gr);;
gap> ChromaticNumber(gr : christofides);
4
gap> gr := Digraph([[2, 4, 7, 3], [3, 5, 8, 1], [1, 6, 9, 2],
> [5, 7, 1, 6], [6, 8, 2, 4], [4, 9, 3, 5], [8, 1, 4, 9], [9, 2, 5, 7],
> [7, 3, 6, 8]]);;
gap> ChromaticNumber(gr : christofides);
3
gap> gr := DigraphSymmetricClosure(ChainDigraph(5));
<immutable symmetric digraph with 5 vertices, 8 edges>
gap> ChromaticNumber(gr : christofides);
2
gap> gr := DigraphFromGraph6String("KmKk~K??G@_@");
<immutable symmetric digraph with 12 vertices, 42 edges>
gap> ChromaticNumber(gr : christofides);
4
gap> gr := CycleDigraph(7);
<immutable cycle digraph with 7 vertices>
gap> ChromaticNumber(gr : christofides);
3
gap> ChromaticNumber(gr : christofides);
3
gap> ChromaticNumber(gr : christofides);
3
gap> a := DigraphRemoveEdges(CompleteDigraph(50), [[1, 2], [2, 1]]);;
gap> b := DigraphAddVertex(a);;
gap> ChromaticNumber(a : christofides);
49
gap> ChromaticNumber(b : christofides);
49

# DegreeMatrix
gap> gr := Digraph([[2, 3, 4], [2, 5], [1, 5, 4], [1], [1, 1, 2, 4]]);;
gap> DegreeMatrix(gr);
Expand Down