5
\$\begingroup\$

The code below finds the index of the lowest unique integer on each line of the file. The numbering starts at 1, and when there's no unique value, the 0 should be output.

The sample input looks like this:

3 3 9 1 6 5 8 1 5 3
9 2 9 9 1 8 8 8 2 1 1

And the sample output is this:

5
0

I wonder if this code can be simplified or made more elegant and idiomatic in any way. It's also interesting to me, could I get by w/o the IntMap and solve the problem with i.e. lists beautifully.

module Main where

import Prelude hiding (filter)
import Control.Applicative ((<$>))
import Data.IntMap (IntMap, alter, empty, filter, findMin, fromList, size)

import System.Environment

counts :: [(Int, Int)] -> IntMap [Int]
counts = foldl f empty
  where f m (i,v) = alter f' v m
          where f' :: Maybe [Int] -> Maybe [Int]
                f' (Just is) = Just (i:is)
                f' (Nothing) = Just [i]

g :: IntMap [Int] -> Int
g a = i
  where rs = filter ((== 1) . length) a
        h m | size m == 0 = fromList [(0, [0])]
            | otherwise = m
        (_, [i]) = findMin $ h rs

minUnique :: [Int] -> Int
minUnique l = g r
  where r = counts (zip [1..] l)

main =
  head <$> getArgs >>= readFile >>=
    putStrLn . unlines . map (show . minUnique . map read . words) . lines
\$\endgroup\$
0

1 Answer 1

5
\$\begingroup\$

First off, your usage of alter can easily be replaced by insertWith:

counts :: [(Int, Int)] -> IntMap [Int]
counts = foldr f empty
  where f (i,v) = insertWith (const (i:)) v [i]

You could also use fromListWith, which is neater but less efficient due to the need to concatenate lists:

counts :: [(Int, Int)] -> IntMap [Int]
counts = fromListWith (flip (++)) . map (\(i,v) -> (v,[i]))

Second, you could shorten g quite a bit as well using a view:

g a = case minView $ filter ((== 1) . length) a of
        Nothing      -> 0
        Just ([i],_) -> i

Which is also probably slightly less efficient, as it will construct at least a thunk for the rest of the IntMap.

Concerning different approaches - you could make a recursive function that removes all duplicates:

import Data.List
import Data.Ord

dedup :: [(Int,Int)] -> [(Int,Int)]
dedup [] = []
dedup ((i,v):ps)
  | null dups = (i,v):dedup ps
  | otherwise = dedup ndups
  where (dups, ndups) = partition ((==v) . snd) ps

minUnique :: [Int] -> Int
minUnique l
  | null ddups = 0
  | otherwise  = fst $ minimumBy (comparing snd) $ ddups
  where ddups = dedup $ zip [1..] l

Once more this trades for a bit of efficiency, as the traversal in partition makes it O(n²). Not sure whether that can be helped though without involving more complex data structures.

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.