Actually I don't think a hash-based approach lends itself well to this problem; it is better to approach it as a list processing problem. I'll describe an approach in Haskell (this could be translated to Perl with only slightly more code).
The general approach is to keep a running tally of how many elements are overlapping at any given time. To do this, we'll build a list of "deltas" - changes in the running total - from the argument list of elements as (start, end) pairs. Then sort these deltas by where they occur, and combine ones that occur at the same time. Finally, keep a running sum of the deltas, which is equal to the number of overlapping elements at the start of each period (each period ends where the next begins).
Complete code:
import Data.List -- sortBy
import Data.Ord -- comparing
import Data.Function -- `on`
withDelta delta x = (x, delta)
episodeToDelta elements = increases ++ decreases
where increases = map (withDelta 1 . fst) elements
decreases = map (withDelta (-1) . snd) elements
compactDelta::[(Integer,Integer)]->(Integer,Integer)
compactDelta (x:xs) = ((fst x),(sum $ map snd (x:xs)))
adjustTotal (t, delta) ((t1, total1):rest) = (t, total1 + delta):(t1,total1):rest
deltasToRunningTotal = reverse . foldr adjustTotal [(0,0)] . reverse
noZeros = filter (\(_, d) -> d /= 0)
elementsToRunningTotal = deltasToRunningTotal . noZeros . map compactDelta . groupBy ((==) `on` fst) . sortBy (comparing fst) . elementToDelta
sample_elements = [(10,15),(5,9),(13,22),(15,19),(14,16),(3,8),(2,12),(20,22),(23,29)]
> sortBy (comparing fst) sample_elements
[(2,12),(3,8),(5,9),(10,15),(13,22),(14,16),(15,19),(20,22),(23,29)]
> episodesToRunningTotal sample_elements
[(0,0),(2,1),(3,2),(5,3),(8,2),(9,1),(10,2),(12,1),(13,2),(14,3),(16,2),(19,1),(20,2),(22,0),(23,1),(29,0)]