KenKen 谜题加数:REDUX A(已修正)非递归算法
这个问题与 KenKen 拉丁方谜题的那些部分有关,这些部分要求您找到 ncells 数字与 x 值的所有可能组合,使得 1 <= x <= maxval 且 x(1) + ... + x(ncells ) = 目标总和。 在测试了几个更有希望的答案后,我将把答案奖授予 Lennart Regebro,因为:
他的例程和我的一样快 (+-5%),并且
他指出我原来的例程在某个地方有一个错误,这让我明白了它真正想要做什么。 谢谢,Lennart。
chrispy 贡献了一种算法,看起来与 Lennart 的算法相当,但 5 小时后,很快,第一个电线就得到了它。
备注: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)
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 <= maxval and x(1) + ... + x(ncells) = targetsum. Having tested several of the more promising answers, I'm going to award the answer-prize to Lennart Regebro, because:
his routine is as fast as mine (+-5%), and
he pointed out that my original routine had a bug somewhere, which led me to see what it was really trying to do. Thanks, Lennart.
chrispy contributed an algorithm that seems equivalent to Lennart's, but 5 hrs later, sooo, first to the wire gets it.
A remark: Alex Martelli's bare-bones recursive algorithm is an example of making every possible combination and throwing them all at a sieve and seeing which go through the holes. This approach takes 20+ times longer than Lennart's or mine. (Jack up the input to max_val = 100, n_cells = 5, target_sum = 250 and on my box it's 18 secs vs. 8+ mins.) Moral: Not generating every possible combination is good.
Another remark: Lennart's and my routines generate the same answers in the same order. Are they in fact the same algorithm seen from different angles? I don't know.
Something occurs to me. If you sort the answers, starting, say, with (8,8,2,1,1) and ending with (4,4,4,4,4) (what you get with max_val=8, n_cells=5, target_sum=20), the series forms kind of a "slowest descent", with the first ones being "hot" and the last one being "cold" and the greatest possible number of stages in between. Is this related to "informational entropy"? What's the proper metric for looking at it? Is there an algorithm that producs the combinations in descending (or ascending) order of heat? (This one doesn't, as far as I can see, although it's close over short stretches, looking at normalized std. dev.)
Here's the Python routine:
#!/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)
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论
评论(9)
抱歉,您的代码有点长,而且可读性不是很好。 如果你可以尝试以某种方式总结它,也许有人可以帮助你写得更清楚。
至于问题本身,我的第一个想法是使用递归。 (据我所知,您已经这样做了。再次抱歉,我无法阅读您的代码。)想出一种方法,可以将问题简化为同一问题的更小更简单的版本,反复进行,直到您有一个一个简单的案例,一个非常简单的答案。
更具体一点,您有这三个参数:max_val、target_sum 和 n_cells。 您能否将其中一个数字设置为某个特定值,以便为您提供一个根本不需要思考的极其简单的问题? 一旦你有了这个,你能将问题的稍微困难的版本减少到已经解决的版本吗?
编辑:这是我的代码。 我不喜欢它重复数据删除的方式。 我确信还有一种更 Pythonic 的方式。 此外,它不允许在一个组合中两次使用相同的数字。 要撤消此行为,只需删除
if n not in numlist:
行即可。 我不确定这是否完全正确,但它似乎有效并且(恕我直言)更具可读性。 您可以轻松地添加记忆,这可能会大大加快速度。Sorry to say, your code is kind of long and not particularly readable. If you can try to summarize it somehow, maybe someone can help you write it more clearly.
As for the problem itself, my first thought would be to use recursion. (For all I know, you're already doing that. Sorry again for my inability to read your code.) Think of a way that you can reduce the problem to a smaller easier version of the same problem, repeatedly, until you have a trivial case with a very simple answer.
To be a bit more concrete, you have these three parameters, max_val, target_sum, and n_cells. Can you set one of those numbers to some particular value, in order to give you an extremely simple problem requiring no thought at all? Once you have that, can you reduce the slightly harder version of the problem to the already solved one?
EDIT: Here is my code. I don't like the way it does de-duplication. I'm sure there's a more Pythonic way. Also, it disallows using the same number twice in one combination. To undo this behavior, just take out the line
if n not in numlist:
. I'm not sure if this is completely correct, but it seems to work and is (IMHO) more readable. You could easily add memoization and that would probably speed it up quite a bit.首先,我自己正在学习Python,所以这个解决方案不会很好,但这只是解决这个问题的尝试。 我尝试以递归方式解决它,我认为递归解决方案对于此类问题来说是理想的选择,尽管该递归解决方案可能不是这个:
First of all, I am learning Python myself so this solution won't be great but this is just an attempt at solving this. I have tried to solve it recursively and I think a recursive solution would be ideal for this kind of problem although THAT recursive solution might not be this one:
这是 C/C++ 中的简单解决方案:
Here a simple solution in C/C++:
有点离题,但仍然可能对 kenken 编程有所帮助。
我使用 DLX 算法求解 Killer Sudoku 得到了很好的结果(与 KenKen 非常相似,它有笼子,但只有求和)。 大多数问题的处理时间不到一秒,并且是用 MATLAB 语言实现的。
参考这个论坛
http://www.setbb.com/ phpbb/viewtopic.php?t=1274&highlight=&mforum=sudoku
杀手数独
“看看维基百科,不能发布超链接”该死的垃圾邮件发送者
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
这是一个使用生成器的简单但简洁的解决方案:
我可以通过直接枚举降序网格列表来优化此代码,但我发现 itertools.product 对于首次解决方案来说更加清晰。 最后,调用该函数:
Here is a naive, but succinct, solution using generators:
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:
这是另一个基于生成器的递归解决方案,但这次使用一些简单的数学来计算每一步的范围,避免不必要的递归:
如果您提供无法满足的参数,此代码将失败并出现断言错误; 这是我的“正确性标准”的副作用,即我们从不进行不必要的递归。 如果您不希望出现这种副作用,请删除断言。
请注意除法后使用 -(-x/y) 进行舍入。 可能有一种更Pythonic的方式来编写它。 另请注意,我正在使用 生成器表达式的产量。
And here is another recursive, generator-based solution, but this time using some simple math to calculate ranges at each step, avoiding needless recursion:
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.
你的算法乍一看似乎相当不错,而且我不认为面向对象或其他语言会改进代码。 我不能说递归是否有帮助,但我很欣赏非递归方法。 我敢打赌,它更难工作,也更难阅读,但它可能更高效,而且绝对非常聪明。 老实说,我没有详细分析该算法,但它看起来确实需要很长时间才能正常工作。 我敢打赌,你必须考虑很多 1 误差和奇怪的边缘情况,是吗?
考虑到所有这些,基本上我想做的就是通过用更惯用的 Python 主义替换大量的 C 主义来尽我所能地美化你的代码。 很多时候,在 C 中需要循环的事情可以在 Python 中用一行完成。 我还尝试重命名一些东西以更好地遵循 Python 命名约定,并清理了一些注释。 希望我的任何改变不会冒犯您。 你可以拿走你想要的,留下其余的。 :-)
以下是我工作时所做的笔记:
tmp
为一堆 1 的代码更改为更惯用的tmp = [1] * n_cells
。tmp_sum
求和的for
循环更改为惯用的sum(tmp)
。tmp =
一行。; +
raise didException
移至init_tmp_new_ceiling
并删除了succeeded
标志。init_tmp_new_ceiling
中的检查实际上似乎没有必要。 删除它后,剩下的唯一的raise
位于make_combos_n_cells
中,所以我只是将它们更改为常规返回,并完全删除doneException
。if
条件周围不必要的括号。tmp[p2] - tmp[p1] == 0
与tmp[p2] == tmp[p1]
相同。while True: if new_ceiling_flag: break
更改为while not new_ceiling_flag
。tmp
重命名为combo
。new_ceiling_flag
重命名为ceiling_changed
。这是供您细读的代码:
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:
tmp
to a bunch of 1's to the more idiomatictmp = [1] * n_cells
.for
loop that sums uptmp_sum
to idiomaticsum(tmp)
.tmp = <list> + <list>
one-liner.raise doneException
toinit_tmp_new_ceiling
and got rid of thesucceeded
flag.init_tmp_new_ceiling
actually seems unnecessary. Removing it, the onlyraise
s left were inmake_combos_n_cells
, so I just changed those to regular returns and droppeddoneException
entirely.if
conditions.tmp[p2] - tmp[p1] == 0
is the same thing astmp[p2] == tmp[p1]
.while True: if new_ceiling_flag: break
towhile not new_ceiling_flag
.combos
list and changed function toyield
its tuples as they are generated.tmp
tocombo
.new_ceiling_flag
toceiling_changed
.And here's the code for your perusal:
首先,我会使用有意义的变量名,以便代码易于理解。 然后,在我理解这个问题之后,这显然是一个递归问题,因为一旦你选择了一个数字,找到其余平方的可能值的问题是完全相同的问题,但具有不同的值。
所以我会这样做:
我还注意到你的解决方案似乎仍然有问题。 对于值
max_val=8、target_sum=20 和 n_cells=5
,您的代码找不到解决方案(8,6,4,1,1,)
,如下一个例子。 我不确定这是否意味着我错过了这方面的规则,但据我了解,这应该是一个有效的选项。这是一个使用生成器的版本,如果值确实很大,它可以节省几行和内存,但作为递归,生成器可能很难“获取”。
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:
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".
这是我能想到的最简单的递归解决方案,“找到 n 个数字与 x 值的所有可能组合,使得 1 <= x <= max_val 且 x(1) + ... + x(n) = target” 。 我正在从头开始开发它。 这是一个根本没有任何优化的版本,只是为了简单起见:
基本情况
n==0
(我们无法产生更多数字)如果满足条件,则产生到目前为止的元组,或什么都不做,然后完成(返回)。如果我们应该生成比迄今为止构建的更长的元组,则
if/else
确保我们只生成非递减元组,以避免重复(您确实说的是“组合”而不是“排列”)。for 尝试“this”项的所有可能性,并循环遍历下一个较低级别的递归仍然能够产生的结果。
我看到的输出是:
这似乎是正确的。
有无数种可能的优化,但是请记住:
我与 Kent Beck 通信,在“Python in a Nutshell”中正确地引用了这句话,他告诉我这是从他父亲那里得到的,他的工作实际上与编程无关;-)。
在这种情况下,在我看来,关键问题是理解正在发生的事情,任何优化都可能会产生干扰,所以我会全力以赴“简单易懂”; 如果需要的话,一旦OP确认他们可以理解这个纯粹的、未经优化的版本中发生了什么,我们就可以对其进行优化!
Here's the simplest recursive solution that I can think of to "find all possible combinations of n numbers with values x such that 1 <= x <= max_val and x(1) + ... + x(n) = target". I'm developing it from scratch. Here's a version without any optimization at all, just for simplicity:
The base case
n==0
(where we can't yield any more numbers) either yield the tuple so far if it satisfies the condition, or nothing, then finishes (returns).If we're supposed to yield longer tuples than we've built so far, the
if/else
makes sure we only yield non-decreasing tuples, to avoid repetition (you did say "combination" rather than "permutation").The
for
tries all possibilities for "this" item and loops over whatever the next-lower-down level of recursion is still able to yield.The output I see is:
which seems correct.
There are a bazillion possible optimizations, but, remember:
I corresponded with Kent Beck to properly attribute this quote in "Python in a Nutshell", and he tells me he got it from his dad, whose job was actually unrelated to programming;-).
In this case, it seems to me that the key issue is understanding what's going on, and any optimization might interfere, so I'm going all out for "simple and understandable"; we can, if need be!, optimize the socks off it once the OP confirms they can understand what's going on in this sheer, unoptimized version!