Haskell-是否有更好的方法将元素均匀分布在列表上

时间:2019-02-17 13:45:47

标签: haskell

给出这样的矩阵

matrix_table =

[[ 0, 0, 0, 0]
,[ 0, 0, 0, 0]
,[ 0, 0, 0, 0]
,[ 0, 0, 0, 0]
]

和列表position_list = [2, 3, 2, 10]

函数的输出

distribute_ones :: [[Int]] -> [Int] -> [[Int]]
distribute_ones matrix_table position_list 

应该看起来像这样

[[ 0, 1, 0, 1] -- 2 '1's in the list
,[ 0, 1, 1, 1] -- 3 '1's in the list
,[ 0, 1, 0, 1] -- 2 '1's in the list
,[ 1, 1, 1, 1] -- Since 10 > 4, all '1's in the list
]

我尝试过的事情:

我生成了列表列表,基本矩阵为

 replicate 4 (replicate 4 0)

然后用chunksOf库中的Data.List.Split划分内部列表,以制作4 - (position_list !! nth)的片段。

最后像这样1附加和连接

take 4 . concat . map (1 :)

尽管我认为这并不是最好的方法。 有更好的方法吗?

2 个答案:

答案 0 :(得分:6)

对于均匀分布的元素,我建议使用Bjorklund的算法。 Bjorklund的算法需要两个序列合并,然后重复:

  1. 尽可能多地合并两个前缀,然后每个取一个
  2. 使用合并的元素作为一个序列递归调用自身,而将较长的输入中的剩余元素作为另一序列递归调用。

在代码中:

bjorklund :: [[a]] -> [[a]] -> [a]
bjorklund xs ys = case zipMerge xs ys of
    ([], leftovers) -> concat leftovers
    (merged, leftovers) -> bjorklund merged leftovers

zipMerge :: [[a]] -> [[a]] -> ([[a]], [[a]])
zipMerge [] ys = ([], ys)
zipMerge xs [] = ([], xs)
zipMerge (x:xs) (y:ys) = ((x++y):merged, leftovers) where
    ~(merged, leftovers) = zipMerge xs ys

以下是ghci中的一些示例:

> bjorklund (replicate 2 [1]) (replicate 2 [0])
[1,0,1,0]
> bjorklund (replicate 5 [1]) (replicate 8 [0])
[1,0,0,1,0,1,0,0,1,0,0,1,0]

如果愿意,您可以编写一个小的包装程序,该包装程序只接受您关心的参数。

ones len numOnes = bjorklund
    (replicate ((-) len numOnes) [0])
    (replicate (min len numOnes) [1])

在ghci中:

> map (ones 4) [2,3,2,10]
[[0,1,0,1],[0,1,1,1],[0,1,0,1],[1,1,1,1]]

答案 1 :(得分:0)

这是另一种算法,可在一行中的itemCount个单元格中分布rowLength个项目。将currentCount初始化为0。然后对于每个单元格:

  1. itemCount添加到currentCount
  2. 如果新的currentCount小于rowLength,请使用单元格的原始值。
  3. 如果新的currentCount至少为rowLength,请减去rowLength,然后将单元格的值加1。

此算法从您提供的输入中产生期望的输出。

我们可以将所需的状态写为简单的数据结构:

data Distribution = Distribution { currentCount :: Int
                                 , itemCount    :: Int
                                 , rowLength    :: Int
                                 } deriving (Eq, Show)

在算法的每个步骤中,我们都需要知道是否要发出输出(并递增值),以及下一个状态值是什么。

nextCount :: Distribution -> Int
nextCount d = currentCount d + itemCount d

willEmit :: Distribution -> Bool
willEmit d = (nextCount d) >= (rowLength d)

nextDistribution :: Distribution -> Distribution
nextDistribution d = d { currentCount = (nextCount d) `mod` (rowLength d) }

要使其保持运行状态,我们可以将其打包在the State monad中。然后,我们可以将上面的“对于每个单元格”列表写为一个函数:

distributeCell :: Int -> State Distribution Int
distributeCell x = do
  emit <- gets willEmit
  modify nextDistribution
  return $ if emit then x + 1 else x

要在整个行上运行此命令,我们可以使用标准库中的traverse函数。这需要某种“容器”和一个将单个值映射到monadic结果,并在同一monad中创建结果的“容器”的函数。这里“容器”类型为[a],“单子”类型为State Distribution a,因此traverse的专用类型签名为

traverse :: (Int -> State Distribution Int)
         -> [Int]
         -> State Distribution [Int]

我们实际上并不关心最终状态,我们只想要结果[Int],这就是evalState所做的。这将产生:

distributeRow :: [Int] -> Int -> [Int]
distributeRow row count =
  evalState
  (traverse distributeCell row :: State Distribution [Int])
  (Distribution 0 count (length row))

将其应用于整个矩阵是zipWith的简单应用(给定两个列表和一个函数,使用两个列表中的项目对重复调用该函数,返回结果列表):

distributeOnes :: [[Int]] -> [Int] -> [[Int]]
distributeOnes = zipWith distributeRow