From 02195f3e5ae2ac7b067db769d60e39e7de034ef2 Mon Sep 17 00:00:00 2001 From: Miles Gould Date: Sun, 17 Feb 2019 21:27:25 +0000 Subject: [PATCH 1/5] Add code to generate supernecklaces and -bracelets A necklace is an equivalence class of strings under rotation; a bracelet is an equivalence class of strings under rotation and reflection. Restricting our attention further to necklaces/bracelets without repetitions, we can search for "supernecklaces" and "superbracelets", which are strings that contain some representative of each non-repeating necklace/bracelet as a substring (equivalently, strings which, for every permutation p, contain a substring that may be rotated and/or reflected to obtain p). This is not a classic TSP, but it can be framed as an Equality-Generalised Travelling Salesman Problem, which can then be converted into an ATSP and solved by the GLKH solver, http://webhotel4.ruc.dk/~keld/research/GLKH/. This work was inspired by Matthew Clarke's questions on the forum: https://groups.google.com/forum/?#!msg/superpermutators/H1_jrq_EO1c/unrp4Gl_GQAJ --- Makefile | 6 ++++++ README.md | 3 +++ bin/mkatsp.py | 45 +++++++++++++++++++++++++++++++++++++++++++-- 3 files changed, 52 insertions(+), 2 deletions(-) diff --git a/Makefile b/Makefile index 06a530b..a4d7f92 100644 --- a/Makefile +++ b/Makefile @@ -7,3 +7,9 @@ tsp/%.tsp: atsp/%.atsp atsp/%.atsp: bin/mkatsp.py $* > "$@" + +necklace/%.gtsp: + bin/mkatsp.py --necklace $* > "$@" + +bracelet/%.gtsp: + bin/mkatsp.py --bracelet $* > "$@" diff --git a/README.md b/README.md index 62902d9..d8e0d45 100644 --- a/README.md +++ b/README.md @@ -106,3 +106,6 @@ concorde -o "$SUPERPERM_DIR"/concorde/out/6.out "$SUPERPERM_DIR"/tsp/6.tsp * The directory `superpermutations` contains various known superpermutations that are interesting for some reason. * Want to see what's in the superpermutations? Check out the `demutator` directory. (Be wary of the C code.) + +* To search for "supernecklaces" and "superbracelets" (strings which contain + each permutation only up to rotation and/or reflection), run `make necklace/6.gtsp` or `make bracelet/6.gtsp`. The resulting input files can be solved using the GLKH solver, available from http://webhotel4.ruc.dk/~keld/research/GLKH/ . diff --git a/bin/mkatsp.py b/bin/mkatsp.py index 43113a3..f07c64a 100755 --- a/bin/mkatsp.py +++ b/bin/mkatsp.py @@ -2,6 +2,7 @@ # -*- encoding: utf-8 -*- from __future__ import division +from collections import defaultdict import math from itertools import permutations import optparse @@ -10,6 +11,8 @@ parser.add_option("-b", "--bound", type="int", help="only include edges up to this weight") parser.add_option("-n", "--no-cyclic", action="store_true", help="no edges between non-adjacent cyclic permutations") parser.add_option("-s", "--simple", action="store_true", help="only include edges from a.b -> b.a^r") +parser.add_option("--necklace", action="store_true", help="consider permutations equivalent under rotation") +parser.add_option("--bracelet", action="store_true", help="consider permutations equivalent under rotation and reflection") (options, args) = parser.parse_args() if len(args) != 1: parser.error("Wrong number of arguments") @@ -49,8 +52,41 @@ def distance(p, q): return weight -print "NAME : superperm %d" % (N,) -print "TYPE : ATSP" +def normalise_necklace(p): + n = list(p) + i = n.index(0) + return n[i:] + n[:i] + +def normalise_bracelet(p): + n = normalise_necklace(p) + i = n.index(1) + j = n.index(2) + if i < j: + n.reverse() + return normalise_necklace(n) + return n + +def print_classes(normalise): + classes = defaultdict(set) + for (i, p) in enumerate(permutations(range(N))): + key = "".join(map(str, normalise(p))) + classes[key].add(i + 1) + num_classes = len(classes) + print "GTSP_SETS: %d" % (len(classes),) + print "GTSP_SET_SECTION" + for (i, c) in enumerate(classes.values()): + print "%d %s -1" % (i + 1, " ".join(map(str, c))) + print "EOF" + +if options.necklace: + print "NAME: supernecklace %d" % (N,) + print "TYPE: GTSP" +elif options.bracelet: + print "NAME: superbracelet %d" % (N,) + print "TYPE: GTSP" +else: + print "NAME : superperm %d" % (N,) + print "TYPE : ATSP" print "DIMENSION : %d" % (n_perms,) print "EDGE_WEIGHT_TYPE : EXPLICIT" print "EDGE_WEIGHT_FORMAT : FULL_MATRIX" @@ -61,3 +97,8 @@ def distance(p, q): for p in perms: print " ".join([ str(distance(p, q)) for q in perms ]) + +if options.necklace: + print_classes(normalise_necklace) +if options.bracelet: + print_classes(normalise_bracelet) From d9de7cc28d19434b7cf8111f2e22799ecc65644a Mon Sep 17 00:00:00 2001 From: Miles Gould Date: Wed, 20 Feb 2019 15:57:03 +0000 Subject: [PATCH 2/5] Mark necklace and bracelet input files as AGTSPs Input files that specify `TYPE: GTSP` are assumed to be symmetric by GLKH. Input files for asymmetric GTSPs must have `TYPE: AGTSP`. This change makes GLKH produce optimal supernecklaces for n = 3 and 4, and substantially improves the lengths of the supernecklaces and -bracelets found for n \in {5, 6, 7}. --- bin/mkatsp.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/bin/mkatsp.py b/bin/mkatsp.py index f07c64a..6169155 100755 --- a/bin/mkatsp.py +++ b/bin/mkatsp.py @@ -80,10 +80,10 @@ def print_classes(normalise): if options.necklace: print "NAME: supernecklace %d" % (N,) - print "TYPE: GTSP" + print "TYPE: AGTSP" elif options.bracelet: print "NAME: superbracelet %d" % (N,) - print "TYPE: GTSP" + print "TYPE: AGTSP" else: print "NAME : superperm %d" % (N,) print "TYPE : ATSP" From 5d22d645326ae9b191e207d47cd4c4faf1bfa478 Mon Sep 17 00:00:00 2001 From: Miles Gould Date: Mon, 17 Jun 2019 17:41:03 +0100 Subject: [PATCH 3/5] Allow generation of the alternating graph --- bin/mkatsp.py | 32 +++++++++++++++++++++++++------- 1 file changed, 25 insertions(+), 7 deletions(-) diff --git a/bin/mkatsp.py b/bin/mkatsp.py index 6169155..c9c6523 100755 --- a/bin/mkatsp.py +++ b/bin/mkatsp.py @@ -13,6 +13,7 @@ parser.add_option("-s", "--simple", action="store_true", help="only include edges from a.b -> b.a^r") parser.add_option("--necklace", action="store_true", help="consider permutations equivalent under rotation") parser.add_option("--bracelet", action="store_true", help="consider permutations equivalent under rotation and reflection") +parser.add_option("--alternating", action="store_true", help="show weights and 1-cycles in the alternating graph") (options, args) = parser.parse_args() if len(args) != 1: parser.error("Wrong number of arguments") @@ -30,6 +31,18 @@ n_perms = len(perms) ordered = tuple(range(N)) +def cyclically_equivalent(p, q): + sp = "".join([ SYMBOLS[i] for i in p ]) + sq = "".join([ SYMBOLS[i] for i in q ]) + return sp in sq+sq + +def alternating_distance(p, q): + if p == q: + return 0 + if cyclically_equivalent(p, q): + return INF + return max(distance((p[-1],) + p[:-1], q) - 1, 0) + def distance(p, q): if q == ordered: return 0 @@ -45,10 +58,8 @@ def distance(p, q): weight = n break - if weight > 1 and options.no_cyclic: - sp = "".join([ SYMBOLS[i] for i in p ]) - sq = "".join([ SYMBOLS[i] for i in q ]) - if sp in sq+sq: return INF + if weight > 1 and options.no_cyclic and cyclically_equivalent(p, q): + return INF return weight @@ -84,6 +95,9 @@ def print_classes(normalise): elif options.bracelet: print "NAME: superbracelet %d" % (N,) print "TYPE: AGTSP" +elif options.alternating: + print "NAME: alternating %d" % (N,) + print "TYPE: AGTSP" else: print "NAME : superperm %d" % (N,) print "TYPE : ATSP" @@ -95,10 +109,14 @@ def print_classes(normalise): print "EDGE_WEIGHT_SECTION :" -for p in perms: - print " ".join([ str(distance(p, q)) for q in perms ]) +if options.alternating: + for p in perms: + print " ".join([ str(alternating_distance(p, q)) for q in perms ]) +else: + for p in perms: + print " ".join([ str(distance(p, q)) for q in perms ]) -if options.necklace: +if options.necklace or options.alternating: print_classes(normalise_necklace) if options.bracelet: print_classes(normalise_bracelet) From c00f4a7f54f1d2e0df58ac5c53e17e5afe662b1b Mon Sep 17 00:00:00 2001 From: Miles Gould Date: Mon, 17 Jun 2019 17:45:36 +0100 Subject: [PATCH 4/5] Add option to bin/printtour to fill in 1-cycles --- bin/printtour.py | 21 +++++++++++++++++---- 1 file changed, 17 insertions(+), 4 deletions(-) diff --git a/bin/printtour.py b/bin/printtour.py index deba97a..599f759 100755 --- a/bin/printtour.py +++ b/bin/printtour.py @@ -3,12 +3,17 @@ from __future__ import division from itertools import permutations -import sys +import optparse + +parser = optparse.OptionParser(usage="%prog [options] N tour_filename") +parser.add_option("-r", "--rotations", action="store_true", + help="fill in rotations of visited permutations") +(options, args) = parser.parse_args() SYMBOLS = "123456789ABCDEFG" -n = int(sys.argv[1]) -tour_filename = sys.argv[2] +n = int(args[0]) +tour_filename = args[1] perms = list(permutations(range(n))) n_perms = len(perms) @@ -24,6 +29,11 @@ def read_tour(tour_filename): if ix == -1: break yield "".join([ SYMBOLS[i] for i in perms[ix - 1] ]) +def rotations(xs): + for x in xs: + for i in range(n): + yield x[i:] + x[:i] + def squash(xs): return reduce(lambda x, y: x + y[overlap(x, y):], xs, "") @@ -33,4 +43,7 @@ def overlap(x, y): if x[len(x)-i:] == y[:i]: return i -print squash(read_tour(tour_filename)) +tour = read_tour(tour_filename) +if options.rotations: + tour = rotations(tour) +print squash(tour) From 052fe15f97ab97c64fd4b90f87f043af069cb6a9 Mon Sep 17 00:00:00 2001 From: Miles Gould Date: Mon, 17 Jun 2019 17:45:52 +0100 Subject: [PATCH 5/5] Add Make rule to generate alternating graphs --- Makefile | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Makefile b/Makefile index a4d7f92..5203840 100644 --- a/Makefile +++ b/Makefile @@ -13,3 +13,6 @@ necklace/%.gtsp: bracelet/%.gtsp: bin/mkatsp.py --bracelet $* > "$@" + +alternating/%.gtsp: + bin/mkatsp.py --alternating $* > "$@"