KenKen puzzle addends: REDUX A (corrected) non-recursive algorithm

后端 未结 9 1662
眼角桃花
眼角桃花 2021-01-03 05:24

This question relates to those parts of the KenKen Latin Square puzzles which ask you to find all possible combinations of ncells numbers with values x such that 1 <= x &

相关标签:
9条回答
  • 2021-01-03 05:47

    And here is another recursive, generator-based solution, but this time using some simple math to calculate ranges at each step, avoiding needless recursion:

    def latinSquares(max_val, target_sum, n_cells):
      if n_cells == 1:
        assert(max_val >= target_sum >= 1)
        return ((target_sum,),)
      else:
        lower_bound = max(-(-target_sum / n_cells), 1)
        upper_bound = min(max_val, target_sum - n_cells + 1)
        assert(lower_bound <= upper_bound)
        return ((v,) + w for v in xrange(upper_bound, lower_bound - 1, -1)
                         for w in latinSquares(v, target_sum - v, n_cells - 1))
    

    This code will fail with an AssertionError if you supply parameters that are impossible to satisfy; this is a side-effect of my "correctness criterion" that we never do an unnecessary recursion. If you don't want that side-effect, remove the assertions.

    Note the use of -(-x/y) to round up after division. There may be a more pythonic way to write that. Note also I'm using generator expressions instead of yield.

    for m in latinSquares(6,12,4):
      print m
    
    0 讨论(0)
  • 2021-01-03 05:49

    Little bit offtopic, but still might help at programming kenken.

    I got good results using DLX algorhitm for solving Killer Sudoku (very simmilar as KenKen it has cages, but only sums). It took less than second for most of problems and it was implemented in MATLAB language.

    reference this forum http://www.setbb.com/phpbb/viewtopic.php?t=1274&highlight=&mforum=sudoku

    killer sudoku "look at wikipedia, cant post hyper link" damt spammers

    0 讨论(0)
  • 2021-01-03 05:51

    Here a simple solution in C/C++:

    const int max = 6;
    int sol[N_CELLS];
    
    void enum_solutions(int target, int n, int min) {
      if (target == 0 && n == 0)
        report_solution(); /* sol[0]..sol[N_CELLS-1] is a solution */
      if (target <= 0 || n == 0) return; /* nothing further to explore */
      sol[n - 1] = min; /* remember */
      for (int i = min; i <= max; i++)
        enum_solutions(target - i, n - 1, i);
    }
    
    enum_solutions(12, 4, 1);
    
    0 讨论(0)
  • 2021-01-03 05:53

    Here is a naive, but succinct, solution using generators:

    def descending(v):
      """Decide if a square contains values in descending order"""
      return list(reversed(v)) == sorted(v)
    
    def latinSquares(max_val, target_sum, n_cells):
      """Return all descending n_cells-dimensional squares,
         no cell larger than max_val, sum equal to target_sum."""
      possibilities = itertools.product(range(1,max_val+1),repeat=n_cells)
      for square in possibilities:
        if descending(square) and sum(square) == target_sum:
          yield square
    

    I could have optimized this code by directly enumerating the list of descending grids, but I find itertools.product much clearer for a first-pass solution. Finally, calling the function:

    for m in latinSquares(6, 12, 4):
      print m
    
    0 讨论(0)
  • 2021-01-03 05:54

    First of all, I'd use variable names that mean something, so that the code gets comprehensible. Then, after I understood the problem, it's clearly a recursive problem, as once you have chosen one number, the question of finding the possible values for the rest of the squares are exactly the same problem, but with different values in.

    So I would do it like this:

    from __future__ import division
    from math import ceil
    
    def make_combos(max_val,target_sum,n_cells):
        combos = []
        # The highest possible value of the next cell is whatever is 
        # largest of the max_val, or the target_sum minus the number 
        # of remaining cells (as you can't enter 0).
        highest = min(max_val, target_sum - n_cells + 1)
        # The lowest is the lowest number you can have that will add upp to 
        # target_sum if you multiply it with n_cells.
        lowest = int(ceil(target_sum/n_cells))
        for x in range(highest, lowest-1, -1):
            if n_cells == 1: # This is the last cell, no more recursion.
                combos.append((x,))
                break
            # Recurse to get the next cell:
            # Set the max to x (or we'll get duplicates like
            # (6,3,2,1) and (6,2,3,1), which is pointless.
            # Reduce the target_sum with x to keep the sum correct.
            # Reduce the number of cells with 1.
            for combo in make_combos(x, target_sum-x, n_cells-1):
                combos.append((x,)+combo)
        return combos
    
    if __name__ == '__main__':
        import pprint
        # And by using pprint the output gets easier to read
        pprint.pprint(make_combos( 6,12,4))
    

    I also notice that your solution still seems buggy. For the values max_val=8, target_sum=20 and n_cells=5 your code doesn't find the solution (8,6,4,1,1,), as an example. I'm not sure if that means I've missed a rule in this or not, but as I understand the rules that should be a valid option.

    Here's a version using generators, It saves a couple of lines, and memory if the values are really big, but as recursion, generators can be tricky to "get".

    from __future__ import division
    from math import ceil
    
    def make_combos(max_val,target_sum,n_cells):
        highest = min(max_val, target_sum - n_cells + 1)
        lowest = int(ceil(target_sum/n_cells))
        for x in xrange(highest, lowest-1, -1):
            if n_cells == 1:
                yield (x,)
                break
            for combo in make_combos(x, target_sum-x, n_cells-1):
                yield (x,)+combo
    
    if __name__ == '__main__':
        import pprint
        pprint.pprint(list(make_combos( 6,12,4)))
    
    0 讨论(0)
  • 2021-01-03 05:59

    Your algorithm seems pretty good at first blush, and I don't think OO or another language would improve the code. I can't say if recursion would have helped but I admire the non-recursive approach. I bet it was harder to get working and it's harder to read but it likely is more efficient and it's definitely quite clever. To be honest I didn't analyze the algorithm in detail but it certainly looks like something that took a long while to get working correctly. I bet there were lots of off-by-1 errors and weird edge cases you had to think through, eh?

    Given all that, basically all I tried to do was pretty up your code as best I could by replacing the numerous C-isms with more idiomatic Python-isms. Often times what requires a loop in C can be done in one line in Python. Also I tried to rename things to follow Python naming conventions better and cleaned up the comments a bit. Hope I don't offend you with any of my changes. You can take what you want and leave the rest. :-)

    Here are the notes I took as I worked:

    • Changed the code that initializes tmp to a bunch of 1's to the more idiomatic tmp = [1] * n_cells.
    • Changed for loop that sums up tmp_sum to idiomatic sum(tmp).
    • Then replaced all the loops with a tmp = <list> + <list> one-liner.
    • Moved raise doneException to init_tmp_new_ceiling and got rid of the succeeded flag.
    • The check in init_tmp_new_ceiling actually seems unnecessary. Removing it, the only raises left were in make_combos_n_cells, so I just changed those to regular returns and dropped doneException entirely.
    • Normalized mix of 4 spaces and 8 spaces for indentation.
    • Removed unnecessary parentheses around your if conditions.
    • tmp[p2] - tmp[p1] == 0 is the same thing as tmp[p2] == tmp[p1].
    • Changed while True: if new_ceiling_flag: break to while not new_ceiling_flag.
    • You don't need to initialize variables to 0 at the top of your functions.
    • Removed combos list and changed function to yield its tuples as they are generated.
    • Renamed tmp to combo.
    • Renamed new_ceiling_flag to ceiling_changed.

    And here's the code for your perusal:

    def initial_combo(ceiling=5, target_sum=13, num_cells=4):
        """
        Returns a list of possible addends, probably to be modified further.
        Starts a new combo list, then, starting from left, fills items to ceiling
        or intermediate between 1 and ceiling or just 1.  E.g.:
        Given ceiling = 5, target_sum = 13, num_cells = 4: creates [5,5,2,1].
        """
        num_full_cells = (target_sum - num_cells) // (ceiling - 1)
    
        combo = [ceiling] * num_full_cells \
              + [1]       * (num_cells - num_full_cells)
    
        if num_cells > num_full_cells:
            combo[num_full_cells] += target_sum - sum(combo)
    
        return combo
    
    def all_combos(ceiling, target_sum, num_cells):
        # p0   points at the rightmost item and moves left under some conditions
        # p1   starts out at rightmost items and steps left
        # p2   starts out immediately to the left of p1 and steps left as p1 does
        #      So, combo[p2] and combo[p1] always point at a pair of adjacent items.
        # d    combo[p2] - combo[p1]; immediate difference
        # cd   combo[p2] - combo[p0]; cumulative difference
    
        # The ceiling decreases by 1 each iteration.
        while True:
            combo = initial_combo(ceiling, target_sum, num_cells)
            yield tuple(combo)
    
            ceiling_changed = False
    
            # Generate all of the remaining combos with this ceiling.
            while not ceiling_changed:
                p2, p1, p0 = -2, -1, -1
    
                while combo[p2] == combo[p1] and abs(p2) <= num_cells:
                    # 3,3,3,3
                    if abs(p2) == num_cells:
                        return
    
                    p2 -= 1
                    p1 -= 1
                    p0 -= 1
    
                cd = 0
    
                # slide_ptrs_left loop
                while abs(p2) <= num_cells:
                    d   = combo[p2] - combo[p1]
                    cd += d
    
                    # 5,5,3,3 or 5,5,4,3
                    if cd > 1:
                        if abs(p2) < num_cells:
                            # 5,5,3,3 --> 5,4,4,3
                            if d > 1:
                                combo[p2] -= 1
                                combo[p1] += 1
                            # d == 1; 5,5,4,3 --> 5,4,4,4
                            else:
                                combo[p2] -= 1
                                combo[p0] += 1
    
                            yield tuple(combo)
    
                        # abs(p2) == num_cells; 5,4,4,3
                        else:
                            ceiling -= 1
                            ceiling_changed = True
    
                        # Resume at make_combo_same_ceiling while
                        # and follow branch.
                        break
    
                    # 4,3,3,3 or 4,4,3,3
                    elif cd == 1:
                        if abs(p2) == num_cells:
                            return
    
                        p1 -= 1
                        p2 -= 1
    
    if __name__ == '__main__':
        print list(all_combos(ceiling=6, target_sum=12, num_cells=4))
    
    0 讨论(0)
提交回复
热议问题