什么样的算法会在合理的时间内找到正方形网格?

时间:2016-03-23 14:49:18

标签: algorithm optimization

我发现有一个大小为9x13的网格,其中包含以下属性:

每个单元格在基数10中包含一个数字。

可以通过选择起始方格从网格中读取数字,转到其最近的8个网格中的一个,保持该方向并连接数字。

例如,如果我们有以下网格:

340934433
324324893
455423343

然后可以选择最左边的上部数字3并选择向右和向下的方向以读取数字3,32和325.

现在必须证明有一个大小为9x13的网格,其中一个人可以读取1到100的正方形,即一个人可以读取形式i ^ 2的所有整数,其中i = 1,...,从广场100。

我在网上找到的最佳网格大小为11x11,在Solving a recreational square packing problem中给出。但看起来很难修改程序以找到矩形网格中的整数。

那么什么样的算法会在合理的时间内输出合适的网格?

我从这段代码中得到了一个关键错误:

import random, time, sys

N = 9
M = 13
K = 100

# These are the numbers we would like to pack
numbers = [str(i*i) for i in xrange(1, K+1)]

# Build the global list of digits (used for weighted random guess)
digits = "".join(numbers)

def random_digit(n=len(digits)-1):
    return digits[random.randint(0, n)]

# By how many lines each of the numbers is currently covered
count = dict((x, 0) for x in numbers)

# Number of actually covered numbers
covered = 0

# All lines in current position (row, cols, diags, counter-diags)
lines = (["*"*N for x in xrange(N)] +
         ["*"*M for x in xrange(M)] +
         ["*"*x for x in xrange(1, N)] + ["*"*x for x in xrange(N, 0, -1)] +
         ["*"*x for x in xrange(1, M)] + ["*"*x for x in xrange(M, 0, -1)])

# lines_of[x, y] -> list of line/char indexes
lines_of = {}
def add_line_of(x, y, L):
    try:
        lines_of[x, y].append(L)
    except KeyError:
        lines_of[x, y] = [L]
for y in xrange(N):
    for x in xrange(N):
        add_line_of(x, y, (y, x))
        add_line_of(x, y, (M + x, y))
        add_line_of(x, y, (2*M + (x + y), x - max(0, x + y - M + 1)))
        add_line_of(x, y, (2*M + 2*N-1 + (x + N-1 - y), x - max(0, x + (M-1 - y) - M + 1)))

# Numbers covered by each line
covered_numbers = [set() for x in xrange(len(lines))]

# Which numbers the string x covers
def cover(x):
    c = x + "/" + x[::-1]
    return [y for y in numbers if y in c]

# Set a matrix element
def setValue(x, y, d):
    global covered
    for i, j in lines_of[x, y]:
        L = lines[i]
        C = covered_numbers[i]
        newL = L[:j] + d + L[j+1:]
        newC = set(cover(newL))
        for lost in C - newC:
            count[lost] -= 1
            if count[lost] == 0:
                covered -= 1
        for gained in newC - C:
            count[gained] += 1
            if count[gained] == 1:
                covered += 1
        covered_numbers[i] = newC
        lines[i] = newL

def do_search(k, r):
    start = time.time()

    for i in xrange(r):
        x = random.randint(0, N-1)
        y = random.randint(0, M-1)
        setValue(x, y, random_digit())

    best = None
    attempts = k
    while attempts > 0:
        attempts -= 1
        old = []
        for ch in xrange(1):
            x = random.randint(0, N-1)
            y = random.randint(0, M-1)
            old.append((x, y, lines[y][x]))
            setValue(x, y, random_digit())
        if best is None or covered > best[0]:
            now = time.time()
            sys.stdout.write(str(covered) + chr(13))
            sys.stdout.flush()
            attempts = k
        if best is None or covered >= best[0]:
            best = [covered, lines[:N][:]]
        else:
            for x, y, o in old[::-1]:
                setValue(x, y, o)
    print
    sys.stdout.flush()
    return best

for y in xrange(N):
    for x in xrange(N):
        setValue(x, y, random_digit())

best = None
while True:
    if best is not None:
        for y in xrange(M):
            for x in xrange(N):
                setValue(x, y, best[1][y][x])
    x = do_search(100000, M)
    if best is None or x[0] > best[0]:
        print x[0]
        print "\n".join(" ".join(y) for y in x[1])
    if best is None or x[0] >= best[0]:
        best = x[:]

1 个答案:

答案 0 :(得分:0)

要创建这样的网格,我首先要用一个表示第一个K(100)数字的方块的字符串列表。

尽可能地减少这些字符串,其中很多字符串包含在其他字符串中(例如,625包含25,因此625覆盖5和25的正方形)。

这应该产生81个唯一正方形的初始列表,需要至少约312个数字:

def construct_optimal_set(K):
  # compute a minimal solution:
  numbers = [str(n*n) for n in range(0,K+1)]
  min_numbers = []
  # note: go in reverse direction, biggest to smallest, to maximize elimination of smaller numbers later
  while len(numbers) > 0:
    i = 0
    while i < len(min_numbers):
      q = min_numbers[i]
      qr = reverse(min_numbers[i])
      # check if the first number is contained within any element of min_numbers
      if numbers[-1] in q or numbers[-1] in qr:
        break
      # check if any element of min_numbers is contained within the first number
      elif q in numbers[-1] or qr in numbers[-1]:
        min_numbers[i] = numbers[-1]
        break
      i += 1
    # if not found, add it
    if i >= len(min_numbers):
      min_numbers.append(numbers[-1])
    numbers = numbers[:-1]
  min_numbers.sort()
  return min_numbers

这将返回一组最小的正方形,其中删除了其他正方形子集的任何正方形。通过连接任何大多数重叠的元素(例如484和841到4841)来扩展它;我把它留作练习,因为它会熟悉这段代码。

然后,你组装这些类似于跨字谜题。在组合值时,根据可能的未来重叠概率进行打包,通过计算每个数字的权重(例如,1&#39; s相当常见,9&#39s不常见,所以如果选择,你我会赞成重叠9而不是1)。

使用类似以下代码的内容来构建当前网格中表示的所有可能值的列表。在构建时定期使用它,以消除已经表示的方块,以及测试您的网格是否是完整的解决方案。

def merge(digits):
  result = 0
  for i in range(len(digits)-1,-1,-1):
    result = result * 10 + digits[i]
  return result
def merge_reverse(digits):
  result = 0
  for i in range(0, len(digits)):
    result = result * 10 + digits[i]
  return result

# given a grid where each element contains a single numeric digit,
# return list of every ordering of those digits less than SQK,
# such that you pick a starting point and one of eight directions,
# and assemble digits until either end of grid or larger than SQK;
# this will construct only the unique combinations;
# also note that this will not construct a large number of values,
# since for any given direction, there are at most
#     (sqrt(n*n + m*m))!
# possible arrangements, and there will rarely be that many.
def construct_lines(grid, k):
  # rather than build a dictionary type, use a little more memory to use faster simple array indexes;
  # index is #, and value at index indicates existence: 0 = does not exist, >0 means exists in grid
  sqk = k*k
  combinations = [0]*(sqk+1)

  # do all horizontals, since they are easiest
  for y in range(len(grid)):
    digits = []
    for x in range(len(grid[y])):
      digits.append(grid[y][x])
      # for every possible starting point...
      for q in range(1,len(digits)):
        number = merge(digits[q:])
        if number <= sqk:
          combinations[number] += 1

  # now do all verticals
  # note that if the grid is really square, grid[0] will give an accurate width of all grid[y][] rows
  for x in range(len(grid[0])):
    digits = []
    for y in range(len(grid)):
      digits.append(grid[y][x])
      # for every possible starting point...
      for q in range(1,len(digits)):
        number = merge(digits[q:])
        if number <= sqk:
          combinations[number] += 1

  # the longer axis (x or y) in both directions will contain every possible diagonal
  # e.g. x is the longer axis here (using random characters to more easily distinguish idea):
  # [1 2 3 4]
  # [a b c d]
  # [. , $ !]
  # 'a,' can be obtained by reversing the diagonal starting on the bottom and working up and to the left
  # this means that every set must be reversed as well
  if len(grid) > len(grid[0]):

    # for each y, grab top and bottom in each of two diagonal directions, for a total of four sets,
    # and include the reverse of each set
    for y in range(len(grid)):

      digitsul = []  # origin point upper-left, heading down and right
      digitsur = []  # origin point upper-right, heading down and left
      digitsll = []  # origin point lower-left, heading up and right
      digitslr = []  # origin point lower-right, heading up and left

      revx = len(grid[y])-1 # pre-adjust this for computing reverse x coordinate

      for deltax in range(len(grid[y])):  # this may go off the grid, so check bounds
        if y+deltax < len(grid):
          digitsul.append(grid[y+deltax][deltax])
          digitsll.append(grid[y+deltax][revx - deltax])

          for q in range(1,len(digitsul)):
            number = merge(digitsul[q:])
            if number <= sqk:
              combinations[number] += 1
            number = merge_reverse(digitsul[q:])
            if number <= sqk:
              combinations[number] += 1

          for q in range(1,len(digitsll)):
            number = merge(digitsll[q:])
            if number <= sqk:
              combinations[number] += 1
            number = merge_reverse(digitsll[q:])
            if number <= sqk:
              combinations[number] += 1

        if y-deltax >= 0:
          digitsur.append(grid[y-deltax][deltax])
          digitslr.append(grid[y-deltax][revx - deltax])

          for q in range(1,len(digitsur)):
            number = merge(digitsur[q:])
            if number <= sqk:
              combinations[number] += 1
            number = merge_reverse(digitsur[q:])
            if number <= sqk:
              combinations[number] += 1

          for q in range(1,len(digitslr)):
            number = merge(digitslr[q:])
            if number <= sqk:
              combinations[number] += 1
            number = merge_reverse(digitslr[q:])
            if number <= sqk:
              combinations[number] += 1

  else:

    # for each x, ditto above
    for x in range(len(grid[0])):

      digitsul = []  # origin point upper-left, heading down and right
      digitsur = []  # origin point upper-right, heading down and left
      digitsll = []  # origin point lower-left, heading up and right
      digitslr = []  # origin point lower-right, heading up and left

      revy = len(grid)-1 # pre-adjust this for computing reverse y coordinate

      for deltay in range(len(grid)):  # this may go off the grid, so check bounds
        if x+deltay < len(grid[0]):
          digitsul.append(grid[deltay][x+deltay])
          digitsll.append(grid[revy - deltay][x+deltay])

          for q in range(1,len(digitsul)):
            number = merge(digitsul[q:])
            if number <= sqk:
              combinations[number] += 1
            number = merge_reverse(digitsul[q:])
            if number <= sqk:
              combinations[number] += 1

          for q in range(1,len(digitsll)):
            number = merge(digitsll[q:])
            if number <= sqk:
              combinations[number] += 1
            number = merge_reverse(digitsll[q:])
            if number <= sqk:
              combinations[number] += 1

        if x-deltay >= 0:
          digitsur.append(grid[deltay][x-deltay])
          digitslr.append(grid[revy - deltay][x - deltay])

          for q in range(1,len(digitsur)):
            number = merge(digitsur[q:])
            if number <= sqk:
              combinations[number] += 1
            number = merge_reverse(digitsur[q:])
            if number <= sqk:
              combinations[number] += 1

          for q in range(1,len(digitslr)):
            number = merge(digitslr[q:])
            if number <= sqk:
              combinations[number] += 1
            number = merge_reverse(digitslr[q:])
            if number <= sqk:
              combinations[number] += 1

  # now filter for squares only
  return [i for i in range(0,k+1) if combinations[i*i] > 0]

构建网格总体上计算成本很高,但您只需要为每个可能的位置运行一次检查功能,以选择最佳位置。

通过查找可以放置一系列数字的重叠区域的子集来优化放置 - 这在所需的时间方面应该是可以容忍的,因为您可以限制要检查的可能位置的数量;例如您可以将其限制在10(再次,通过实验找到最佳数字),这样您可以针对上述函数测试前10个可能的展示位置,以确定哪个展示位置(如果有)添加了最可能的正方形。随着您的进步,您将有更少的可能插入数字的位置,因此,在搜索可能的展示位置变得更加昂贵,相互平衡的同时,测试哪个展示位置最好在计算上更便宜。

这不会处理所有组合,并且不会像尝试每种可能的排列和计算覆盖多少个方格那样紧密打包,因此可能会遗漏一些,但与O((N * M)!)相比,此算法实际上将在你的一生中完成(我实际上估计在一台不错的计算机上几分钟 - 如果你并行检查安置,则更多)。