Triangularisieren einer Liste in Haskell


8

Ich bin daran interessiert, eine effiziente Haskell-Funktion zu schreiben triangularize :: [a] -> [[a]], die eine (möglicherweise unendliche) Liste in eine Liste von Listen "trianguliert". Zum Beispiel triangularize [1..19]sollte zurückkehren

[[1,  3,  6,  10, 15]
,[2,  5,  9,  14]
,[4,  8,  13, 19]
,[7,  12, 18]
,[11, 17]
,[16]]

Mit effizient meine ich, dass ich möchte, dass es O(n)rechtzeitig ausgeführt wird, wo ndie Länge der Liste ist.


Beachten Sie, dass dies in einer Sprache wie Python recht einfach ist, da das Anhängen an das Ende einer Liste (eines Arrays) eine konstante Zeitoperation ist. Eine sehr wichtige Python-Funktion, die dies erreicht, ist:

def triangularize(elements):
    row_index = 0
    column_index = 0
    diagonal_array = []
    for a in elements:
        if row_index == len(diagonal_array):
            diagonal_array.append([a])
        else:
            diagonal_array[row_index].append(a)
        if row_index == 0:
            (row_index, column_index) = (column_index + 1, 0)
        else:
            row_index -= 1
            column_index += 1
    return diagonal_array

Dies ist darauf zurückzuführen, dass ich Haskell verwendet habe, um einige "tabl" -Sequenzen in der Online-Enzyklopädie der ganzzahligen Sequenzen (OEIS) zu schreiben , und ich möchte in der Lage sein, eine gewöhnliche (eindimensionale) Sequenz in eine (2-) Sequenz umzuwandeln dimensionale) Sequenz von Sequenzen auf genau diese Weise.

Vielleicht gibt es eine clevere (oder nicht so clevere) Möglichkeit, foldrdie Eingabeliste zu überschreiten, aber ich konnte sie nicht klären.



1
@MikaelF Das glaube ich nicht. Dies setzt insbesondere voraus, dass Sie für die Eingabe eine Matrix haben, keine (möglicherweise unendliche) Liste.
Joseph Sible-Reinstate Monica

@ JosephSible-ReinstateMonica Ich verstehe, du hast recht.
MikaelF

Idiomatischer, als foldrSie es unfoldr (Just . combWith comb)für unendliche Listen mögen . Leider ist, wie ich unter meiner Antwort erwähnt habe, combWithO (n), daher ist die akzeptierte Antwortverwendung splitAtwesentlich effizienter.
Reduzieren Sie den

Antworten:


13

Machen Sie immer größere Stücke:

chunks :: [a] -> [[a]]
chunks = go 0 where
    go n [] = []
    go n as = b : go (n+1) e where (b,e) = splitAt n as

Dann transponieren Sie einfach zweimal:

diagonalize :: [a] -> [[a]]
diagonalize = transpose . transpose . chunks

Probieren Sie es in ghci:

> diagonalize [1..19]
[[1,3,6,10,15],[2,5,9,14],[4,8,13,19],[7,12,18],[11,17],[16]]

2
Hm. Nun, mir fällt ein, dass ich nicht sehr zuversichtlich bin, transposeO (n) zu sein. Ich bin auch nicht sehr zuversichtlich, dass dies nicht der Fall ist - die Implementierung ist etwas kompliziert!
Daniel Wagner

1
Denken Sie, dass eine Variante davon auf unendlichen Listen funktionieren könnte? Ich bin wirklich neugierig.
MikaelF

1
@MikaelF Sieht für mich richtig aus ...? take 3 . map (take 3) . diagonalize $ [1..]gibt [[1,3,6],[2,5,9],[4,8,13]], was in Ordnung scheint.
Daniel Wagner

1
Das liegt daran, dass die erste Liste in der Liste selbst unendlich ist. take 10 $ map (take 10) $ diagonalize [1..]in der Tat gibt die ersten zehn Elemente der ersten zehn Zeilen.
Peter Kagey

4
Diese Lösung ist fantastisch. Ich habe eine Lösung mit einem faulen Versuch von ganzen Zahlen erstellt, die im Vergleich dazu in Bezug auf die Leistung verblasst. Empirische Messungen zeigen, dass dies auch sehr nahe an der linearen Zeit liegt. Ich verstehe nicht wie ...
luqui

6

Dies scheint in direktem Zusammenhang mit dem Argument der Mengenlehre zu stehen, das beweist, dass die Menge der Ganzzahlpaare eins zu eins mit der Menge der Ganzzahlen ( denumerierbar ) übereinstimmt . Das Argument beinhaltet eine sogenannte Cantor-Pairing-Funktion .

Lassen Sie uns aus Neugier sehen, ob wir auf diese Weise eine diagonalizeFunktion erhalten können. Definieren Sie die unendliche Liste der Cantor-Paare rekursiv in Haskell:

auxCantorPairList :: (Integer, Integer) -> [(Integer, Integer)]
auxCantorPairList (x,y) =
    let nextPair = if (x > 0) then (x-1,y+1) else (x+y+1, 0)
    in (x,y) : auxCantorPairList nextPair

cantorPairList :: [(Integer, Integer)]
cantorPairList = auxCantorPairList (0,0)

Und versuchen Sie das in ghci:

 λ> take 15 cantorPairList
[(0,0),(1,0),(0,1),(2,0),(1,1),(0,2),(3,0),(2,1),(1,2),(0,3),(4,0),(3,1),(2,2),(1,3),(0,4)]
 λ> 

Wir können die Paare nummerieren und zum Beispiel die Zahlen für diejenigen Paare extrahieren, die eine Null-x-Koordinate haben:

 λ> 
 λ> xs = [1..]
 λ> take 5 $ map fst $ filter (\(n,(x,y)) -> (x==0)) $ zip xs cantorPairList
[1,3,6,10,15]
 λ> 

Wir erkennen, dass dies die oberste Zeile aus dem Ergebnis des OP im Text der Frage ist. Ähnliches gilt für die nächsten beiden Zeilen:

 λ> 
 λ> makeRow xs row = map fst $ filter (\(n,(x,y)) -> (x==row)) $ zip xs cantorPairList
 λ> take 5 $ makeRow xs 1
[2,5,9,14,20]
 λ> 
 λ> take 5 $ makeRow xs 2
[4,8,13,19,26]
 λ> 

Von dort aus können wir unseren ersten Entwurf einer diagonalizeFunktion schreiben :

 λ> 
 λ> printAsLines xs = mapM_ (putStrLn . show) xs
 λ> diagonalize xs = takeWhile (not . null) $ map (makeRow xs) [0..]
 λ> 
 λ> printAsLines $ diagonalize [1..19]
[1,3,6,10,15]
[2,5,9,14]
[4,8,13,19]
[7,12,18]
[11,17]
[16]
 λ> 

BEARBEITEN: Leistungsaktualisierung

Bei einer Liste mit 1 Million Elementen beträgt die Laufzeit 18 Sekunden und bei 4 Millionen Elementen 145 Sekunden. Wie von Redu erwähnt, scheint dies eine Komplexität von O (n√n) zu sein.

Das Verteilen der Paare auf die verschiedenen Zielunterlisten ist ineffizient, da die meisten Filtervorgänge fehlschlagen.

Um die Leistung zu verbessern, können wir eine Data.Map-Struktur für die Zielunterlisten verwenden.


{-#  LANGUAGE  ExplicitForAll       #-}
{-#  LANGUAGE  ScopedTypeVariables  #-}

import qualified  Data.List  as  L
import qualified  Data.Map   as  M

type MIL a = M.Map Integer [a]

buildCantorMap :: forall a.  [a] -> MIL a
buildCantorMap xs = 
    let   ts     =  zip xs cantorPairList -- triplets (a,(x,y))
          m0     = (M.fromList [])::MIL a
          redOp m (n,(x,y)) = let  afn as = case as of
                                              Nothing  -> Just [n]
                                              Just jas -> Just (n:jas)
                              in   M.alter afn x m
          m1r = L.foldl' redOp m0 ts
    in
          fmap reverse m1r

diagonalize :: [a] -> [[a]]
diagonalize xs = let  cm = buildCantorMap xs
                 in   map snd $ M.toAscList cm


Mit dieser zweiten Version scheint die Leistung viel besser zu sein: 568 ms für die Liste mit 1 Million Artikeln, 2669 ms für die Liste mit 4 Millionen Artikeln. Es liegt also nahe an der O (n * Log (n)) -Komplexität, auf die wir gehofft haben könnten.


3

Es könnte eine gute Idee sein, einen combFilter zu knacken .

Was macht combFilter also? Es ist wie , splitAtaber statt Spaltung an einem einzigen indizieren Art von Reißverschluss die gegebene unendliche Liste mit dem angegebenen Kamm der Elemente coressponding zu trennen Trueund Falseauf dem Kamm. So dass;

comb :: [Bool]  -- yields [True,False,True,False,False,True,False,False,False,True...]
comb = iterate (False:) [True] >>= id

combWith :: [Bool] -> [a] -> ([a],[a])
combWith _ []          = ([],[])
combWith (c:cs) (x:xs) = let (f,s) = combWith cs xs
                         in if c then (x:f,s) else (f,x:s)

λ> combWith comb [1..19]
([1,3,6,10,15],[2,4,5,7,8,9,11,12,13,14,16,17,18,19])

Jetzt müssen wir nur noch unsere unendliche Liste kämmen und fstdie erste Zeile nehmen und die sndmit derselben weiter kämmen comb.

Machen wir das;

diags :: [a] -> [[a]]
diags [] = []
diags xs = let (h,t) = combWith comb xs
           in h : diags t

λ> diags [1..19]
[ [1,3,6,10,15]
, [2,5,9,14]
, [4,8,13,19]
, [7,12,18]
, [11,17]
, [16]
]

scheint auch faul zu sein :)

λ> take 5 . map (take 5) $ diags [1..]
[ [1,3,6,10,15]
, [2,5,9,14,20]
, [4,8,13,19,26]
, [7,12,18,25,33]
, [11,17,24,32,41]
]

Ich denke, die Komplexität könnte wie O (n√n) sein, aber ich kann nicht sicher sein. Irgendwelche Ideen..?


Meine erste naive Lösung hatte auch O (n√n) Komplexität. Die Verwendung einer Data.Map-Struktur zum Verteilen der Ergebnisse auf die Zielliste von Listen bietet eine große Verbesserung. Details am Ende meiner Antwort.
jpmarinier

@jpmarinier In vielen Fällen kann es aufgrund der Faulheit schwierig sein, aussagekräftige Leistungsmetriken zu erhalten, aber wir können immer noch ein Gefühl dafür bekommen :set +s. @Daniel Wagners akzeptierte Antwort scheint mit dem Listentyp ziemlich schnell zu laufen. Könnten Sie bitte überprüfen, wie es mit Ihrem verglichen wird? Ich hatte gehofft, eine ähnliche Leistung zu erzielen, aber das combWithist nirgends so schnell wie spilitAt.
Redu

1
Ich bin etwas skeptisch gegenüber der Verwendung von ghci für Leistungsmessungen, daher verwende ich ghc -O2. Was die Faulheit betrifft, drucke ich die Auswertung von (sum $ map length (diagonalize input)) aus, wodurch ich die Länge der Eingabeliste zurückerhalte. Die Lösung von @Daniel Wagner läuft etwa 20% schneller als die Cantor-Kartenlösung, befindet sich also definitiv im O (n * log (n)) - Lager. Daniels Bedenken hinsichtlich der Nichtlinearität von transposescheinen unbegründet zu sein. Darüber hinaus wirkt es fauler als die Cantor-Karte. Gut gemacht !
jpmarinier

@jpmarinier Wenn Sie diese Antwort von @Daniel Wagner überprüfen , scheint es, als würde der Rückgabewert sndvon splitAt'in O (1) erhalten, aber der Wert fstsollte immer noch O (n) sein. Irgendwie spiegelt sich dies in der Gesamtleistung als O (nlogn) wider.
Reduzieren Sie den

Ja, nachdem wir uns gerade die rekursive Definition für splitAt angesehen haben , scheint es, dass der Teil (drop n xs) als Nebeneffekt des Erhaltens (take n xs) im Wesentlichen kostenlos erhalten wird. Daniel hat also Recht, splitAtanstatt anzurufen dropund takeseparat zu verwenden.
jpmarinier
Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.