Enigmatic Code

Programming Enigma Puzzles

Enigma 290: Dice with a difference

From New Scientist #1438, 10th January 1985 [link]

Throwing two dice will give you a number from 2 to 12. Of course, some numbers are more likely that others. The probability of 2 for instance is 1/36; of 3 is 2/36; of 4 is 3/36; …; of 7 is 6/36; …; of 8 is 5/36; …; of 12 is 1/36.

That is true of two ordinary 6-sided dice, each bearing the letters of ENIGMA (which stand for the numbers one to six).

It is also true of this special pair of dice I have made — one with 9 sides bearing the letters IMAGINING, the other with 4 sides bearing the letters of GAGS. (S is a positive integer).

I’m not going to tell you how I constructed 9-sided and 4-sided dice. But I did, and they are fair dice. Can you interpret the MEANINGS of these fascinating facts?

[enigma290]

Advertisements

4 responses to “Enigma 290: Dice with a difference

  1. Jim Randell 26 June 2015 at 8:11 am

    This Python code runs in 49ms.

    # each die must have a single 1 on it (so that there is only one combination that makes 2)
    # so one of the following holds:
    #  A = 1 and S > 1
    #  M = 1 and S = 1
    #
    # also there can only be one combination that makes 12
    # so one of the following holds:
    #  A = 6 and S < 6
    #  M = 6 and S = 6
    #  E = 6 and (A = 5 or M = 5) and S = 7
    
    from itertools import permutations, product
    from collections import defaultdict
    from enigma import irange, printf
    
    # counts for a normal pair of dice
    normal = { 2: 1, 3: 2, 4: 3, 5: 4, 6: 5, 7: 6, 8: 5, 9: 4, 10: 3, 11: 2, 12: 1 }
    
    # possible values for E, N, I, G, M, A
    for (E, N, I, G, M, A) in permutations(irange(1, 6)):
      if not(A == 1 or M == 1): continue
      if not(A == 6 or M == 6 or E == 6): continue
      if E == 6 and not(A == 5 or M == 5): continue
      # and S
      S = (M if M == 1 or M == 6 else 7)
      # count the values for the new dice
      count = defaultdict(int)
      for (d1, d2) in product((I, M, A, G, I, N, I, N, G), (G, A, G, S)):
        v = d1 + d2
        if v not in normal: break
        c = count[v] + 1
        if c > normal[v]: break
        count[v] = c
      else:
        # check the counts have been achieved
        if all(c == normal[v] for (v, c) in count.items()):
          # output the solution
          printf("E={E} N={N} I={I} G={G} M={M} A={A} S={S}")
    

    Solution: MEANINGS = 56123247.

    So the 9-sided die has values 1, 2, 2, 3, 3, 3, 4, 4, 5. And the 4-sided die has values 1, 4, 4, 7.

    Without the analysis from the 2 and 12 cases the same program examining all the possible permutations of values for ENIGMA from 1 to 6 and S from 1 to 7 runs in 113ms.

  2. Brian Gladman 26 June 2015 at 10:11 am

    A Funny Dice problem involving non standard dice was published in the Sunday Times in March this year. The ensuing discussion involved one contributor alerting me to an interesting mathematical approach for solving puzzles of this general type. Here is a solution to this enigma teaser using the approach (it requires my number theory and polynomial libraries).

    from itertools import combinations, permutations
    from functools import reduce
    from operator import mul
    
    from number_theory import divisors
    from polynomial import Poly, cyclotomic
    
    # output the face values of a die specified by polynomial p
    def die(p):
      d = []
      for e, v in enumerate(p.coeff):
        d += [e] * v
      return tuple(sorted(d))
    
    def dice(faces):
      # for saving solutions
      sol = []
      # holds the cyclotomic polynomial factorisations of the dice 
      # generating polynomial x + x^2 + ... + x^faces  
      l_poly = []
      for d in divisors(faces):
        if d > 1:
          l_poly += [cyclotomic(d)]
      ln_poly = len(l_poly)
      l_p2 = l_poly + l_poly
      # now choose the number of polynomial factors for the
      # first die
      for nf in range(1, ln_poly + 1):
        # check all combinations of this number of polynomials
        for f in combinations(range(len(l_p2)), nf):
          # list the polynomials for the first die  
          t = [l_p2[i] for i in f]
          # multiply them (and x) for the polynomial for the first die
          p1 = reduce(mul, t, Poly([0, 1]))
          # list the remaining polynomials for the second die
          t = [l_p2[i] for i in range(len(l_p2)) if i not in f]
          # multiply them (and x) for the polynomial for the second die
          p2 = reduce(mul, t, Poly([0, 1]))
          # all polynomial coefficients must be non-negative without zeros
          if all(x >= 0 for x in p1.coeff + p2.coeff) and p1(0) == p2(0):
            # save the solution if it is new
            t = tuple(sorted((die(p1), die(p2))))
            if t not in sol:
              sol += [t]
              yield t
    
    # generate pairs of dice giving the same results as normal
    # six sided dice when thrown as pairs and added
    for p in dice(6):
      # order the dice on their numbers of faces
      d1, d2 = sorted(p, key=len)
      # consider only dice pairs with 4 and 9 faces that have three
      # and five distinct face values respectively
      if (len(d1) == 4 and len(d2) == 9 and
            len(set(d1)) == 3 and len(set(d2)) == 5):
    
        # they must share a value that must also occur twice on each die
        for g in (x for x in set(d1) if d1.count(x) == d2.count(x) == 2):
          # now permute the other two numbers on die one for A and S
          for a, s in permutations(set(d1).difference([g])):
            # and check that A's number occurs only once on die 2
            if d2.count(a) == 1:
              # now allocate the other numbers on die two to I, M and N
              for i, m, n in permutations(set(d2).difference([g, a])):
                # and check that they occur the correct number of times
                if d2.count(i) == 3 and d2.count(m) == 1 and d2.count(n) == 2:
                  # E's value occurs on normal die but not on die two
                  for e in set(range(1, 7)).difference(d2):
                    d = dict(zip('EGASIMN', (e, g, a, s) + (i, m, n)))
                    mngs = ''.join(str(d[x]) for x in 'MEANINGS') 
                    print('MEANINGS = {}, dice = {}, {}'.format(mngs, d1, d2))
    

    It takes a lot more effort to program (and is slower too) but the maths is elegant and that appeals to me.

  3. hakank 26 June 2015 at 7:58 pm

    Here’s a MiniZinc solution of #290: http://hakank.org/dice_with_a_difference_enigma_290.mzn
    I especially like the declarative constraints of this.

    int: n = 9; % first die, 9 sides, IMAGINING
    int: f = 4; % second die, 4 sides, GAGS
    int: e = 6; % ENIGMA dice, 6 sides
    
    int: k = 12;
    array[2..k] of int: probs = array1d(2..k, [1,2,3,4,5,6,5,4,3,2,1]);
    
    % decision variables
    var 1..k: I;
    var 1..k: M;
    var 1..k: A;
    var 1..k: G;
    var 1..k: N;
    var 1..k: S;
    var 1..e: E;
    
    array[1..n] of var int: dice9 = [I,M,A,G,I,N,I,N,G];
    array[1..f] of var int: dice4 = [G,A,G,S];
    array[1..e] of var int: dice6 = [E,N,I,G,M,A];
    
    solve satisfy;
    
    constraint
     forall(k in 2..k) (
       probs[k] = sum(i in 1..n, j in 1..f) ( dice9[i]+dice4[j] == k) /\
       probs[k] = sum(i,j in 1..e) ( dice6[i]+dice6[j] == k)
     )
    ;
    
    output [
      "dice9 (IMAGINING): ", show(dice9), "\n",
      "dice4 (GAGS): ", show(dice4), "\n",
      "dice6 (ENIGMA): ", show(dice6), "\n",
      "MEANINGS: ", show([M,E,A,N,I,N,G,S]), "\n"
    ];
    
  4. hakank 26 June 2015 at 8:39 pm

    And here’s a MiniZinc model of the Funny Dice problem, using the same basic approach for calculating the probabilities (though now they are decision variables instead of constants): http://hakank.org/minizinc/funny_dice.mzn

    Gecode solves this in 48ms using “var int”, or 36ms when using var 1..12 as the domains.

    include "globals.mzn"; 
    int: n = 6;
    
    % decision variables
    % Note: It doesn't matter if we use "var int" or var 1..12 as the domains.
    % Gecode solves this in 48ms using var int, and 38ms using var 1..12.
    % In "production", I always use as small domains as possible (but not smaller).
    array[1..n] of var 1..12: red;
    array[1..n] of var 1..12: blue;
    array[2..12] of var 1..12: probs;
    
    % array[1..n] of var int: red;
    % array[1..n] of var int: blue;
    % array[2..12] of var int: probs;
    
    % solve satisfy;
    solve :: int_search(probs ++ red ++ blue, occurrence, indomain_min, complete) satisfy;
    
    constraint
     % domains
     forall(i in 1..n) ( red[i] >= 1 /\ blue[i] >= 1) /\
    
     % The total of the six numbers on the red die is higher than the same total for the blue die.
     sum(red) > sum(blue) /\
    
     % ensure the probabilities
     forall(k in 2..12) (
       probs[k] >= 1 /\ % domains
       probs[k] = sum(i,j in 1..n) ( red[i]+blue[j] == k)
     )
     /\ % the relations of the probabilities
     forall(i in 2..6) (
       probs[i] = probs[12-i+2] /\
       probs[i]  probs[6+i] 
     )
     /\ probs[7] = max(probs)
    
     % symmetry breaking (without it, it takes longer time)
     /\ increasing(red) 
     /\ increasing(blue) 
    ;
    
    output [
      "probs: ", show(probs), "\n",
      "red: ", show(red), "\n",
      "blue: ", show(blue), "\n",
    ];
    

    Related: The Nontransitive dice problem (with some explorations): http://hakank.org/minizinc/nontransitive_dice.mzn

Leave a Comment

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

%d bloggers like this: