【问题标题】:Has anyone seen a programming puzzle similar to this?有没有人见过类似的编程难题?
【发布时间】:2010-10-29 03:39:54
【问题描述】:

“假设你想用一排 4×1 和 6×1 的乐高积木拼成一个实心面板。为了结构强度,积木之间的空间绝不能在相邻的行中排列。例如,18×下面显示的 3 面板是不可接受的,因为顶部两行中的块之间的空间是对齐的。

构建 10×1 面板有 2 种方法,构建 10×2 面板有 2 种方法,构建 18×3 面板有 8 种方法,构建 36×5 面板有 7958 种方法。

构建 64×10 面板有多少种不同的方法?答案将适合 64 位有符号整数。编写程序计算答案。你的程序应该运行得非常快——当然,它不应该超过一分钟,即使在旧机器上也是如此。让我们知道您的程序计算的值,您的程序计算该值所用的时间,以及您在哪种机器上运行它。包括程序的源代码作为附件。 "

我最近收到了一个编程难题,并且一直在绞尽脑汁试图解决它。我使用 c++ 编写了一些代码,我知道这个数字很大......我的程序运行了几个小时,然后我决定停止它,因为即使在慢速计算机上也需要 1 分钟的运行时间。有没有人见过类似的谜题?已经有几个星期了,我不能再提交了,但这真的让我很烦恼,我无法正确解决它。关于使用算法的任何建议?或者也许可能的解决方法是“开箱即用”。我采用的是制作一个程序,该程序构建每个可能的 4x1 和 6x1 块“层”以制作 64x1 层。结果证明有大约 3300 个不同的层。然后我让我的程序运行并将它们堆叠到所有可能的 10 层高墙上,这些墙没有排列的裂缝......正如你所看到的,这个解决方案需要很长时间。所以很明显,蛮力似乎并不能在时间限制内有效地解决这个问题。任何建议/见解将不胜感激。

【问题讨论】:

  • 这里应该有图片吗?我没有看到。我猜这是因为你不能发布图片,除非你有超过 15 个 Rep。
  • 整数分区可能是解决方案的一部分:en.wikipedia.org/wiki/Integer_partition
  • 我认为您的 3,300 数字是错误的,根据我编写的程序,它接近 47,000。也许你没有考虑到顺序。
  • @Pax:不,他是对的。 3328 种方式。 (不是基于程序,而是简单的组合)有 10 种方法来拥有 1 个四板和 10 个六板。有 495 种方法可以有 4 个四板和 8 个六板(495 == 12 选择 4)。 7个四板和6个六板有1716种方式(1716 == 13选7)。等等。继续将四板的数量增加 3,将六板的数量减少 2。然后将所有可能性相加:10 + 495 + 1716 + 1001 + 105 + 1 = 3228。(我的意思是“选择",见en.wikipedia.org/wiki/Combination)
  • @Daniel,我发现我的代码 (PEBCAK) 有问题。一个小改动,1x4+10x6有11种排列方式。除此之外,你的数字是正确的。

标签: language-agnostic dynamic-programming


【解决方案1】:

主要观点是:在确定第 3 行中的内容时,您并不关心第 1 行中的内容,只关心第 2 行中的内容。

因此,让我们将如何构建 64x1 图层称为“行场景”。您说大约有 3300 行场景。这还不错。

让我们计算一个函数:

f(s, r) = 将行场景号“s”放入“r”行的方式数,并合法填充“r”以上的所有行。

(我在顶部是“1”行,底部是“10”行)

如果您想避免剧透,请立即停止阅读。

现在清楚了(将我们的行从 1 编号到 10):

f(s, 1) = 1

对于“s”的所有值。

另外,这就是洞察力的来源,(使用 Mathematica-ish 表示法)

f(s, r) = Sum[ f(i, r-1) * fits(s, i) , {i, 1, 3328} ]

其中“fits”是一个函数,它接受两个场景编号,如果您可以合法地将这两行堆叠在一起,则返回“1”,如果不能,则返回“0”。这使用了洞察力,因为放置场景的合法方式的数量仅取决于根据“适合度”在其上方放置场景的方式的数量。

现在,可以预先计算拟合并将其存储在 3328 x 3328 字节数组中。这只是大约 10 兆的内存。 (如果你看中并存储为位数组,则更少)

那么答案显然就是

Sum[ f(i, 10) , {i, 1, 3328} ]

【讨论】:

  • 我不明白你是如何计算 f(s,r) 的,你能告诉我这个吗?
  • @Morpheus - 你的问题太模糊,我无法充分回答;具体有什么不明白的? f(s,r) 由给定公式计算; f(s, 1) = 1f(s,2) = f(1,1)*fits(s,1) + f(2,1)*fits(s,2) + f(3,1)*fits(s,3) + ... + f(3328,1)*fits(s,3328),你会得到 f(s,r) 更大的 r 类似。但这就是我上面已经说过的,所以你肯定在问别的问题。
  • 感谢您的回复。我正要说 fit(s,i) 实际上,你能不能给出一些伪代码,这样我就可以清楚地描绘出来,我们如何将两行堆叠在一起!
【解决方案2】:

这是我的答案。它是 Haskell,除其他外,您可以免费获得 bignums。

编辑:它现在实际上在合理的时间内解决了问题。

更多编辑:使用稀疏矩阵在我的计算机上需要半秒钟。

您计算每一种可能的方式来平铺一行。假设有 N 种方法来平铺一行。制作一个 NxN 矩阵。如果第 i 行可以出现在第 j 行旁边,则元素 i,j 为 1,否则为 0。从包含 N 个 1 的向量开始。将矩阵乘以向量的次数等于墙的高度减 1,然后对结果向量求和。

module Main where
import Data.Array.Unboxed
import Data.List
import System.Environment
import Text.Printf
import qualified Data.Foldable as F
import Data.Word
import Data.Bits

-- This records the index of the holes in a bit field
type Row = Word64

-- This generates the possible rows for given block sizes and row length
genRows :: [Int] -> Int -> [Row]
genRows xs n = map (permToRow 0 1) $ concatMap comboPerms $ combos xs n
  where
    combos [] 0 = return []
    combos [] _ = [] -- failure
    combos (x:xs) n =
      do c <- [0..(n `div` x)]
         rest <- combos xs (n - x*c)
         return (if c > 0 then (x, c):rest else rest)
    comboPerms [] = return []
    comboPerms bs =
      do (b, brest) <- choose bs
         rest <- comboPerms brest
         return (b:rest)
    choose bs = map (\(x, _) -> (x, remove x bs)) bs
    remove x (bc@(y, c):bs) =
      if x == y
         then if c > 1
                 then (x, c - 1):bs
                 else bs
         else bc:(remove x bs)
    remove _ [] = error "no item to remove"
    permToRow a _ [] = a
    permToRow a _ [_] = a
    permToRow a n (c:cs) =
      permToRow (a .|. m) m cs where m = n `shiftL` c

-- Test if two rows of blocks are compatible
-- i.e. they do not have a hole in common
rowCompat :: Row -> Row -> Bool
rowCompat x y = x .&. y == 0

-- It's a sparse matrix with boolean entries
type Matrix = Array Int [Int]
type Vector = UArray Int Word64

-- Creates a matrix of row compatibilities
compatMatrix :: [Row] -> Matrix
compatMatrix rows = listArray (1, n) $ map elts [1..n] where
  elts :: Int -> [Int]
  elts i = [j | j <- [1..n], rowCompat (arows ! i) (arows ! j)]
  arows = listArray (1, n) rows :: UArray Int Row
  n = length rows

-- Multiply matrix by vector, O(N^2)
mulMatVec :: Matrix -> Vector -> Vector
mulMatVec m v = array (bounds v)
    [(i, sum [v ! j | j <- m ! i]) | i <- [1..n]]
  where n = snd $ bounds v

initVec :: Int -> Vector
initVec n = array (1, n) $ zip [1..n] (repeat 1)

main = do
  args <- getArgs
  if length args < 3
    then putStrLn "usage: blocks WIDTH HEIGHT [BLOCKSIZE...]"
    else do
      let (width:height:sizes) = map read args :: [Int]
      printf "Width: %i\nHeight %i\nBlock lengths: %s\n" width height
             $ intercalate ", " $ map show sizes
      let rows = genRows sizes width
      let rowc = length rows
      printf "Row tilings: %i\n" rowc
      if null rows
        then return ()
        else do
          let m = compatMatrix rows
          printf "Matrix density: %i/%i\n"
                 (sum (map length (elems m))) (rowc^2)
          printf "Wall tilings: %i\n" $ sum $ elems
                  $ iterate (mulMatVec m) (initVec (length rows))
                            !! (height - 1)

结果……

$ time ./a.out 64 10 4 6
Width: 64
Height 10
Block lengths: 4, 6
Row tilings: 3329
Matrix density: 37120/11082241
Wall tilings: 806844323190414

real    0m0.451s
user    0m0.423s
sys     0m0.012s

好的,500 毫秒,我可以忍受。

【讨论】:

  • @Jreeter:是的,我相信是的。措辞有点笨拙,因为我通常认为矩阵 x 向量与右侧的向量相乘。所以如果矩阵为M,向量为V,则结果为M^(height-1)V
  • 向量中的每一个元素、矩阵中的行、矩阵中的每列都对应一个图案X。向量中的一个元素记录了构建高度为N的具有图案的墙的不同方式的数量X 在顶部。所以(4, 3, 9) 表示模式#1 的4 个可能的墙、#2 的3 个和#3 的9 个可能的墙。将矩阵乘以高度 N 的向量可以得到高度 N+1 的向量。如果你仔细阅读矩阵乘法的定义并计算出一个小例子,你就会看到它是如何工作的。
  • 就我对这种方法的看法而言,您会凭直觉识别出诸如“哦,我知道——这就是矩阵乘法”之类的东西。然后你可以通过并证明矩阵乘法解决了这个问题,或者如果你非常擅长数学(研究生),你的直觉已经足够发达,你可以跳过证明,因为你认为它很明显。 (这就是为什么一些数学教授觉得教数学很困难。)
  • @Jreeter:让 A 成为我们正在讨论的矩阵。 A 相当稀疏。然而,随着 K 的增加,A^K 变得越来越密集。我怀疑一旦 K 达到 3 左右就根本没有零。所以计算 A^K 是不可能的,但我们不需要计算 A^K。我们只需要计算 A^KV,其中 V 是我们的起始向量。乘法是关联的,因此我们可以将其计算为A(A(...A(AV))...)) 而不是((...(AA)A...)A)V。这为我们提供了 O(NM) 数量级的操作,其中 N 是高度,M 是矩阵中非零项的数量。
  • 相比之下,直接计算 A^N 大约需要 O(J^3*log(N)),其中 J 是矩阵的宽度,N 是墙的高度。如您所见,如果 N 非常大,直接计算 A^N 会变得更加高效。当然,这是使用朴素矩阵乘法,但使用更好的乘法算法只会使 J^3 更接近 J^2。
【解决方案3】:

我在编程竞赛中解决了一个类似的问题,该竞赛用各种形状的瓷砖拼贴了一条长长的走廊。我使用了动态编程:给定任何面板,有一种方法可以通过一次放置一行来构建它。每行的末端可以有有限多个形状。因此,对于每个行数,每个形状,我计算有多少种方法可以制作该行。 (对于底行,每个形状都有一种制作方法。)然后每一行的形状决定了下一行可以采用的形状数量(即永远不要排列空格)。这个数字对于每一行都是有限的,事实上因为你只有两种尺寸的砖块,它会很小。因此,您最终会在每行花费固定的时间,并且程序会很快完成。

为了表示一个形状,我只需列出一个 4 和 6 的列表,然后使用这个列表作为表中的键来存储在行 i 中制作该形状的方法的数量,例如每个

【讨论】:

  • 你好,你能详细说明一下这个方法让我理解吗?真的很有帮助!
  • 请求代码,否则很难理解。
猜你喜欢
  • 2011-08-21
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2020-04-27
  • 2020-10-11
  • 1970-01-01
  • 1970-01-01
  • 2011-01-07
相关资源
最近更新 更多