10
\$\begingroup\$

Lexicographic Ordering

For this challenge we will be talking about the lexicographic ordering of strings. If you know how to put words in alphabetical order you already understand the basic idea of lexicographic ordering.

Lexicographic ordering is a way of ordering strings of characters.

When comparing two strings lexicographically, you look at the first character of each string. If one of the strings is empty and has no first character that string is smaller. If the first characters are different then the string with the smaller first character is smaller. If the characters are equal we remove them from the strings and compare the rest of the strings

Here is a Haskell program that implements this comparison

compare :: Ord a => [a] -> [a] -> Ordering
compare [] [] =
  EQ
compare [] (y : ys) =
  LT
compare (x : xs) [] =
  GT
compare (x : xs) (y : ys) =
  if
    x == y
  then
    compare xs ys
  else
    compare x y

Lexicograpic ordering has some interesting properties. Just like integers, every string has a next biggest string. For example after [1,0,1,0] there is [1,0,1,0,0], and there is nothing in between. But unlike integers there are strings which have an infinite number of strings between each other. For example [0] and [1], all strings of more than 1 0 are greater than [0] but less than [1].

Challenge

In this challenge you will write a program or function (from now on just called "function"), which maps binary strings (that is, strings made of an alphabet of two symbols) to ternary strings (strings made of an alphabet of three symbols).

Your function should be bijective, meaning that every string gives a unique output, and every ternary string is the output for some input.

Your function should also be monotonic, meaning that it preserves comparisons when applied

$$ x < y \iff f(x) < f(y) $$

Where \$<\$ is the lexicographic ordering of the strings.

Scoring

This is so answers will be scored in bytes with fewer being better.

IO

You may take input and produce output as a string with chosen characters for the sets of 2 and 3 symbols, or as a list / array / vector of integers.

Testing

It's rather hard to make test cases for this. However it should be noted that when given a string of only 0s your code must give back the same string. So I can give the following test cases

[] -> []
[0] -> [0]
[0, 0, 0] -> [0, 0, 0]

However this doesn't go very far in terms of testing. So I recommend you also run a random battery of strings checking that the monotonicity property holds.

\$\endgroup\$
3
  • \$\begingroup\$ Is there more than one such function? \$\endgroup\$ – Greg Martin yesterday
  • 2
    \$\begingroup\$ This should be an order-preserving non-identity bijection between the set of binary strings and itself: 0^n -> 0^n, 00w -> 0w, 010^n -> 10^n, 01w -> 10w, 10^n -> 110^n, 1w -> 11w (where w ranges over all strings with at least one 1) \$\endgroup\$ – Nitrodon yesterday
  • \$\begingroup\$ @Nitrodon Nice! With that we can show there are in fact \$\beth_1\$ solutions, or an unaccountably infinite number of them. \$\endgroup\$ – Wheat Wizard yesterday
8
\$\begingroup\$

Haskell, 41 bytes

f x|sum x<1=x
f(0:y:x)=y:f x
f(1:x)=2:f x

Try it online!

Here's a solution. Just to show that this is possible and give a potential starting point for others.

Explanation

The first thing we can notice is that any valid \$f\$ must preserve the number of zeros at the end of the input in the output.

The proof of this is pretty simple. Lets say we have some string \$x\$. We know that there are no strings between \$x\$ and \$x\mathbin{|}0\$ (here \$\mathbin{|}\$ represents appending a character to a string). Thus there can be no strings between \$f(x)\$ and \$f(x\mathbin{|}0)\$. If there were some string \$y\$ such that \$f(x) < f(y) < f(x\mathbin{|}0)\$, then it would have to be that \$x < y < x \mathbin{|} 0\$. So since there are no strings between \$f(x)\$ and \$f(x\mathbin{|}0)\$, then \$f(x\mathbin{|}0) = f(x)\mathbin{|}0\$.

So this allows us to write the first line of our program

f x|sum x<1=x

Which says if we get a list of only \$0\$s then output that list intact.

It might be tempting to say that all zeros should be preserved in the output, not just trailing ones. However this ends up not being possible, I will leave the proof of this as an exercise for the reader.

Now the task is to translate strings with no trailing zeros, while preserving order. The way I chose to do this is pretty simple. We go left to right replacing certain sequences with the correct symbols. If we see a zero we replace read it as the next symbol (and skip over that symbol), there must be a next symbol since otherwise there would be a trailing \$0\$.

f(0:y:x)=y:f x

If the symbol is \$1\$ we read it as \$2\$.

f(1:x)=2:f x

Haskell, 52 48 bytes

4 bytes saved by ovs

f(0:x)=0:f x
f(1:x)|sum x>0,z:y<-x=1+z:f y
f x=x

Try it online!

This is a different bijection discovered by a friend of mine and golfed by me.

\$\endgroup\$
1
  • 1
    \$\begingroup\$ 48 bytes on the second bijection by using the base case of f x=x for [1, 0, ..., 0]. \$\endgroup\$ – ovs yesterday
4
\$\begingroup\$

Jelly, 10 bytes

2ḢḢ?;ßƊ¹Ẹ?

A monadic Link that accepts a list (of \$\{0,1\}\$) and yields a list (of \$\{0,1,2\}\$).

Try it online!

How?

Results in the same mapping as used by Wheat Wizard.

2ḢḢ?;ßƊ¹Ẹ? - Link: list, A
        Ẹ? - if: any?
      Ɗ    - then: last three links as a monad - f(A):
  Ḣ?       -   if: head (leftmost element of A (or 0 if empty);
                         changes A in place)
2          -   then: literal two
 Ḣ         -   else: head (leftmost element of augmented A (or 0 if empty);
                           changes A in place again)
     ß     -   call this Link again -> f(augmented A)
    ;      -   concatenate
       ¹   - else: identity (yield A)
\$\endgroup\$
2
  • \$\begingroup\$ Nice. Might be better to start the footer with F since otherwise you lose most of the input part of the output before it gets displayed \$\endgroup\$ – Nick Kennedy yesterday
  • 1
    \$\begingroup\$ @NickKennedy oops, thanks! \$\endgroup\$ – Jonathan Allan yesterday
2
\$\begingroup\$

JavaScript (Node.js), 34 bytes

s=>s.replace(/0(?=.*2)./g,t=>t/2);

Try it online!

Wheat Wizard♦'s idea using the allowance of custom char

\$\endgroup\$
1
\$\begingroup\$

Retina, 20 bytes

/0*$/%`1(.)
$.($1*__

Try it online! Explanation: Uses @WheatWizard's friend's mapping.

/0*$/%`

Match trailing zeros and temporarily exclude them from the following substitution.

1(.)
$.($1*__

Match a 1 followed by a digit and replace it with 1 more than the digit, i.e. 11 becomes 2, 10 (in the middle) becomes 1, but other digits are unaffected.

\$\endgroup\$
0
\$\begingroup\$

Charcoal, 20 bytes

≔I⪪⮌S¹υWΣυ¿⊟υ2I⊟υFυ0

Try it online! Link is to verbose version of code. Explanation: Uses @WheatWizard's mapping.

≔I⪪⮌S¹υ

Reverse the input, split it into digits, and cast the digits to integer.

WΣυ

Repeat while there are still 1s left in the input...

¿⊟υ2

... if the current digit is a 1 then print a 2...

I⊟υ

... otherwise print the next digit.

Fυ0

Print any trailing zeros.

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.