327 lines
7.7 KiB
Go
327 lines
7.7 KiB
Go
// Copyright (c) 2024 Karl Gaissmaier
|
|
// SPDX-License-Identifier: MIT
|
|
|
|
// Package bitset implements bitsets, a mapping
|
|
// between non-negative integers and boolean values.
|
|
//
|
|
// Studied [github.com/bits-and-blooms/bitset] inside out
|
|
// and rewrote needed parts from scratch for this project.
|
|
//
|
|
// This implementation is smaller and faster as the more
|
|
// general [github.com/bits-and-blooms/bitset].
|
|
//
|
|
// All functions can be inlined!
|
|
//
|
|
// can inline BitSet.Set with cost 63
|
|
// can inline BitSet.Clear with cost 24
|
|
// can inline BitSet.Test with cost 26
|
|
// can inline BitSet.Rank0 with cost 66
|
|
// can inline BitSet.Clone with cost 7
|
|
// can inline BitSet.Compact with cost 35
|
|
// can inline BitSet.FirstSet with cost 25
|
|
// can inline BitSet.NextSet with cost 71
|
|
// can inline BitSet.AsSlice with cost 50
|
|
// can inline BitSet.All with cost 62
|
|
// can inline BitSet.IntersectsAny with cost 42
|
|
// can inline BitSet.IntersectionTop with cost 56
|
|
// can inline BitSet.IntersectionCardinality with cost 35
|
|
// can inline (*BitSet).InPlaceIntersection with cost 71
|
|
// can inline (*BitSet).InPlaceUnion with cost 77
|
|
// can inline BitSet.Size with cost 16
|
|
// can inline popcount with cost 12
|
|
// can inline popcountAnd with cost 30
|
|
package bitset
|
|
|
|
import (
|
|
"math/bits"
|
|
)
|
|
|
|
// A BitSet is a slice of words. This is an internal package
|
|
// with a wide open public API.
|
|
type BitSet []uint64
|
|
|
|
// xIdx calculates the index of i in a []uint64
|
|
// func wIdx(i uint) int {
|
|
// return int(i >> 6) // like (i / 64) but faster
|
|
// }
|
|
|
|
// bIdx calculates the index of i in a `uint64`
|
|
// func bIdx(i uint) uint {
|
|
// return i & 63 // like (i % 64) but faster
|
|
// }
|
|
//
|
|
// just as an explanation of the expressions,
|
|
//
|
|
// i>>6 or i<<6 and i&63
|
|
//
|
|
// not factored out as functions to make most of the methods
|
|
// inlineable with minimal costs.
|
|
|
|
// Set bit i to 1, the capacity of the bitset is increased accordingly.
|
|
func (b BitSet) Set(i uint) BitSet {
|
|
// grow?
|
|
if i >= uint(len(b)<<6) {
|
|
words := int((i + 64) >> 6)
|
|
switch {
|
|
case b == nil:
|
|
b = make([]uint64, words)
|
|
case cap(b) >= words:
|
|
b = b[:words]
|
|
default:
|
|
// be exact, don't use append!
|
|
// max 512 prefixes/node (8*uint64), and a cache line has 64 Bytes
|
|
newset := make([]uint64, words)
|
|
copy(newset, b)
|
|
b = newset
|
|
}
|
|
}
|
|
|
|
b[i>>6] |= 1 << (i & 63)
|
|
return b
|
|
}
|
|
|
|
// Clear bit i to 0.
|
|
func (b BitSet) Clear(i uint) BitSet {
|
|
if x := int(i >> 6); x < len(b) {
|
|
b[x] &^= 1 << (i & 63)
|
|
}
|
|
return b
|
|
}
|
|
|
|
// Test if bit i is set.
|
|
func (b BitSet) Test(i uint) (ok bool) {
|
|
if x := int(i >> 6); x < len(b) {
|
|
return b[x]&(1<<(i&63)) != 0
|
|
}
|
|
return
|
|
}
|
|
|
|
// Clone this BitSet, returning a new BitSet that has the same bits set.
|
|
func (b BitSet) Clone() BitSet {
|
|
return append(b[:0:0], b...)
|
|
}
|
|
|
|
// Compact, preserve all set bits, while minimizing memory usage.
|
|
func (b BitSet) Compact() BitSet {
|
|
last := len(b) - 1
|
|
|
|
// find last word with at least one bit set.
|
|
for ; last >= 0; last-- {
|
|
if b[last] != 0 {
|
|
b = b[: last+1 : last+1]
|
|
return b
|
|
}
|
|
}
|
|
|
|
// BitSet was empty, shrink to nil
|
|
return nil
|
|
}
|
|
|
|
// FirstSet returns the first bit set along with an ok code.
|
|
func (b BitSet) FirstSet() (uint, bool) {
|
|
for x, word := range b {
|
|
if word != 0 {
|
|
return uint(x<<6 + bits.TrailingZeros64(word)), true
|
|
}
|
|
}
|
|
return 0, false
|
|
}
|
|
|
|
// NextSet returns the next bit set from the specified index,
|
|
// including possibly the current index along with an ok code.
|
|
func (b BitSet) NextSet(i uint) (uint, bool) {
|
|
x := int(i >> 6)
|
|
if x >= len(b) {
|
|
return 0, false
|
|
}
|
|
|
|
// process the first (maybe partial) word
|
|
first := b[x] >> (i & 63) // i % 64
|
|
if first != 0 {
|
|
return i + uint(bits.TrailingZeros64(first)), true
|
|
}
|
|
|
|
// process the following words until next bit is set
|
|
// x < len(b), no out-of-bounds panic in following slice expression
|
|
x++
|
|
for j, word := range b[x:] {
|
|
if word != 0 {
|
|
return uint((x+j)<<6 + bits.TrailingZeros64(word)), true
|
|
}
|
|
}
|
|
return 0, false
|
|
}
|
|
|
|
// AsSlice returns all set bits as slice of uint without
|
|
// heap allocations.
|
|
//
|
|
// This is faster than All, but also more dangerous,
|
|
// it panics if the capacity of buf is < b.Size()
|
|
func (b BitSet) AsSlice(buf []uint) []uint {
|
|
buf = buf[:cap(buf)] // len = cap
|
|
|
|
size := 0
|
|
for idx, word := range b {
|
|
for ; word != 0; size++ {
|
|
// panics if capacity of buf is exceeded.
|
|
buf[size] = uint(idx<<6 + bits.TrailingZeros64(word))
|
|
|
|
// clear the rightmost set bit
|
|
word &= word - 1
|
|
}
|
|
}
|
|
|
|
buf = buf[:size]
|
|
return buf
|
|
}
|
|
|
|
// All returns all set bits. This is simpler but slower than AsSlice.
|
|
func (b BitSet) All() []uint {
|
|
buf := make([]uint, b.Size())
|
|
|
|
slot := 0
|
|
for idx, word := range b {
|
|
for word != 0 {
|
|
buf[slot] = uint(idx<<6 + bits.TrailingZeros64(word))
|
|
slot++
|
|
|
|
// clear the rightmost set bit
|
|
word &= word - 1
|
|
}
|
|
}
|
|
|
|
return buf
|
|
}
|
|
|
|
// IntersectsAny returns true if the intersection of base set with the compare set
|
|
// is not the empty set.
|
|
func (b BitSet) IntersectsAny(c BitSet) bool {
|
|
i := min(len(b), len(c)) - 1
|
|
// bounds check eliminated (BCE)
|
|
for ; i >= 0 && i < len(b) && i < len(c); i-- {
|
|
if b[i]&c[i] != 0 {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
// IntersectionTop computes the intersection of base set with the compare set.
|
|
// If the result set isn't empty, it returns the top most set bit and true.
|
|
func (b BitSet) IntersectionTop(c BitSet) (top uint, ok bool) {
|
|
i := min(len(b), len(c)) - 1
|
|
// bounds check eliminated (BCE)
|
|
for ; i >= 0 && i < len(b) && i < len(c); i-- {
|
|
if word := b[i] & c[i]; word != 0 {
|
|
return uint(i<<6+bits.Len64(word)) - 1, true
|
|
}
|
|
}
|
|
return
|
|
}
|
|
|
|
// IntersectionCardinality computes the popcount of the intersection.
|
|
func (b BitSet) IntersectionCardinality(c BitSet) int {
|
|
return popcntAnd(b, c)
|
|
}
|
|
|
|
// InPlaceIntersection overwrites and computes the intersection of
|
|
// base set with the compare set. This is the BitSet equivalent of & (and).
|
|
// If len(c) > len(b), new memory is allocated.
|
|
func (b *BitSet) InPlaceIntersection(c BitSet) {
|
|
// bounds check eliminated, range until minLen(b,c)
|
|
for i := 0; i < len(*b) && i < len(c); i++ {
|
|
(*b)[i] &= c[i]
|
|
}
|
|
|
|
// b >= c
|
|
if len(*b) >= len(c) {
|
|
// bounds check eliminated
|
|
for i := len(c); i < len(*b); i++ {
|
|
(*b)[i] = 0
|
|
}
|
|
return
|
|
}
|
|
|
|
// b < c
|
|
newset := make([]uint64, len(c))
|
|
copy(newset, *b)
|
|
*b = newset
|
|
}
|
|
|
|
// InPlaceUnion creates the destructive union of base set with compare set.
|
|
// This is the BitSet equivalent of | (or).
|
|
// If len(c) > len(b), new memory is allocated.
|
|
func (b *BitSet) InPlaceUnion(c BitSet) {
|
|
// b >= c
|
|
if len(*b) >= len(c) {
|
|
// bounds check eliminated
|
|
for i := 0; i < len(*b) && i < len(c); i++ {
|
|
(*b)[i] |= c[i]
|
|
}
|
|
|
|
return
|
|
}
|
|
|
|
// b < c
|
|
newset := make([]uint64, len(c))
|
|
copy(newset, *b)
|
|
*b = newset
|
|
|
|
// bounds check eliminated
|
|
for i := 0; i < len(*b) && i < len(c); i++ {
|
|
(*b)[i] |= c[i]
|
|
}
|
|
}
|
|
|
|
// Size (number of set bits).
|
|
func (b BitSet) Size() int {
|
|
return popcntSlice(b)
|
|
}
|
|
|
|
// Rank0 is equal to Rank(i) - 1
|
|
//
|
|
// With inlined popcount to make Rank0 itself inlineable.
|
|
func (b BitSet) Rank0(i uint) (rnk int) {
|
|
// Rank count is inclusive
|
|
i++
|
|
|
|
if wordIdx := int(i >> 6); wordIdx >= len(b) {
|
|
// inlined popcount, whole slice
|
|
for _, x := range b {
|
|
rnk += bits.OnesCount64(x)
|
|
}
|
|
} else {
|
|
// inlined popcount, partial slice ...
|
|
for _, x := range b[:wordIdx] {
|
|
rnk += bits.OnesCount64(x)
|
|
}
|
|
|
|
// ... plus partial word?
|
|
if bitsIdx := i & 63; bitsIdx != 0 {
|
|
rnk += bits.OnesCount64(b[wordIdx] << (64 - bitsIdx))
|
|
}
|
|
|
|
}
|
|
|
|
// correct for offset by one
|
|
return rnk - 1
|
|
}
|
|
|
|
// popcntSlice
|
|
func popcntSlice(s []uint64) (cnt int) {
|
|
for _, x := range s {
|
|
// count all the bits set in slice.
|
|
cnt += bits.OnesCount64(x)
|
|
}
|
|
return
|
|
}
|
|
|
|
// popcntAnd
|
|
func popcntAnd(s, m []uint64) (cnt int) {
|
|
for j := 0; j < len(s) && j < len(m); j++ {
|
|
// words are bitwise & followed by popcount.
|
|
cnt += bits.OnesCount64(s[j] & m[j])
|
|
}
|
|
return
|
|
}
|