Enigmatic Code

Programming Enigma Puzzles

Enigma 1455: Deck calendar

From New Scientist #2616, 11th August 2007

A standard pack of playing cards has 52 cards and (counting 1 point for an ace, 2 for a 2, and so on up to 13 for a king) it is worth a total of 364 points. So, on Sunday 1 January one year I took a pack and noted that the number of days yet to come in the year equalled the number of points in the pack.

Then the following Sunday and every Sunday for the rest of the year I discarded one card and counted the points left in the remaining pack. So, finally, after doing this on Sunday 31 December, there were no points left and no days left, and once again the number of days yet to come equalled the number of points left in the pack.

In fact the point count and day count were equal every fortnight from January 1 (and on some other occasions). On all the remaining Sundays the point count was either a palindrome or a perfect square or a perfect cube.

The week after discarding the king of clubs, I discarded the king of spades, and the week after discarding the king of diamonds, I discarded the king of hearts.

On which four dates did I discard a 6?

[enigma1455]

Advertisements

2 responses to “Enigma 1455: Deck calendar

  1. Jim Randell 4 March 2013 at 8:38 am

    This Python program runs in 1.5s. It uses the yield from ... construct, so requires at least Python 3.3. I think it could be made to run quicker by folding the requirement for the pairs of kings to be discarded in adjacent weeks into the recursive solver, but it would make the code a little messier.

    from collections import defaultdict
    from datetime import date, timedelta
    from enigma import irange, is_square, is_cube, printf
    
    def is_palindrome(n):
      s = str(n)
      return s == s[::-1]
    
    # special numbers are either palindromes, squares, cubes
    special = set(n for n in irange(1, 364) if is_palindrome(n) or is_square(n) or is_cube(n))
    
    def check(s):
      # check the kings
      k = tuple(i for (i, v) in enumerate(s) if v == 13)
      if not(k[0] + 1 == k[1] and k[2] + 1 == k[3]): return
      # return the week numbers of the sixes
      return tuple(i for (i, v) in enumerate(s) if v == 6)
    
    def solve(s, r):
      if r == 0:
        yield s
        return
      t = 364 - sum(s)
      if len(s) % 2 == 0:
        # on even length point count is special
        for n in irange(1, 13):
          if s.count(n) == 4: continue
          if t - n in special:
            yield from solve(s + [n], r - 7)
      else:
        # on odd length point count and days remaining is the same
        # (or equivalently: n = 14 - s[-1], as pairwise the numbers must sum 14)
        n = t - r + 7
        if s.count(n) < 4:
          yield from solve(s + [n], r - 7)
    
    # find possibile weeks for the sixes
    r = defaultdict(int)
    for s in solve([], 364):
      x = check(s)
      if x is None: continue
      r[x] += 1
      printf("[sixes={x} cards={s}]")
    
    # output the results
    for x, n in r.items():
      # turn the week numbers into dates (2006 works as a template year)
      d1 = date(2006, 1, 1)
      printf("{d} [{n} solutions]", d=' / '.join((d1 + timedelta(i * 7)).strftime("%d %b") for i in x))
    

    Solution: The sixes were discarded on 2nd April, 7th May, 11th June and 19th November.

    There are sixteen possible sequences of discards which give the solution. The program above finds them all.

  2. Brian Gladman 4 March 2013 at 12:05 pm

    Here is my solution which is also recursive but steps forward two weeks at a time:

    from datetime import date, timedelta
    
    # check for a number that is a palindrome
    def is_pal(x):
      s = str(x) 
      return s == s[::-1]
    
    # check for a number that is a perfect root
    def is_root(x, n):
      return round(x ** (1.0 / n)) ** n == x
    
    # numbers that are palindromes, square roots or cube roots
    special = set(x for x in range(365) 
                  if is_pal(x) or is_root(x, 2) or is_root(x, 3))
    
    # step forward a fortnight at a time
    def step(cards, days_left, card_seq):
      if days_left:
        # place the next two cards - any available cards unless we
        # placed a king last time and hence place one again now
        if not card_seq or card_seq[-1] != 13:
          lo, hi = 1, 13
        else:
          lo, hi = 13, 14
        # try each possible pair of cards for this fortnight
        for c in range(lo, hi):
          # if the two cards are available
          if cards[c] and cards[14 - c]:
            # if the card is a 7 then the intermediate card 
            # count and days left will match, otherwsie the
            # intermediate days left has to be 'special'
            if c == 7 or days_left - c in special:
              # take the two cards out of the pack 
              cards[c] -= 1
              cards[14 - c] -= 1
              # and move onto the next fortnight
              step(cards, days_left - 14, card_seq + [c, 14 - c])
              cards[14 - c] += 1
              cards[c] += 1
      else:
        # we have a solution - locate the four 6 cards
        ix, s = -1, []
        for i in range(4):
          ix = card_seq.index(6, ix + 1)
          # get the date the card was discarded as a string
          d = date(2007, 1, 1) + timedelta(days = 7 * ix)
          # compile the four dates
          s += [d.strftime('%d %B')]
        # output the solution
        print(', '.join(s), card_seq)
    
    # set a full pack of cards and 364 days left          
    cards = {x:4 for x in range(1, 14)}
    step(cards, 364, [])
    

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: