This is my response to the Perl Weekly Challenge #115.
A string $S can be put before another string $T in circle if the last character of $S is same as first character of $T.Examples:
Input: @S = ("abc", "dea", "cd")
Output: 1 as we can form circle e.g. "abc", "cd", "dea".
Input: @S = ("ade", "cbd", "fgh")
Output: 0 as we can't form circle.
This challange describes a directed graph, without using that name.
I have written a five-part article about (undirected) graphs. See Seven Bridges to Raku.
A Hamiltonian Circuit (also called Hamiltonian Circle) seems promising as an
answer to what we are looking for (except that it is undirected, and we are dealing with a
directed graph; e.g. abc -> cd -> dea -> abc
). Part 3 of my article discusses
Hamilton, as does
en.wikipedia.org/wiki/Hamiltonian_path.
Let us follow the Seven Bridges, and see where we end up. (Preferably not in hot water...)
We can start by generating the configuration file used by the programs in the Seven Bridges articles (presented in part 1, and extended to support directed graphs in part 5). Then we can (re)use the programs from that article to test the graphs.
File: string-chain-7bridges
#! /usr/bin/env raku
unit sub MAIN (*@S where @S.elems > 0); # [1]
die "Non-unique string not allowed" if @S.elems != @S.unique.elems; # [2]
for @S -> $elem # [3]
{
say "$elem: $elem";
}
for @S -> $first # [4]
{
for @S -> $second # [4]
{
next if $second eq $first; # [5]
if $second.substr(*-1,1) eq $first.substr(0,1) # [6]
{
say "$second>$first {$first.substr(0,1)}";
}
}
}
[1] Ensure at least one string in the array. The challenge does not specify a lower limit, but this is the absolute minimum that can work. Sort of.
[2] We are using the strings as identifiers, so they have to be unique. (This may be a problem, but I'll come back to that later on.)
[3] Print all the strings; the nodes (or circles) in the graph. We use the string both as identifier and name, as it is unique.
[4] For each string, in a double loop.
[5] Skip the situation where both loops have the same string.
[6] Print a connection (edge) if the last letter of the first string (outer loop) is the same as the first letter of the second string (inner loop). Use the letter as the name.
Running it, redirecting the output to a file:
$ ./string-chain-7bridges abc dea cd > example1.def
$ ./string-chain-7bridges ade cbd fgh > example2.def
File: example1.def
abc: abc
dea: dea
cd: cd
dea>abc a
cd>dea d
abc>cd c
File: example2.def
ade: ade
cbd: cbd
fgh: fgh
Then we can use the bridges3svg
shell script wrapper around
bridges-graphviz-directional
, both copied from part 5 of the Seven
Bridges, to generate the Graphviz «dot» file and the resulting graph as a SVG
file in one go:
$ ./bridges3svg example1.def
: example1.def -> example1.dot
: example1.dot -> example1.svg
$ ./bridges3svg example2.def
: example1.def -> example2.dot
: example1.dot -> example2.svg
The graphs:
We can clearly see that the first example forms a Hamiltonian Circuit (and the directionality business does not matter here), and the second one does not.
Just for fun; here is a directed graph that does not form a circle, but it satisfies Hamilton if we make it undirected:
$ ./string-chain-7bridges abc dex conyd cd > revenge.def
$ ./bridges3svg revenge.def
: revenge.def -> revenge.dot
: revenge.dot -> revenge.svg
That was kind of fun. But we were asked to write a program to tell us. We can use the «hamiltonian2» program from part 3 of the Seven Bridges. Except that it does not support directed graphs, which we do have. That is easily fixed though, and the resulting «hamiltonian-unchained» program is included in the zip file:
File: hamiltonian-unchained (changes only)
elsif $line ~~ /^(\w+) (<[\s\>]>) (\w+) \s (.*) / # [1]
{
my ($from, $dir, $to, $name) = ($0, $1, $2, $3.trim); # [2]
die "No such node $from" unless %nodes{$from};
die "No such node $to" unless %nodes{$to};
$name = $0.trim if $name ~~ /(.*?)\#/;
%edges{$from}.push: $name;
%edges{$to}.push: $name;
%next{$from}.push: $to.Str;
%next{$to}.push: $from.Str unless $dir eq ">"; # [3]
}
[1] Accept either a space (\s
) or a >
as separator between the two
nodes.
[2] Take note of the separator character. The other matches have been renumbered as a consequence.
[3] The second direction is only there for an undirectional connection (bridge).
Running it:
$ ./hamiltonian-unchained example1.def
Eulerian path/trail & Eulerian cycle/circuit.
Hamiltonian path: abc -> cd -> dea
$ ./hamiltonian-unchained example2.def
Not an Eulerian path/trail nor a cycle/circuit (as we have at least two graphs).
The program does not explicitly say that the second graph is not a Hamiltonian path, but that follows from the fact that a Hamiltonian path is also a Eulerian graph (but not the other way round).
We can fix that. (Especially the part where we exclaim that it is a Hamiltonian Path, as it really is not.)
But perhaps not…
Let us take a step back, and reflect. The chosen approach does give us the solution to the challenge, but it is not very efficiently programmed. We can certainly do better (as in shorter).
Let us do just that.
File: string-chain
#! /usr/bin/env raku
unit sub MAIN (*@S where @S.elems > 1, # [1]
:v(:$verbose),
:n(:$no-duplicates)); # [2]
die "Non-unique string not allowed" # [2a]
if $no-duplicates && @S.elems != @S.unique.elems;
for @S.permutations -> @perm # [3]
{
say ": perm: { @perm.join(", ") }" if $verbose;
if is-circle(@perm) # [4]
{
say 1;
exit;
}
}
say 0; # [5]
sub is-circle (@list is copy) # [6]
{
my $first = @list.shift; # [7]
my $first-start = $first.substr(0,1); # [8]
my $second; # [9]
while @list # [10]
{
$second = @list.shift; # [10a]
return False if $first.substr(*-1,1) ne $second.substr(0,1); # [10b]
$first = $second; # [10c]
}
return True if $first-start eq $second.substr(*-1,1); # [11]
return False; # [12]
}
[1] Ensure at least two words.
[2] Use this command line option if you want to disallow duplicate words. The program
will fail if the list of words have shrunk after removing duplicates (with the
unique
method) [2a].
[3] This gives us all the permutations of the words (i.e. all the possible sorting order). E.g. will «a b c» result in «a b c», «a c b», «b a c», «b c a», «c a b» and «c b a» (shown here in sorted order).
[4] If the current permutation forms a circle, print «1» and exit.
[5] No match? Then print 0 and we are done.
[6] Do the given words form a circle, in the given order?
[7] We are handling two and two words at a time. The first one goes here.
[8] The first letter of the first word. We need this for comparison with the last letter of the last word in [11].
[9] The second one goes here.
[10] As long as we have more words, get the next one (in the second variable) [10a],
return False
if the two words do not match up (last letter to first letter)
[10b]. Shuffle the secdond word to the first variable, ready for the next iteration
[10c].
[11] See [8].
[12] No match.
See
docs.raku.org/routine/permutations for more information about permutations
.
Running it:
$ ./string-chain -v abc dea cd
: perm: abc, dea, cd
: perm: abc, cd, dea
1
$ ./string-chain -v ade cbd fgh
: perm: ade, cbd, fgh
: perm: ade, fgh, cbd
: perm: cbd, ade, fgh
: perm: cbd, fgh, ade
: perm: fgh, ade, cbd
: perm: fgh, cbd, ade
0
Looking good.
Note that this program handles duplicates:
$ ./string-chain -v axe axe eva elena
: perm: axe, axe, eva, elena
: perm: axe, axe, elena, eva
: perm: axe, eva, axe, elena
1
Unless we tell it otherwise:
$ ./string-chain -v -n axe axe eva elena
Non-unique string not allowed
in sub MAIN at ./string-chain line 5
The graph generating program should really cope with duplicate values. That is easy(ish) to fix by using incremental IDs for the nodes:
File: string-chain-7bridges-duplicates
#! /usr/bin/env raku
unit sub MAIN (*@S where @S.elems > 0);
my %elems; # [1]
my $id = 0;
for @S -> $elem
{
%elems{++$id} = $elem; # [2]
say "$id: $elem"; # [2a]
}
for 1 .. $id -> $first
{
for 1 .. $id -> $second
{
next if $second eq $first;
if %elems{$second}.substr(*-1,1) eq %elems{$first}.substr(0,1) # [3]
{
say "$second>$first {%elems{$first}.substr(0,1)}"; # [4]
}
}
}
[1] The mapping between IDs and names. I have chosen a hash (instead of an array) so that it is possible to use another ID scheme later.
[2] Note the mapping between the ID and name, and print them [2a].
[3] Compare the names, and not the IDs.
[4] Use the IDs, but the first letter from the name (on the connection).
$./string-chain-7bridges-duplicates axe axe eva elena >duplicates.def
$ ./bridges3svg duplicates.def
: duplicates.def -> duplicates.dot
: duplicates.dot -> duplicates.svg
File: duplicates.def
1: axe
2: axe
3: eva
4: elena
3>1 a
4>1 a
3>2 a
4>2 a
1>3 e
2>3 e
1>4 e
2>4 e
The resulting graph:
It is circular - even if it is hard to tell. We can show it, by highlighting the (or a) path with «-highlight» command line option. This is not supported by the shell script wrapper, so we must run the two commands manually:
$ ./bridges-graphviz-directional --highlight="1 3 2 4 1" duplicates.def > duplicates-path.dot
$ dot -Tsvg duplicates-path.dot > duplicates-path.svg
We got the node names (IDs, really) from the definition file, made sure we got the directions right when we chose the path to highlight - and that we closed the circle (by specifiying the same node as start and end):
File: duplicates.def
1: axe
2: axe
3: eva
4: elena
3>1 a
4>1 a
3>2 a
4>2 a
1>3 e
2>3 e
1>4 e
2>4 e
#! /usr/bin/env perl
use strict;
use warnings;
use feature 'say';
use Getopt::Long;
use Algorithm::Combinatorics 'permutations'; # [1]
use feature 'signatures';
no warnings qw(experimental::signatures);
my $verbose = 0;
GetOptions("verbose" => \$verbose);
die "At least two strings" unless @ARGV > 1;
for my $list (permutations(\@ARGV))
{
my @perm = @$list;
say ": perm: " . join(", ", @perm) if $verbose;
if (is_circle(@perm))
{
say 1;
exit;
}
}
say 0;
sub is_circle (@list)
{
my $first = shift(@list);
my $first_start = substr($first, 0,1);
my $second;
while (@list)
{
$second = shift(@list);
return 0 if substr($first,-1,1) ne substr($second,0,1);
$first = $second;
}
return 1 if $first_start eq substr($second,-1,1);
return 0;
}
[1] Perl does not have a permutations
function, but this module supplies one for us.
Running it gives the same result as the Raku version:
$ ./string-chain-perl -v abc dea cd
: perm: abc, dea, cd
: perm: abc, cd, dea
1
$ ./string-chain-perl -v ade cbd fgh
: perm: ade, cbd, fgh
: perm: ade, fgh, cbd
: perm: cbd, ade, fgh
: perm: cbd, fgh, ade
: perm: fgh, ade, cbd
: perm: fgh, cbd, ade
0
I have dropped the duplicate suppresion code.
Input: @N = (1, 0, 2, 6)
Output: 6210
Input: @N = (1, 4, 2, 8)
Output: 8412
Input: @N = (4, 1, 7, 6)
Output: 7614
«Multiple of 2» is another name for «even integers larger than 0».
File: largest-multiple
#! /usr/bin/env raku
unit sub MAIN (*@N where @N.elems > 0 && all(@N) == any(0..9), # [1]
:v(:$verbose));
my @all = @N.permutations>>.join.grep(* ~~ / <[02468]> $/).sort.reverse; # [2]
say ": { @all.join(", ") }" if $verbose && @all;
say @all[0] if @all[0] && @all[0] != 0; # [3]
[1] Ensure that the input list has at least one element, and that all of them are a single digit.
[2] Get all possible soring orders of the digits (with permutations
),
then join the digits for each permutation (with >>.join
) so that
we get a list of numbers. Keep the numbers ending with 0, 2, 4, 6 or 8 (as that gives
an even number) (with grep
). Then we sort the list, giving the lowest
number first. Adding reverse
gives the highest value first.
[3] Print the first value if it is defined and non-zero. (Defined as we could have ended up with nothing after getting rid of the odd numbers, and zero as we could end up with that number.
Running it:
$ ./largest-multiple -v 1 0 2 6
: 6210, 6120, 6102, 6012, 2610, 2160, 2106, 2016, 1620, 1602, 1260, 1206, \
1062, 1026, 0612, 0216, 0162, 0126
6210
$ ./largest-multiple -v 1 4 2 8
: 8412, 8214, 8142, 8124, 4812, 4218, 4182, 4128, 2814, 2418, 2184, 2148, \
1842, 1824, 1482, 1428, 1284, 1248
8412
$ ./largest-multiple -v 4 1 7 6
: 7614, 7416, 7164, 7146, 6714, 6174, 4716, 4176, 1764, 1746, 1674, 1476
7614
Looking good.
Let us try the cases from comment [3]:
$ ./largest-multiple -v 1 3 5 7
$ ./largest-multiple -v 0 0 0
: 000, 000, 000, 000, 000, 000
The first one does not print anything, not even in verbose mode. The second one ends up with the number 0, which is not a muliple of 2. So no answer here as well.
One more:
$ ./largest-multiple -v 1 1 1 0
: 1110, 1110, 1110, 1110, 1110, 1110
1110
We could have removed the duplicates with unique
(works all
the time) or squish
(works on sorted structures only), but it does not really
matter.
See
docs.raku.org/routine/unique
for more information about unique
.
See
docs.raku.org/routine/squish
for more information about squish
.
#! /usr/bin/env perl
use strict;
use warnings;
use feature 'say';
use Getopt::Long;
use Algorithm::Combinatorics 'permutations';
my $verbose = 0;
GetOptions("verbose" => \$verbose);
my @all;
for my $list (permutations(\@ARGV)) # [1]
{
my @candidate = @$list;
my $value = join("", @candidate);
next unless $value =~ /[02468]$/;
push(@all, $value);
}
@all = reverse sort @all;
say ": " . join(", ", @all) if $verbose && @all;
say $all[0] if $all[0] && $all[0] != 0;
[1] I have chosen to use a loop instead of method chaining (with e.g. grep
as in the Raku version), as the number of steps in the original Raku code line (marked [2])
is quite large.
Running it gives the same result as the Raku version:
$ ./largest-multiple-perl -v 1 0 2 6
: 6210, 6120, 6102, 6012, 2610, 2160, 2106, 2016, 1620, 1602, 1260, 1206, \
1062, 1026, 0612, 0216, 0162, 0126
6210
$ ./largest-multiple-perl -v 1 4 2 8
: 8412, 8214, 8142, 8124, 4812, 4218, 4182, 4128, 2814, 2418, 2184, 2148, \
1842, 1824, 1482, 1428, 1284, 1248
8412
$ ./largest-multiple-perl -v 4 1 7 6
: 7614, 7416, 7164, 7146, 6714, 6174, 4716, 4176, 1764, 1746, 1674, 1476
7614
$ ./largest-multiple-perl -v 1 3 5 7
$ ./largest-multiple-perl -v 0 0 0
: 000, 000, 000, 000, 000, 000
And that's it.