package fluvial // A monotone bucket priority queue, which is what priority-flood actually needs. // // The flood pops cells in non-decreasing elevation and never pushes anything below the cell it just popped: // a neighbour lower than the current front is raised to it and goes to the FIFO instead. That "monotone" // property is exactly the condition under which a bucket queue beats a binary heap, because the read cursor // only ever moves forward and both operations become an append and a scan. The heap was costing about // log2(3.2M) = 22 comparisons and as many cache misses per operation, on two thirds of the solve's runtime. // // Elevations are quantised into fixed-width buckets. Cells inside one bucket pop in an arbitrary but // deterministic order (last in, first out), so a spill point can be wrong by at most one bucket width. At a // centimetre against a 2 km elevation range that is far below the millimetre-per-cell epsilon the flood adds // anyway, and it is the same approximation an integer-elevation priority-flood makes by construction. type bucketPQ struct { lo float64 width float64 buckets [][]int32 cur int count int } const bucketWidthM = 0.01 func newBucketPQ(loM, hiM float64) *bucketPQ { if hiM <= loM { hiM = loM + 1 } // Headroom above the top: the flood raises cells by epsilon as it fills, so the highest key pushed can // sit slightly above the terrain's own maximum. n := int((hiM-loM)/bucketWidthM) + 64 return &bucketPQ{lo: loM, width: bucketWidthM, buckets: make([][]int32, n)} } func (q *bucketPQ) reset() { for i := range q.buckets { q.buckets[i] = q.buckets[i][:0] } q.cur = 0 q.count = 0 } func (q *bucketPQ) len() int { return q.count } func (q *bucketPQ) push(elev float32, idx int32) { b := int((float64(elev) - q.lo) / q.width) if b < q.cur { b = q.cur // monotone: never behind the cursor, whatever rounding says } if b >= len(q.buckets) { b = len(q.buckets) - 1 } q.buckets[b] = append(q.buckets[b], idx) q.count++ } // pop returns the lowest cell. The cursor only moves forward, so the total scan cost over a whole flood is // the number of buckets, not the number of pops. func (q *bucketPQ) pop() int32 { for q.cur < len(q.buckets) && len(q.buckets[q.cur]) == 0 { q.cur++ } if q.cur >= len(q.buckets) { return -1 } b := q.buckets[q.cur] v := b[len(b)-1] q.buckets[q.cur] = b[:len(b)-1] q.count-- return v } // frontElev is the elevation the cursor is at, which the FIFO compares itself against. func (q *bucketPQ) frontElev() float32 { for q.cur < len(q.buckets) && len(q.buckets[q.cur]) == 0 { q.cur++ } return float32(q.lo + float64(q.cur)*q.width) }