Хаскелл. Самый быстрый способ найти похожие элементы в двух Data.Map

Я пытаюсь сравнить элементы двух карт с помощью функции сравнения. Результирующее решение должно вернуть список пар ключ-значение из первой карты, которые похожи на некоторые значения из второй (сравниваются только значения). Ввод выглядит следующим образом:

let a = fromList [(0,15),(1,150),(2,39),(3,18)]
let b = fromList [(0,151),(1,39),(2,0),(3,1)]

-- Function that performs comparison. Returns True if items are similar
isSimilar :: Int -> Int -> Bool
isSimilar a b = abs (b - a) <= 1 

Результат должен быть следующим:

result = [(1,150),(2,39)]

Я закончил со следующей реализацией:

import qualified Data.IntMap.Strict as M
import Test.Hspec

type Elem = M.IntMap Int


-- Function that does all the work
getSimilarities :: Elem -> Elem -> [(Int,Int)]
getSimilarities cur intersected = M.foldrWithKey (\k p lst -> if M.null (check p)
                                               then lst
                                               else (k, p) : lst) [] cur
    where check pnt = M.filter (isSimilar pnt) intersected

-- Comparator that helps to filter out unnecessary values
isSimilar :: Int -> Int -> Bool
isSimilar a b = abs (b - a) <= 1 

-- Main function that runs the test
main :: IO ()
main = hspec spec

spec :: Spec
spec = do
        it "Get similarities test" $ do
            let xs        = [3,31,0,151,25,120]
            let mp        = M.fromList $ zip [0..(length xs)] xs

            let xs'        = [5,17,32,150,9,90]
            let mp'        = M.fromList $ zip [0..(length xs')] xs'

            let expected   = [(1, 31),(3, 151)]
            getSimilarities mp mp' `shouldBe` expected

Однако мне не очень нравится функция «проверить», так как мне не нужно формировать там новую карту, мне нужно просто проверить значение. Не могли бы вы предложить, есть ли более эффективный и производительный способ сделать то, что мне нужно? Спасибо.


person Sergii Sopin    schedule 14.01.2020    source источник
comment
Я не могу придумать способ более эффективно сравнивать значения, используя эту структуру данных. Для ключей у нас есть поиск по журналу (см., например, lookupGE, который можно использовать для поиска похожих ключей), но для значений у нас ничего нет. Возможно, вам следует построить набор значений второй карты (стоимость: N*log N), а затем использовать этот набор для выполнения множества поисков (N поисков, журнал N каждый). Это может быть лучше, чем подход N^2, которому вы сейчас следуете.   -  person chi    schedule 14.01.2020
comment
Я относительно новичок в Haskell, поэтому решил использовать карты в качестве контейнера, потому что на других языках это довольно быстро. Может быть, вы могли бы порекомендовать мне, что лучше, если мне нужно хранить какую-то пару ключ-значение? Кроме того, с наборами я не вижу функции поиска, которая принимает предикат в качестве параметра. Не могли бы вы уточнить, что именно я должен использовать?   -  person Sergii Sopin    schedule 14.01.2020
comment
Наивное решение состоит в том, чтобы вычислить векторное произведение ваших двух списков и отфильтровать его, используя вашу функцию isSimilar. Это занимает O(n^2) времени. Если вы используете контейнеры ordered и можете предположить, что пары кандидатов имеют форму (x+/-d, x+/-d) для некоторой константы d (в вашем случае здесь d == 1), вы можете построить список кандидатов только за O(n lg n) время. (Я думаю, что во втором чтении это то, что предлагает @chi.)   -  person chepner    schedule 14.01.2020
comment
Haskell здесь не исключение — его карты примерно такие же, как в других языках. Имея ключ, вы можете быстро найти значение. Но, если у вас есть только значение, вам нужно отсканировать всю карту. Нет функции поиска с общим предикатом, так как она должна быть медленной. Единственный быстрый поиск — это ключ->значение. Вы можете искать, какой следующий ключ, начиная с X? используя lookupGE и друзей, быстро.   -  person chi    schedule 14.01.2020
comment
AFAICS, вы на самом деле не используете вторую карту как карту, вас интересуют только ее значения. Итак, начните брать все значения и помещайте их в IntSet (это нужно сделать только один раз). Затем используйте IntSet.lookupGE для выполнения check. Я не могу гарантировать, что это будет быстрее, но может быть, особенно для больших карт/наборов.   -  person chi    schedule 14.01.2020
comment
Вы поменяли ключи и значения!   -  person Daniel Wagner    schedule 14.01.2020
comment
Кроме того, есть забавный пограничный случай, над которым стоит подумать: isSimilar (-9223372036854775388) 420 оценивается как True на 64-битных машинах (и isSimilar (-2147483228) 420 на 32-битных машинах). Мой ответ предполагает, что это просто ошибка, и я не буду отмечать их как похожие; если это действительно предполагаемое поведение, все становится намного сложнее.   -  person Daniel Wagner    schedule 14.01.2020


Ответы (1)


Если вы хотите работать с любой функцией isSimilar, вы не сможете сделать асимптотически лучше, чем ваше предложение. Но для конкретных isSimilar функций часто можно сделать лучше. В конкретном случае

isSimilar a b = abs (b - a) <= 1

вы можете построить набор всех значений рядом со значениями вашей второй коллекции и установить пересечение, например:

import Data.Semigroup
import Data.Set (Set)
import qualified Data.Set as S

getSimilarities :: Set (Arg Int Int) -> [Int] -> Set (Arg Int Int)
getSimilarities cur intersected = S.intersection cur . S.fromList $
    [ Arg (i+di) (error "The impossible happened: ignored Arg values were inspected in getSimilarities")
    | i <- intersected
    , di <- [-1, 0, 1]
    ]

Смотрите, как это происходит в ghci:

> xs = [3,31,0,151,25,120]
> mp = S.fromList $ zipWith Arg xs [0..]
> xs' = [5,17,32,150,9,90]
> getSimilarities mp xs'
fromList [Arg 31 1,Arg 151 3]

Я оставляю решение о том, действительно ли вы хотите использовать карты и кортежи вместо наборов и Arg, а также функций для преобразования между ними, читателю.

person Daniel Wagner    schedule 14.01.2020
comment
Спасибо за идею! Я немного изменил реализацию, чтобы иметь кортеж (Int,Int) в качестве значения и определение isSimilar для isSimilar (a1,b1) (a2,b2) = let f = abs (a2-a1) f2 = abs (b2 - b1 ) в f ‹= 1 && f2 ‹= 1. Но тоже должно работать. - person Sergii Sopin; 14.01.2020