Numeric Mahjong is a hypothetical variation of Japanese Mahjong, played with nonnegative integers instead of Mahjong tiles. Given a list of nonnegative integers, it is a winning hand if it satisfies the following:

  • its length is \$3n+2\$ for some nonnegative integer \$n\$, and
  • it can be partitioned into \$n\$ triples and a pair so that
    • each triple consists of three identical numbers \$(x,x,x)\$ or three consecutive numbers \$(x,x+1,x+2)\$, and
    • the pair consists of two identical numbers \$(x,x)\$.


Write a program or function that tests if a given a list of nonnegative integers is a winning hand in Numeric Mahjong. As this is a challenge, your source code (when converted to a list of integers) must be a winning hand. For example, print(1) is not a valid answer even if it implements the task, but the following may be:


whose ASCII values in sorted order is [35, 35, 35, 40, 41, 42, 49, 49, 49, 105, 105, 110, 111, 112, 114, 115, 116], which can be partitioned to five triples [35, 35, 35], [40, 41, 42], [49, 49, 49], [110, 111, 112], [114, 115, 116] and a pair [105, 105].

When converting the source code to integers, you may choose between

  • Unicode codepoints of each char (when the source code is a sequence of Unicode characters),
  • one of your language's fixed-width encodings, or
  • a raw byte sequence (when the source code has an integral number of bytes).

If there are other sensible conversions, please leave a comment.

For output, you can choose to

  • output truthy/falsy using your language's convention (swapping is allowed), or
  • use two distinct, fixed values to represent true (affirmative) or false (negative) respectively.

Standard rules apply. The shortest code in bytes wins.

Test cases


[0, 0]
[999, 999, 999, 1234, 1234]
[1, 2, 2, 2, 3]
[1, 4, 7, 2, 5, 5, 5, 8, 3, 6, 9]
[1, 2, 2, 3, 3, 4, 4, 4, 5, 5, 6, 6, 7, 7]
[1, 1, 1, 1, 2, 2, 2, 2, 3, 3, 3]
[0, 0, 0, 0, 0, 0, 0, 0]


[1, 3, 5, 7, 9]
[1, 1, 2, 2, 4, 4, 8, 8, 16, 16, 32, 32, 64, 64]
  • \$\begingroup\$ Can we assume the input is sorted? \$\endgroup\$
    – corvus_192
    Sep 25 at 11:12
  • 2
    \$\begingroup\$ @corvus_192 I assume not, since the bytes in your source code won't be sorted either. And the fourth truthy test case isn't sorted either. \$\endgroup\$ Sep 25 at 11:24
  • \$\begingroup\$ Can I output using zero/nonzero if that isn't my language's truthy/falsy convention? \$\endgroup\$ Sep 25 at 12:04
  • 3
    \$\begingroup\$ Suggested falsey test case: [1,2,3,4,5] \$\endgroup\$ Sep 25 at 12:12
  • \$\begingroup\$ @CommandMaster Sorry but no. \$\endgroup\$
    – Bubbler
    Sep 25 at 13:02

8 Answers 8


05AB1E, 47 47 44 41 38 bytes


Try it online!, or try all test cases (this code can handle in under a minute).

See the codepoints

Returns 0 for true and [] for false.


The general approach is this: Test for the existence of a permutation such that if we split it to parts of length 3, and then keep all the valid ones and count them, and multiply by 3, we should get 1 plus the length of the original input.

œ       # all permutations
ʒ       # keep those such that:
 3ô      # split to parts of length 3
 ʒ       # keep parts where:
  ¥       # their differences
  J       # joined to a string
  >       # plus one
 }       # (is equal to 1)
 g       # find the length
 y       # push the input again
 3ô      # and split to parts of length 3
 ʒ       # keep parts such that:
  ¥      # their differences
  T>     # 11
  S      # [1, 1]
  Q      # [1, 1] is equal to the differences
 g       # take their lengths
 +       # sum the lengths of the two types of parts
 3*      # times 3
 I       # input
 g       # length
 >       # plus one
 Q       # is equal to the number of valid parts times 3
ćQ      # test whether the array is empty, by comparing arr[0] == arr[1:]
,        # output - this suppresses implicit output, so the rest of the code doesn't matter
¥›HUyy  # filler to satisfy the self validation requirement
  • 1
    \$\begingroup\$ @KevinCruijssen Fixed \$\endgroup\$ Sep 25 at 12:27
  • 1
    \$\begingroup\$ Not sure if it opens up -3 bytes, but the I can be one of the s. Then you have to get rid of the J somehow (and the H and one of the s in the trailing no-ops of course). \$\endgroup\$ Sep 25 at 13:22
  • \$\begingroup\$ @KevinCruijssen Couldn't find a way to utilize that. Now it can be replaced with a y, but there aren't a lot of leftover letters which could be used to get rid of the J \$\endgroup\$ Sep 26 at 3:09

JavaScript (ES6),  143 122  119 bytes

-24 bytes thanks to @tsh!

Expects an Uint32Array. Returns a Boolean value.


Try it online!

The above function is too slow to validate itself on TIO, so this is using my original answer with an additional optimization:

Try it online!

  • \$\begingroup\$ @tsh Impressive optimizations! \$\endgroup\$
    – Arnauld
    Sep 26 at 8:22
  • \$\begingroup\$ q=([v,...f],e=2)=>-3/v?[e,5,5.5].some(n=>q(f.sort().filter(i=>p>n|i-v-n%1*p||[p*=2]%0,p=2),n<p?4>n?4:e:q%q)):e>>2 \$\endgroup\$
    – tsh
    Sep 26 at 10:41

K (ngn/k), 71 bytes


Try it online!

Based on a polynomial-time algorithm with respect to the maximal element, so it can test all test cases and itself in a second.

Let's call (x,x,x) an x-triplet and (x,x+1,x+2) an x-sequence.

  • Count the occurrence of each number in [0, 1, 2, ...] up to the maximal element that appears in the input. Let's call it C.
  • For i from 0 to the maximum index of C,
    • Construct a copy of C where i-th element is subtracted by 2. (i.e., Assume a pair is (i,i).)
    • Repeat until one of the halting condition is met:
      • If the remaining array is all zeros, return true.
      • If the remaining array contains a negative value, return false.
      • Otherwise, pop x from the front, and subtract x%3 from the first two elements. (i.e., Maximally create j-triplets and try creating j-sequences for the remaining js.)
  • If any choice of i gives true, return true. Otherwise return false.

If a partition exists and it contains three or more j-sequences for some j, three copies of j-sequences can be replaced with j-, j+1-, and j+2-triplets to get another valid partition. Also, when considering all possible partitions where the minimum value is j, choosing to have three j-sequences (compared to one j-triplet) has the effect of fixing a j+1- and j+2-triplet from the remaining numbers, leaving a strict subset of choices afterwards. Therefore, it suffices to check if a greedy partitioning (favoring triplets) leads to a valid one.


+@[-2*=3+/x;x;1+]  construct number-count arrays with all choices of pairs
      =3+/x        identity matrix of size `sum of x + 3`
   -2*             change 1s on the diagonal to -2
 @[        ;x;1+]  for each number i in x, add 1 to i-th row of the above
+                  transpose

{:[...]}'  test if each number-count array is partitionable
&/0=x;1;   if all elements are 0, return 1
|/^%x;0;   if any element is negative, return 0
           (monadic % is square root, which gives 0n for negatives;
            monadic ^ is null-check, which gives 1 for 0n)
o@@[1_x;!2;-;3!*x]  otherwise, update the array and do recursive call
    1_x             remove first element k
        !2          and update 0th and 1st elements by
           - 3!*x   subtracting k%3
o@                  recursive call

|/  return 1 if any is true

*0-0!=  filler that preserves truthiness
oo::    globally assign function to the name oo

Charcoal, 128 125 122 95 bytes


Try it online! Uses the Charcoal code page. Add the -x argument to show the hex dump which you can compare against the input. Outputs a Charcoal boolean, i.e. - for a winning hand, nothing if not. Explanation: Now a port of @Bubbler's algorithm.


Count the number of times each integer appears in the array, plus some extra dummy values.


Try each integer in case it's the pair.


Repeat until a count becomes negative or the length is reduced to 2.


Get the number of runs needed to satisfy the next integer.


Subtract them from the next two integers.


Output a - if this reduced all the integers to zero (but not negative).


Filler bytes to satisfy the source layout, wrapped in a while loop whose condition is false.

Original verbose code (without padding): Try it online!


JavaScript (Node.js), 122 bytes


Try it online!

Besides tuplets: && ++,,-- --..// AABBCC [\] ijjkkl mno rst {|}


Jelly, 50 bytes


Try it online!

The main part is a monadic link that takes a list of integers and returns an empty list for false and a list containing one or more items for truthy. To meet the challenge rules, the program includes another link above the main one which is just filler.

Completes all test cases including itself in about 17 seconds on TIO. Full explanation to follow. Note the footer turns the falsy and truthy outcomes to 0 and 1 for more compact printing.

General explanation:

  • Remove every possible combination of runs of three (including removing none)
  • Check the remaining integers are in groups of length 0 mod 3, except for one that is of length 2 mod 3


°þþ&&,@ƑƒWß»Ȥ | Filler to meet challenge requirements

WÞ                                   | Sort (technically with order based on that after wrapping each item in a list, but this doesn't affect sort order)
  W                                  | Wrap in a list
                   $Ƭ                | Repeat the following until no changes are made, gathering up intermediate results
                Ʋ€                   | - For each list in the lists of lists (example for e.g. [1, 2, 3, 4, 4])
   +þ@3                              |   - Outer addition using 1, 2, 3 e.g. [[[2, 3, 4], [3, 4, 5], [4, 5, 6], [5, 6, 7], [5, 6, 7]]
       ’                             |   - Decrease by 1 e.g. [[1, 2, 3], [2, 3, 4], [3, 4, 5], [4, 5, 6], [4, 5, 6]]
        œ&ƑƇ                         |   - Filter, keeping only those lists that were seen in the original list e.g. [[1, 2, 3], [2, 3, 4]]
            œ-@€                     |   - Filter the original list removing each of these in turn [[4, 4], [1, 4]]
                  Ẏ                  | - Join inner lists together, leaving one list of lists
                     Ẏ               | Join all gathered intermediate results into one list of lists
                           ɗ€        | For each list within this list:
                      Œɠ             | - Run lengths
                        %    3       | - Mod 3
                         ¹Ƈ          | - Only keep those non-zero (i.e. those lists that were not a multiple of 3 in length)
                              ‘      | Increase by 1
                               œ&ƑƇ3 | Only keep those that consist of a single 3

Some of the code above (e.g. using rather than ) would be suboptimal in normal code-golf challenges, but here improve length by reducing the amount of filler required.

The runtime is sufficient to complete all the test cases, but as written becomes very inefficient as the number of runs of three consecutive numbers increases. This can be greatly improved by adding a uniquify step (TIO), reducing the run time for all test cases and my original code down to ~0.3 seconds total).


Raku, 121 116 bytes

{sub k(\u){u{*}~~[.2/.1]||?(u{*}:k).grep:{?grep {u (>)$_&&k u (-)$_},^3+$_,[$_,$_,$_]}}(bag |$_)}#%&//:>>?\`aabeqqss

Try it online!

The inner subroutine name k and its argument u were somewhat haphazardly chosen to try to produce more triples and runs.

  • bag |$_ converts the argument list into a Bag (a set with multiplicity) which is then given to the inner recursive subroutine k.
  • The subroutine returns true if u{*} ~~ [.2/.1], that is, if the bag contains a single element with multiplicity 2. This is the pair that a winning mah jong hand must contain.
  • Otherwise, for the keys 𝑥 in the bag (u{*}:k), return true if the bag is a superset (u (>) $_) of either the run 𝑥, 𝑥 + 1, 𝑥 + 2 (^3 + $_) or the triple 𝑥, 𝑥, 𝑥 ([$_, $_, $_]), and the bag minus those elements (u (-) $_) produces a true result when passed to the inner recursive function k. (Normally I prefer using the Unicode characters for superset () and set minus (), which are the same number of bytes as the equivalent plain ASCII operators ((>) and (-) respectively), but then I'd have to repeat both of them twice in the trailing comment for a substantial byte penalty.)

Of course, the trailing comment #%&//:>>?\`aabeqqss makes the overall code snippet into a winning numerical mah jong hand. The function takes an infeasibly long time to validate itself; I constructed the comment by visual inspection of the function's character distribution.

The pair is: ~~

The runs are: #$%, ()*, ()*, +,-, 123, [\], [\], ^_ plus a backtick I can't figure out how to escape, pqr, pqr

The triples are: , , $$$, $$$, &&&, (((, ))), ,,,, ..., ///, :::, >>>, ???, ___, ___, aaa, bbb, eee, ggg, kkk, sss, uuu, uuu, {{{, {{{, |||, }}}, }}}

  • \$\begingroup\$ The length of the code has to be 2 mod three, so this must be invalid. Looking at the character distribution, you seem to have one ) too many, and no pair (only triples) \$\endgroup\$ Sep 26 at 3:26
  • \$\begingroup\$ @CommandMaster Thanks for the catch. The answer has been revised. \$\endgroup\$
    – Sean
    Sep 26 at 15:00

Ruby, 116 92 bytes

Brute force solution checking all permutations until it finds one that matches. Times out on medium inputs, so verification was done using the program below since it doesn't time out as easily.

Recursive approach saving 24 bytes by G B.


Attempt This Online!

Ruby, 137 bytes

Count the number of occurrences of each element. Then, for each element, removes a pair from the count (if possible), then checks if everything else is sequences or triplets.


Attempt This Online!

  • \$\begingroup\$ By using recursion: 85 bytes + padding f=->a{a[2]?a.permutation.any?{x,y,z,*a=_1;(x==y&&y==z||(x-y)*(y-z)==1)&&f[a]}:a!=a|a} \$\endgroup\$
    – G B
    Sep 27 at 10:52
  • \$\begingroup\$ And save on padding if you call the function e \$\endgroup\$
    – G B
    Sep 27 at 11:14
  • 1
    \$\begingroup\$ 89 bytes including padding: e=->a{a[2]?a.permutation.any?{_,o,z,*i=_1;_-o==(o-=z)&&o**2<2&&e[i]}:a!=a|a}#"')).11@@A[]ass \$\endgroup\$
    – G B
    Sep 27 at 13:20

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.

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