時間帯重複チェック(応用編)

お題:時間帯重複チェック(応用編) - No Programming, No Life を解く。*1


調べてみたら 区間木 - Wikipedia というものがあるみたいだが、手頃な感じではないので Groovy で力づくな方法で解くことにする。

class Time implements Comparable<Time> {
  int h, m

  Time(int h, int m) {
    this.h = h
    this.m = m
  }

  int compareTo(Time t) {
    h <=> t.h ?: m <=> t.m
  }
}

class Period {
  Time s, e

  Period(Time s, Time e) {
    this.s = s
    this.e = e
  }

  def and(Period p) {
    s < p.e && e > p.s ? new Period([s, p.s].max(), [e, p.e].min()) : null
  }

  def or(Period p) {
    s <= p.e && e >= p.s ? [new Period([s, p.s].min(), [e, p.e].max())] : [this, p]
  }

  @Override String toString() {
    "($s.h, $s.m, $e.h, $e.m)"
  }
}

def period(int h1, int m1, int h2, int m2) {
  def t1 = new Time(h1, m1)
  def t2 = new Time(h2, m2)
  assert t1 <= t2
  new Period(t1, t2)
}

def combs(list, n) {
  if (n == 0 || n > list.size()) return []
  if (n == 1) return list.collect { [it] }
  def (x, xs) = [list.head(), list.tail()]
  combs(xs, n-1).collect { [x, *it] } + combs(xs, n)
}

def overlaps(input) {
  def periods = input.collect { period(it) }
  def results = combs(periods, 2).collect { p1, p2 ->
    // 全組合せの重複期間を取得する
    p1 & p2
  }.findAll {
    it != null
  }.sort { p1, p2 ->
    // 期間開始の昇順にソートする
    p1.s <=> p2.s
  }.inject([]) { acc, p ->
    // 重複または連続している重複期間をマージする
    if (acc.empty) acc += p
    else           acc += acc.pop() | p
  }
  println "入力:${periods.join(', ')}"
  println "出力:${results.join(', ') ?: 'なし'}"
}

println "例1)"
overlaps([[12, 0, 13, 0], [10, 0, 12, 15]])

println "例2)"
overlaps([[16, 0, 23, 0], [9, 0, 17, 0], [5, 0, 10, 30]])

println "例3)"
overlaps([[12, 0, 23, 0], [13, 0, 14, 0], [15, 0, 16, 0], [17, 0, 18, 0], [19, 0, 20, 0], [21, 0, 22, 0]])

println "例4)"
overlaps([[10, 0, 12, 0], [11, 0, 11, 30], [10, 30, 11, 15]])

println "例5)"
overlaps([[9, 0, 17, 0], [19, 0, 21, 0]])


最近、Haskell の本を見ながら Groovy で書いているのでその逆をやってみる。
Groovy のコードをそのまま Haskell に置き換える。*2

import Data.List

type Period = (Int, Int, Int, Int)
type Time   = (Int, Int)

combinations :: Int -> [a] -> [[a]]
combinations 0 _      = [[]]
combinations _ []     = []
combinations n (x:xs) = (map (x:) (combinations (n-1) xs)) ++ (combinations n xs)

period :: Time -> Time -> Period
period (h1, m1) (h2, m2) = (h1, m1, h2, m2)

(-&-) :: Period -> Period -> Maybe Period
(a1, b1, c1, d1) -&- (a2, b2, c2, d2)
  | (a1, b1) < (c2, d2) && (c1, d1) > (a2, b2) = Just (period (max (a1, b1) (a2, b2)) (min (c1, d1) (c2, d2)))
  | otherwise                                  = Nothing

(-|-) :: Period -> Period -> [Period]
(a1, b1, c1, d1) -|- (a2, b2, c2, d2)
  | (a1, b1) <= (c2, d2) && (c1, d1) >= (a2, b2) = [period (min (a1, b1) (a2, b2)) (max (c1, d1) (c2, d2))]
p1 -|- p2 = [p1, p2]

overlapsPeriod :: [Period] -> Maybe Period
overlapsPeriod (p1:p2:_) = p1 -&- p2

startAsc :: Maybe Period -> Maybe Period -> Ordering
startAsc (Just (a1, b1, c1, d1)) (Just (a2, b2, c2, d2)) = compare (a1, b1) (a2, b2)

overlapsOrFollowedBy :: [Period] -> Maybe Period -> [Period]
overlapsOrFollowedBy [] (Just p) = [p]
overlapsOrFollowedBy (p1:ps) (Just p2) = (p2 -|- p1) ++ ps

onlyOverlaps :: Maybe Period -> Bool
onlyOverlaps Nothing = False
onlyOverlaps _       = True

overlaps = reverse . foldl overlapsOrFollowedBy ([]) . sortBy startAsc . filter onlyOverlaps . map overlapsPeriod . combinations 2

ex1 :: [Period]
ex1 = [(12, 0, 13, 0), (10, 0, 12, 15)]
ex2 :: [Period]
ex2 = [(16, 0, 23, 0), (9, 0, 17, 0), (5, 0, 10, 30)]
ex3 :: [Period]
ex3 = [(12, 0, 23, 0), (13, 0, 14, 0), (15, 0, 16, 0), (17, 0, 18, 0), (19, 0, 20, 0), (21, 0, 22, 0)]
ex4 :: [Period]
ex4 = [(10, 0, 12, 0), (11, 0, 11, 30), (10, 30, 11, 15)]
ex5 :: [Period]
ex5 = [(9, 0, 17, 0), (19, 0, 21, 0)]


実行形式では書けなかったが ghci で動作確認するところまで辿り着けた。

*Main> overlaps ex1
[(12,0,12,15)]
*Main> overlaps ex2
[(9,0,10,30),(16,0,17,0)]
*Main> overlaps ex3
[(13,0,14,0),(15,0,16,0),(17,0,18,0),(19,0,20,0),(21,0,22,0)]
*Main> overlaps ex4
[(10,30,11,30)]
*Main> overlaps ex5
[]

*1:フラグで解決する話は機転が利いている

*2:combinations は H99 から