按照“视觉相似度”对数字列表进行排序

时间:2016-12-18 17:16:19

标签: sorting haskell

考虑一个函数,它对两个数字之间的“视觉相似度”进行评级:666666和666166非常相似,不像666666和111111

type N = Int
type Rate = Int

similar :: N -> N -> Rate
similar a b = length . filter id . zipWith (==) a' $ b'
  where a' = show a
        b' = show b

similar 666666 666166
--> 5
-- high rate : very similar

similar 666666 111111
--> 0
-- low rate : not similar

将会有更复杂的实现,但这符合目的。

目的是找到一个对N的给定列表进行排序的函数,以便每个项目与它的前一个项目最相似。由于第一个项目没有前任,因此必须有一个给定的前N个。

similarSort :: N -> [N] -> [N]

让我们看看一些示例数据:它们不需要具有相同的arity,但它可以更容易推理它。

sample :: [N]
sample = [2234, 8881, 1222, 8888, 8822, 2221, 5428]

可能会试图像这样实现这个功能:

similarSortWrong x xs = reverse . sortWith (similar x) $ xs

但这会导致错误的结果:

similarSortWrong 2222 sample
--> [2221,1222,8822,2234,5428,8888,8881]

一开始它看起来是正确的,但显然8822应该跟8881,因为它与2234更相似。

所以这是我提出的实现:

similarSort _ [] = []
similarSort x xs = x : similarSort a as
  where (a:as) = reverse . sortWith (similar x) $ xs

similarSort 2222 sample
--> [2222,2221,2234,1222,8822,8888,8881]

似乎有效。但它似乎也做了比必要更多的工作。每一步,整个休息都会再次排序,只是为了获取第一个元素。通常懒惰应该允许这样做,但reverse可能会再次破坏这一点。如果有人知道这个问题是否存在共同的抽象,我会很想听到。

1 个答案:

答案 0 :(得分:3)

实现您要求的贪婪算法相对简单。让我们从一些样板开始;我们会将these包用于zip - 就像我们将拉链式列表的“未使用”尾端交给我们一样:

import Data.Align
import Data.These
sampleStart = "2222"
sampleNeighbors = ["2234", "8881", "1222", "8888", "8822", "2221", "5428"]

我不会使用数字,而是使用数字列表 - 只需这样我们就不必在代码之间进行转换,方便用户使用的表单和方便算法的表单。关于如何对两位数字符串的相似性进行评分,你有点模糊,所以让它尽可能具体:任何数字不同的数字为1,如果数字字符串的长度不同,我们必须为每个扩展名支付1在右边。因此:

distance :: Eq a => [a] -> [a] -> Int
distance l r = sum $ alignWith elemDistance l r where
    elemDistance (These l r) | l == r = 0
    elemDistance _ = 1

一个方便的辅助函数将选择某个列表中的最小元素(通过用户指定的度量),并以某个实现定义的顺序返回列表的其余部分。

minRestOn :: Ord b => (a -> b) -> [a] -> Maybe (a, [a])
minRestOn f [] = Nothing
minRestOn f (x:xs) = Just (go x [] xs) where
    go min rest [] = (min, rest)
    go min rest (x:xs) = if f x < f min
                         then go x (min:rest) xs
                         else go min (x:rest) xs

现在贪婪的算法几乎写出来了:

greedy :: Eq a => [a] -> [[a]] -> [[a]]
greedy here neighbors = here : case minRestOn (distance here) neighbors of
    Nothing -> []
    Just (min, rest) -> greedy min rest

我们可以试试你的样品:

> greedy sampleStart sampleNeighbors
["2222","1222","2221","2234","5428","8888","8881","8822"]

只要注意它,这似乎没关系。但是,与许多贪婪算法一样,这个算法只能最小化路径中每条边的 local 成本。如果要最小化找到的路径的成本,则需要使用其他算法。例如,我们可以提取astar包。为简单起见,我将以非常低效的方式做所有事情,但要做到“正确”并不太难。我们需要更多的进口产品:

import Data.Graph.AStar
import Data.Hashable
import Data.List
import Data.Maybe
import qualified Data.HashSet as HS

与以前不同,我们只想要最近的邻居,我们现在想要所有的邻居。 (实际上,我们可以使用以下函数和minRestOn或者其他东西来实现minimumOn的先前使用。如果您感兴趣,可以尝试一下!)

neighbors :: (a, [a]) -> [(a, [a])]
neighbors (_, xs) = go [] xs where
    go ls [] = []
    go ls (r:rs) = (r, ls ++ rs) : go (r:ls) rs

我们现在可以使用适当的参数调用aStar搜索方法。我们将使用([a], [[a]]) - 表示当前的数字列表和我们可以选择的其余列表 - 作为我们的节点类型。然后,aStar的参数依次为:查找相邻节点的函数,计算相邻节点之间距离的函数,我们离开的距离的启发式(我们只会说1对于列表中的每个唯一元素),我们是否已到达目标节点,以及从中开始搜索的初始节点。我们将调用fromJust,但它应该没问题:所有节点至少有一条到目标节点的路径,只需按顺序选择剩余的数字列表。

optimal :: (Eq a, Ord a, Hashable a) => [a] -> [[a]] -> [[a]]
optimal here elsewhere = (here:) . map fst . fromJust $ aStar
    (HS.fromList . neighbors)
    (\(x, _) (y, _) -> distance x y)
    (\(x, xs) -> HS.size (HS.fromList (x:xs)) - 1)
    (\(_, xs) -> null xs)
    (here, elsewhere)

让我们看看它在ghci中运行:

> optimal sampleStart sampleNeighbors
["2222","1222","8822","8881","8888","5428","2221","2234"]

通过添加pathLength函数来计算结果中邻居之间的所有距离,我们可以看到这次做得更好。

pathLength :: Eq a => [[a]] -> Int
pathLength xs = sum [distance x y | x:y:_ <- tails xs]

在ghci:

> pathLength (greedy sampleStart sampleNeighbors)
15
> pathLength (optimal sampleStart sampleNeighbors)
14

在这个特定的例子中,我认为贪婪算法可以找到最佳路径,如果它有最小下一步的关系时做出“正确”的选择;但是我觉得制作贪婪算法被迫做出糟糕的早期选择的例子并不难。