考虑一个函数,它对两个数字之间的“视觉相似度”进行评级: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
可能会再次破坏这一点。如果有人知道这个问题是否存在共同的抽象,我会很想听到。
答案 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
在这个特定的例子中,我认为贪婪算法可以找到最佳路径,如果它有最小下一步的关系时做出“正确”的选择;但是我觉得制作贪婪算法被迫做出糟糕的早期选择的例子并不难。