4

这个问题与 KenKen 拉丁方谜题的那些部分有关,这些部分要求您找到 ncells 数字的所有可能组合,其值为 x 使得 1 <= x <= maxval 和 x(1) + ... + x(ncells) =目标总和。在测试了几个更有希望的答案之后,我将把答案奖授予 Lennart Regebro,因为:

  1. 他的例行程序和我的一样快(+-5%),并且

  2. 他指出我原来的例程在某个地方有一个错误,这让我看到了它真正想要做什么。谢谢,伦纳特。

chrispy 贡献了一个似乎与 Lennart 相当的算法,但 5 小时后,sooo,第一个得到它。

备注:Alex Martelli 的基本递归算法是一个示例,它制作了所有可能的组合并将它们全部扔到筛子上,看看哪个穿过了孔。这种方法比 Lennart 或我的方法花费的时间要长 20 倍以上。(将输入提升到 max_val = 100,n_cells = 5,target_sum = 250,在我的盒子上是 18 秒 vs. 8+ 分钟。) 道德:不生成所有可能的组合是好的。

另一句话:Lennart 和我的例程以相同的顺序生成相同的答案。它们实际上是从不同角度看到的相同算法吗?我不知道。

我发生了什么事。如果您对答案进行排序,例如,以 (8,8,2,1,1) 开头并以 (4,4,4,4,4) 结尾(max_val=8, n_cells=5, target_sum =20),该系列形成一种“最慢下降”,第一个是“热”,最后一个是“冷”,中间有尽可能多的阶段。这与“信息熵”有关吗?观察它的正确指标是什么?是否有一种算法可以按热量的降序(或升序)产生组合?(据我所见,这个没有,尽管它在很短的时间内很接近,看着标准化的标准开发。)

这是 Python 例程:

#!/usr/bin/env python
#filename: makeAddCombos.07.py -- stripped for StackOverflow

def initialize_combo( max_val, n_cells, target_sum):
    """returns combo
    Starting from left, fills combo to max_val or an intermediate value from 1 up.  
    E.g.:  Given max_val = 5, n_cells=4, target_sum = 11, creates [5,4,1,1].
    """
    combo = []
    #Put 1 in each cell.
    combo += [1] * n_cells
    need = target_sum - sum(combo)
    #Fill as many cells as possible to max_val.
    n_full_cells = need //(max_val - 1)
    top_up = max_val - 1
    for i in range( n_full_cells): combo[i] += top_up
    need = target_sum - sum(combo)
    # Then add the rest to next item.
    if need > 0:
        combo[n_full_cells] += need
    return combo
#def initialize_combo()

def scrunch_left( combo):
    """returns (new_combo,done)
    done   Boolean; if True, ignore new_combo, all done;
            if Falso, new_combo is valid.

    Starts a new combo list.  Scanning from right to left, looks for first
    element at least 2 greater than right-end element.  
    If one is found, decrements it, then scrunches all available counts on its
    right up against its right-hand side.  Returns the modified combo.
    If none found, (that is, either no step or single step of 1), process
    done.
    """
    new_combo = []
    right_end = combo[-1]
    length = len(combo)
    c_range = range(length-1, -1, -1)
    found_step_gt_1 = False
    for index in c_range:
        value = combo[index]
        if (value - right_end) > 1:
            found_step_gt_1 = True
            break
    if not found_step_gt_1:
        return ( new_combo,True)

    if index > 0:
        new_combo += combo[:index]
    ceil = combo[index] - 1
    new_combo += [ceil]
    new_combo += [1] * ((length - 1) - index)
    need = sum(combo[index:]) - sum(new_combo[index:])
    fill_height = ceil - 1
    ndivf = need // fill_height
    nmodf = need % fill_height
    if ndivf > 0:
        for j in range(index + 1, index + ndivf + 1):
            new_combo[j] += fill_height
    if nmodf > 0:
        new_combo[index + ndivf + 1] += nmodf
    return (new_combo, False)
#def scrunch_left()

def make_combos_n_cells_ge_two( combos, max_val, n_cells, target_sum):
    """
    Build combos, list of tuples of 2 or more addends.
    """
    combo = initialize_combo( max_val, n_cells, target_sum)
    combos.append( tuple( combo))
    while True:
        (combo, done) = scrunch_left( combo)
        if done:
            break
        else:
            combos.append( tuple( combo))
    return combos
#def make_combos_n_cells_ge_two()

if __name__ == '__main__':

    combos = []
    max_val     = 8
    n_cells     = 5
    target_sum  = 20
    if n_cells == 1: combos.append( (target_sum,))
    else:
        combos = make_combos_n_cells_ge_two( combos, max_val, n_cells, target_sum)
    import pprint
    pprint.pprint( combos)
4

9 回答 9

3

乍一看,您的算法似乎相当不错,而且我认为 OO 或其他语言不会改进代码。我不能说递归是否会有所帮助,但我很欣赏非递归方法。我敢打赌,它更难开始工作,也更难阅读,但它可能更有效,而且绝对非常聪明。老实说,我没有详细分析算法,但它确实看起来需要很长时间才能正常工作。我敢打赌,您必须仔细考虑很多错误和奇怪的边缘情况,是吗?

考虑到这一切,基本上我所做的就是尽可能地完善你的代码,用更惯用的 Python 主义替换众多的 C 主义。很多时候,在 C 语言中需要循环的事情可以在 Python 中用一行来完成。此外,我尝试重命名以更好地遵循 Python 命名约定,并稍微清理了注释。希望我的任何更改都不会冒犯您。你可以拿走你想要的,留下剩下的。:-)

以下是我在工作时做的笔记:

  • 将初始化tmp为一堆 1 的代码更改为更惯用的tmp = [1] * n_cells.
  • 改变for循环总结tmp_sum为惯用sum(tmp)的。
  • 然后用单线替换所有循环tmp = <list> + <list>
  • 移动raise doneExceptioninit_tmp_new_ceiling摆脱了succeeded旗帜。
  • 签入init_tmp_new_ceiling实际上似乎没有必要。删除它,只剩下raises 了make_combos_n_cells,所以我只是将它们更改为常规返回并doneException完全删除。
  • 用于缩进的 4 个空格和 8 个空格的标准化组合。
  • 删除了if条件周围不必要的括号。
  • tmp[p2] - tmp[p1] == 0和 是一样的tmp[p2] == tmp[p1]
  • 改为。while True: if new_ceiling_flag: break_while not new_ceiling_flag
  • 您不需要在函数顶部将变量初始化为 0。
  • 删除combos列表并将函数更改yield为其生成的元组。
  • 重命名tmpcombo.
  • 重命名new_ceiling_flagceiling_changed.

这是供您阅读的代码:

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))
于 2009-06-30T04:34:45.397 回答
2

这是我能想到的最简单的递归解决方案,“找到所有可能的 n 数字与值 x 的组合,使得 1 <= x <= max_val 和 x(1) + ... + x(n) = target”。我正在从头开始开发它。这是一个根本没有任何优化的版本,只是为了简单起见:

def apcnx(n, max_val, target, xsofar=(), sumsofar=0):
  if n==0:
    if sumsofar==target:
      yield xsofar
    return

  if xsofar:
    minx = xsofar[-1] - 1
  else:
    minx = 0

  for x in xrange(minx, max_val):
    for xposs in apcnx(n-1, max_val, target, xsofar + (x+1,), sumsofar+x+1):
      yield xposs

for xs in apcnx(4, 6, 12):
  print xs

基本情况n==0(我们不能再产生更多的数字)如果满足条件则产生到目前为止的元组,或者什么都不产生,然后完成(返回)。

如果我们应该产生比我们迄今为止构建的更长的元组,那么if/else确保我们只产生非递减的元组,以避免重复(你确实说“组合”而不是“排列”)。

尝试“this”项目的for所有可能性,并循环遍历下一个较低级别的递归仍然能够产生的任何内容。

我看到的输出是:

(1, 1, 4, 6)
(1, 1, 5, 5)
(1, 2, 3, 6)
(1, 2, 4, 5)
(1, 3, 3, 5)
(1, 3, 4, 4)
(2, 2, 2, 6)
(2, 2, 3, 5)
(2, 2, 4, 4)
(2, 3, 3, 4)
(3, 3, 3, 3)

这似乎是正确的。

有无数种可能的优化,但请记住:

先让它工作,然后让它快速

我与 Kent Beck 通信,正确地将这句话归于“Python in a Nutshell”中,他告诉我他是从他父亲那里得到的,他的工作实际上与编程无关;-)。

在这种情况下,在我看来,关键问题是了解发生了什么,任何优化都可能会干扰,所以我全力以赴“简单易懂”;如果需要,我们可以在 OP 确认他们可以理解这个纯粹的、未优化的版本中发生的事情后优化它!

于 2009-06-30T04:58:43.767 回答
2

首先,我会使用有意义的变量名,以便代码易于理解。然后,在我理解了这个问题之后,这显然是一个递归问题,因为一旦你选择了一个数字,找到其余正方形的可能值的问题就是完全相同的问题,但其中的值不同。

所以我会这样做:

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))

我还注意到您的解决方案似乎仍然存在问题。例如,对于max_val=8, target_sum=20 and n_cells=5您的代码找不到解决方案的值(8,6,4,1,1,)。我不确定这是否意味着我错过了这个规则,但据我所知,这些规则应该是一个有效的选项。

这是一个使用生成器的版本,如果值非常大,它可以节省几行代码和内存,但是作为递归,生成器可能很难“获取”。

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)))
于 2009-06-30T05:37:53.983 回答
1

很抱歉,您的代码有点长,而且不是特别可读。如果您可以尝试以某种方式对其进行总结,也许有人可以帮助您将其写得更清楚。

至于问题本身,我的第一个想法是使用递归。(据我所知,你已经在这样做了。再次抱歉我无法阅读你的代码。)想一个方法,你可以将问题简化为同一问题的更小更简单的版本,重复,直到你有一个一个非常简单的答案的琐碎案例。

更具体一点,您有这三个参数,max_val、target_sum 和 n_cells。您能否将其中一个数字设置为某个特定值,以便给您一个完全不需要思考的极其简单的问题?一旦你有了这个,你能把问题的稍微难一点的版本减少到已经解决的问题吗?

编辑:这是我的代码。我不喜欢它进行重复数据删除的方式。我确信有一种更 Pythonic 的方式。此外,它不允许在一个组合中两次使用相同的数字。要撤消此行为,只需取出行if n not in numlist:。我不确定这是否完全正确,但它似乎有效并且(恕我直言)更具可读性。您可以轻松添加记忆,这可能会加快速度。

def get_combos(max_val, target, n_cells):
    if target <= 0:
        return []
    if n_cells is 1:
        if target > max_val:
            return []
        else:
            return [[target]]
    else:
        combos = []
        for n in range(1, max_val+1, 1):
            for numlist in get_combos(max_val, target-n, n_cells-1):
                if n not in numlist:
                    combos.append(numlist + [n])
        return combos

def deduplicate(combos):
    for numlist in combos:
        numlist.sort()
    answer = [tuple(numlist) for numlist in combos]
    return set(answer)

def kenken(max_val, target, n_cells):
    return deduplicate(get_combos(max_val, target, n_cells))
于 2009-06-30T04:32:51.030 回答
1

首先,我自己正在学习 Python,所以这个解决方案不会很好,但这只是解决这个问题的尝试。我试图以递归方式解决它,我认为递归解决方案将是此类问题的理想选择,尽管递归解决方案可能不是这个:

def GetFactors(maxVal, noOfCells, targetSum):
    l = []
    while(maxVal != 0):
        remCells = noOfCells - 1
        if(remCells > 2):
            retList = GetFactors(maxVal, remCells, targetSum - maxVal)
            #Append the returned List to the original List
            #But first, add the maxVal to the start of every elem of returned list.
            for i in retList:
                i.insert(0, maxVal)
            l.extend(retList)

        else:
            remTotal = targetSum - maxVal
            for i in range(1, remTotal/2 + 1):
                itemToInsert = remTotal - i;
                if (i > maxVal or itemToInsert > maxVal):
                    continue
                l.append([maxVal, i, remTotal - i])
        maxVal -= 1
    return l



if __name__ == "__main__":
    l = GetFactors(5, 5, 15)
    print l
于 2009-06-30T06:31:16.770 回答
1

这是 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);
于 2009-06-30T07:10:49.753 回答
1

这是一个使用生成器的简单但简洁的解决方案:

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

我本可以通过直接枚举降序网格列表来优化此代码,但我发现 itertools.product 对于首次通过的解决方案来说更加清晰。最后,调用函数:

for m in latinSquares(6, 12, 4):
  print m
于 2009-06-30T09:58:23.677 回答
1

这是另一个基于生成器的递归解决方案,但这次使用一些简单的数学来计算每一步的范围,避免不必要的递归:

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))

如果您提供无法满足的参数,此代码将失败并返回 AssertionError;这是我的“正确性标准”的副作用,我们从不进行不必要的递归。如果您不想要这种副作用,请删除断言。

注意除法后使用 -(-x/y) 进行四舍五入。可能有一种更 Pythonic 的方式来编写它。另请注意,我使用的是生成器表达式而不是 yield。

for m in latinSquares(6,12,4):
  print m
于 2009-06-30T10:23:57.523 回答
1

有点离题,但仍然可能有助于编程 kenken。

使用 DLX 算法解决杀手数独问题我得到了很好的结果(与 KenKen 非常相似,它有笼子,但只有总和)。大多数问题只用了不到一秒,并且是用 MATLAB 语言实现的。

参考这个论坛 http://www.setbb.com/phpbb/viewtopic.php?t=1274&highlight=&mforum=sudoku

杀手数独“看看维基百科,不能发布超链接”该死的垃圾邮件发送者

于 2009-06-30T13:03:02.180 回答