Hình tam giác một danh sách trong Haskell


8

Tôi quan tâm đến việc viết một hàm Haskell hiệu quả triangularize :: [a] -> [[a]], lấy một danh sách (có lẽ là vô hạn) và "tam giác hóa" nó thành một danh sách các danh sách. Ví dụ, triangularize [1..19]nên trả lại

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

Theo hiệu quả, tôi có nghĩa là tôi muốn nó chạy trong O(n)thời gian mà nđộ dài của danh sách.


Lưu ý rằng điều này khá dễ thực hiện trong một ngôn ngữ như Python, bởi vì việc thêm vào cuối danh sách (mảng) là một hoạt động thời gian không đổi. Một hàm Python rất bắt buộc thực hiện điều này là:

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

Điều này xuất hiện bởi vì tôi đã sử dụng Haskell để viết một số chuỗi "tabl" trong Từ điển bách khoa toàn thư về chuỗi số nguyên (OEIS) và tôi muốn có thể chuyển đổi một chuỗi (1 chiều) thông thường thành một (2- chiều) thứ nguyên) trình tự các chuỗi theo cách chính xác.

Có lẽ có một số cách thông minh (hoặc không thông minh) để foldrvượt qua danh sách đầu vào, nhưng tôi không thể sắp xếp nó ra.


Điều này có trả lời câu hỏi của bạn không? Lấy tất cả các đường chéo của ma trận trong Haskell
MikaelF

1
@MikaelF Tôi không nghĩ vậy. Cụ thể, giả định rằng đối với đầu vào, bạn có một ma trận, không phải là danh sách (có khả năng vô hạn).
Joseph Sible-Phục hồi lại

@ JosephSible-RebstateMonica Tôi thấy, bạn nói đúng.
MikaelF

Thành ngữ hơn foldrbạn có thể thích unfoldr (Just . combWith comb)cho danh sách vô hạn. Than ôi như tôi đã đề cập dưới câu trả lời của tôi combWithlà O (n) do đó câu trả lời được chấp nhận sử dụng splitAtcó hiệu quả hơn đáng kể.
Redu

Câu trả lời:


13

Làm tăng kích thước khối:

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

Sau đó chỉ cần hoán chuyển hai lần:

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

Hãy thử nó trong ghci:

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

2
Hừm. Chà, điều đó xảy ra với tôi rằng tôi không tự tin lắm transposelà O (n). Tôi cũng không tự tin lắm phải không - việc thực hiện nó khá phức tạp!
Daniel Wagner

1
Bạn có nghĩ rằng một biến thể của điều này có thể làm việc trên danh sách vô hạn? Tôi thực sự tò mò.
MikaelF

1
@MikaelF Có vẻ đúng với tôi ...? take 3 . map (take 3) . diagonalize $ [1..]cho [[1,3,6],[2,5,9],[4,8,13]], mà có vẻ tốt.
Daniel Wagner

1
Đó là bởi vì danh sách đầu tiên trong danh sách là vô hạn. take 10 $ map (take 10) $ diagonalize [1..]thực sự đưa ra mười yếu tố đầu tiên trong mười hàng đầu tiên.
Peter Kagey

4
Giải pháp này thật tuyệt vời. Tôi đã xây dựng một giải pháp bằng cách sử dụng một bộ số nguyên lười biếng và nó mờ nhạt so với điều này, hiệu suất khôn ngoan. Các phép đo thực nghiệm chỉ ra rằng điều này cũng rất gần với thời gian tuyến tính. Tôi không hiểu làm thế nào ...
luqui

6

Điều này dường như có liên quan trực tiếp đến đối số lý thuyết tập hợp chứng minh rằng tập hợp các cặp số nguyên nằm trong sự tương ứng một-một với tập hợp các số nguyên (có thể đếm được ). Đối số liên quan đến cái gọi là hàm ghép Cantor .

Vì vậy, vì tò mò, hãy xem liệu chúng ta có thể có được một diagonalizechức năng theo cách đó không. Xác định danh sách vô hạn các cặp Cantor theo cách đệ quy trong 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)

Và thử điều đó bên trong 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)]
 λ> 

Chúng ta có thể đánh số các cặp và ví dụ trích xuất các số cho các cặp có tọa độ 0 x:

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

Chúng tôi nhận ra đây là hàng trên cùng từ kết quả của OP trong văn bản của câu hỏi. Tương tự cho hai hàng tiếp theo:

 λ> 
 λ> 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]
 λ> 

Từ đó, chúng ta có thể viết bản nháp đầu tiên của một diagonalizehàm:

 λ> 
 λ> 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]
 λ> 

EDIT: cập nhật hiệu suất

Đối với danh sách 1 triệu mục, thời gian chạy là 18 giây và 145 giây cho 4 triệu mục. Như Redu đã đề cập, điều này có vẻ như phức tạp O (n√n).

Phân phối các cặp trong số các danh sách mục tiêu khác nhau là không hiệu quả, vì hầu hết các hoạt động của bộ lọc đều thất bại.

Để cải thiện hiệu suất, chúng tôi có thể sử dụng cấu trúc Data.Map cho danh sách con mục tiêu.


{-#  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

Với phiên bản thứ hai đó, hiệu suất có vẻ tốt hơn nhiều: 568 msec cho danh sách 1 triệu mặt hàng, 2669 msec cho danh sách 4 triệu mặt hàng. Vì vậy, nó gần với độ phức tạp O (n * Log (n)) mà chúng ta có thể hy vọng.


3

Nó có thể là một ý tưởng tốt để vượt qua một combbộ lọc.

Vậy combbộ lọc làm gì ..? Nó giống như splitAtnhưng thay vì chia tách theo một chỉ mục duy nhất, nó sắp xếp danh sách vô hạn đã cho bằng lược đã cho để phân tách các mục được xác định TrueFalsetrong lược. Như vậy mà;

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])

Bây giờ tất cả những gì chúng ta cần làm là kết hợp danh sách vô hạn của chúng ta và lấy fsthàng đầu tiên và tiếp tục kết hợp sndvới cùng comb.

Hãy làm nó;

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]
]

cũng có vẻ lười biếng quá :)

λ> 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]
]

Tôi nghĩ rằng sự phức tạp có thể giống như O (n√n) nhưng tôi không thể chắc chắn. Có ý kiến ​​gì không ..?


giải pháp ngây thơ đầu tiên của tôi cũng có độ phức tạp O (n√n). Sử dụng cấu trúc Data.Map để phân phối kết quả vào danh sách mục tiêu của danh sách, có một sự cải tiến lớn. Chi tiết ở cuối câu trả lời của tôi.
jpmarinier

@jpmarinier Trong nhiều trường hợp, có thể khó có được số liệu hiệu suất có ý nghĩa do sự lười biếng nhưng chúng ta vẫn có thể cảm thấy chỉ bằng một chút :set +s. Làm như vậy câu trả lời được chấp nhận của @Daniel Wagner dường như đang chạy khá nhanh với loại danh sách. Bạn có thể vui lòng kiểm tra xem nó so sánh với bạn như thế nào không? Tôi đã hy vọng đạt được hiệu suất tương tự nhưng combWithkhông nơi nào nhanh bằng spilitAt.
Redu

1
Tôi hơi nghi ngờ về việc sử dụng ghci để đo hiệu suất, vì vậy tôi sử dụng ghc -O2. Đối với sự lười biếng, tôi in đánh giá (tổng $ chiều dài bản đồ (đầu vào chéo)), điều này giúp tôi lấy lại độ dài của danh sách đầu vào. Giải pháp của @Daniel Wagner chạy nhanh hơn khoảng 20% ​​so với giải pháp bản đồ Cantor, do đó, nó chắc chắn nằm trong trại O (n * log (n)). Vì vậy, những cảm nhận của Daniel về sự phi tuyến tính transposedường như không có cơ sở. Trên hết, nó có vẻ lười biếng thân thiện hơn bản đồ Cantor. Làm tốt !
jpmarinier

@jpmarinier Kiểm tra câu trả lời này của @Daniel Wagner nó có vẻ như sndtrong những splitAt's giá trị trả về được thu được trong thời gian O (1) nhưng fstvẫn là nên O (n). Bằng cách nào đó, điều này phản ánh xuống hiệu suất tổng thể là O (nlogn).
Redu

Vâng, chỉ cần nhìn vào định nghĩa đệ quy cho splitAt , có vẻ như phần (drop n xs) về cơ bản thu được miễn phí như là một tác dụng phụ của việc nhận (lấy n xs). Vì vậy, Daniel có quyền sử dụng splitAtthay vì gọi droptakeriêng biệt.
jpmarinier
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.