<em>Mac</em>Book项目 2009年学校开始实施<em>Mac</em>Book项目,所有师生配备一本<em>Mac</em>Book,并同步更新了校园无线网络。学校每周进行电脑技术更新,每月发送技术支持资料,极大改变了教学及学习方式。因此2011
2021-06-01 09:32:01
我們都知道計算機是基於二進位制的,位運算是計算機的基礎運算。位運算的優勢很明顯,CPU 指令原生支援、速度快。基於位運算的位集合在有限的場景中替換集合資料結構可以收到意想不到的效果。bitset庫實現了位集合及相關操作,不妨拿來即用。
本文程式碼使用 Go Modules。
建立目錄並初始化:
$ mkdir -p bitset && cd bitset $ go mod init github.com/darjun/go-daily-lib/bitset
安裝bitset
庫:
$ go get -u github.com/bits-and-blooms/bitset
位集合的基本操作有:
位集合一般用於小數值的非負整數的場景中。就拿遊戲中簡單的簽到舉例吧,很多遊戲都有簽到活動,短的有 7 天的,長的有 30 天。這種就很適合使用位集合。每個位的值表示其索引位置對應的那天有沒有簽到。
type Player struct { sign *bitset.BitSet } func NewPlayer(sign uint) *Player { return &Player{ sign: bitset.From([]uint64{uint64(sign)}), } } func (this *Player) Sign(day uint) { this.sign.Set(day) } func (this *Player) IsSigned(day uint) bool { return this.sign.Test(day) } func main() { player := NewPlayer(1) // 第一天簽到 for day := uint(2); day <= 7; day++ { if rand.Intn(100)&1 == 0 { player.Sign(day - 1) } } for day := uint(1); day <= 7; day++ { if player.IsSigned(day - 1) { fmt.Printf("day:%d signedn", day) } } }
bitset 提供了多種建立 BitSet 物件的方法。
首先 bitset.BitSet 零值可用,如果一開始不知道有多少元素,可以使用這種方式建立:
var b bitset.BitSet
BitSet 在設定時自動調整大小。如果事先知道長度,建立 BitSet 時可傳入此值,能有效避免自動調整的開銷:
b := bitset.New(100)
bitset 結構支援鏈式呼叫,大部分方法返回自身的指標,所以可以這樣寫:
b.Set(10).Set(11).Clear(12).Flip(13);
注意,bitset 的索引是從 0 開始的。
記得之前在網上看過一道題:
一個農夫帶著一隻狼、一頭羊和一顆白菜來到河邊。他需要用船把他們帶到對岸。然而,這艘船隻能容下農夫本人和另外一種東西(要麼是狼,要麼是羊,要麼是白菜)。如果農夫不在場的話,狼就會吃掉羊,羊會吃掉白菜。請為農夫解決這個問題。
這其實是一個狀態搜尋的問題,用回溯法就能解決。農夫、狼、羊、白菜都有兩個狀態,即在河左岸(假設剛開始農夫所處的是左岸)還是河右岸。這裡實際上還有個船的狀態,由於船一定和農夫的狀態是一致的,就不用額外考慮了。這些狀態我們很容易用位集合來表示:
const ( FARMER = iota WOLF SHEEP CABBAGE )
編寫一個函數來判斷狀態是否合法。有兩種狀態不合法:
func IsStateValid(state *bitset.BitSet) bool { if state.Test(WOLF) == state.Test(SHEEP) && state.Test(WOLF) != state.Test(FARMER) { // 狼和羊在同一邊,並且不和農夫在同一邊 // 狼會吃掉羊,非法 return false } if state.Test(SHEEP) == state.Test(CABBAGE) && state.Test(SHEEP) != state.Test(FARMER) { // 羊和白菜在同一邊,並且不和農夫在同一邊 // 羊會吃掉白菜,非法 return false } return true }
接下來編寫搜尋函數:
func search(b *bitset.BitSet, visited map[string]struct{}) bool { if !IsStateValid(b) { return false } if _, exist := visited[b.String()]; exist { // 狀態已遍歷 return false } if b.Count() == 4 { return true } visited[b.String()] = struct{}{} for index := uint(FARMER); index <= CABBAGE; index++ { if b.Test(index) != b.Test(FARMER) { // 與農夫不在一邊,不能帶上船 continue } // 帶到對岸去 b.Flip(index) if index != FARMER { // 如果 index 為 FARMER,表示不帶任何東西 b.Flip(FARMER) } if search(b, visited) { return true } // 狀態恢復 b.Flip(index) if index != FARMER { b.Flip(FARMER) } } return false }
主函數:
func main() { b := bitset.New(4) visited := make(map[string]struct{}) fmt.Println(search(b, visited)) }
初始時,所有狀態為 0,都到對岸之後所有狀態為 1,故b.Count() == 4
表示都已到達對岸了。由於搜尋是盲目的,可能會無限迴圈:這次農夫將羊帶到對岸,下次又將其從對岸帶回來了。所以我們需要做狀態去重。bitset.String()
返回當前位集合的字串表示,我們以此來判斷狀態是否重複。
for 迴圈依次嘗試帶各種物品,或什麼也不帶。驅動查詢過程。
如果想得到農夫正確的動作序列,可以給 search 加一個引數,記錄每一步的操作:
func search(b *bitset.BitSet, visited map[string]struct{}, path *[]*bitset.BitSet) bool { // 記錄路徑 *path = append(*path, b.Clone()) if b.Count() == 4 { return true } // ... *path = (*path)[:len(*path)-1] return false } func main() { b := bitset.New(4) visited := make(map[string]struct{}) var path []*bitset.BitSet if search(b, visited, &path) { PrintPath(path) } }
如果找到解,列印之:
var names = []string{"農夫", "狼", "羊", "白菜"} func PrintState(b *bitset.BitSet) { fmt.Println("=======================") fmt.Println("河左岸:") for index := uint(FARMER); index <= CABBAGE; index++ { if !b.Test(index) { fmt.Println(names[index]) } } fmt.Println("河右岸:") for index := uint(FARMER); index <= CABBAGE; index++ { if b.Test(index) { fmt.Println(names[index]) } } fmt.Println("=======================") } func PrintMove(cur, next *bitset.BitSet) { for index := uint(WOLF); index <= CABBAGE; index++ { if cur.Test(index) != next.Test(index) { if !cur.Test(FARMER) { fmt.Printf("農夫將【%s】從河左岸帶到河右岸n", names[index]) } else { fmt.Printf("農夫將【%s】從河右岸帶到河左岸n", names[index]) } return } } if !cur.Test(FARMER) { fmt.Println("農夫獨自從河左岸到河右岸") } else { fmt.Println("農夫獨自從河右岸到河左岸") } } func PrintPath(path []*bitset.BitSet) { cur := path[0] PrintState(cur) for i := 1; i < len(path); i++ { next := path[i] PrintMove(cur, next) PrintState(next) cur = next } }
執行結果:
=======================
河左岸:
農夫
狼
羊
白菜
河右岸:
=======================
農夫將【羊】從河左岸帶到河右岸
=======================
河左岸:
狼
白菜
河右岸:
農夫
羊
=======================
農夫獨自從河右岸到河左岸
=======================
河左岸:
農夫
狼
白菜
河右岸:
羊
=======================
農夫將【狼】從河左岸帶到河右岸
=======================
河左岸:
白菜
河右岸:
農夫
狼
羊
=======================
農夫將【羊】從河右岸帶到河左岸
=======================
河左岸:
農夫
羊
白菜
河右岸:
狼
=======================
農夫將【白菜】從河左岸帶到河右岸
=======================
河左岸:
羊
河右岸:
農夫
狼
白菜
=======================
農夫獨自從河右岸到河左岸
=======================
河左岸:
農夫
羊
河右岸:
狼
白菜
=======================
農夫將【羊】從河左岸帶到河右岸
=======================
河左岸:
河右岸:
農夫
狼
羊
白菜
=======================
即農夫操作過程為:將羊帶到右岸->獨自返回->將白菜帶到右岸->再將羊帶回左岸->帶上狼到右岸->獨自返回->最後將羊帶到右岸->完成。
似乎直接使用位運算就可以了,為什麼要使用 bitset 庫呢?
因為通用,上面列舉的兩個例子都是很小的整數值,如果整數值超過 64 位,我們必然要通過切片來儲存。此時手寫操作就非常不便了,而且容易出錯。
庫的優勢體現在:
只要對外提供的介面保持不變,它可以一直優化內部實現。雖然我們也可以做,但是費時費力。而且有些優化涉及到比較複雜的演演算法,自己實現的難度較高且易錯。
有一個很典型的例子,求一個 uint64 的二進位制表示中 1 的數量(popcnt,或 population count)。實現的方法有很多。
最直接的,我們一位一位地統計:
func popcnt1(n uint64) uint64 { var count uint64 for n > 0 { if n&1 == 1 { count++ } n >>= 1 } return count }
考慮空間換時間,我們可以預先計算 0-255 這 256 個數位的二進位制表示中 1 的個數,然後每 8 位計算一次,可能將計算次數減少到之前的 1/8。這也是標準庫中的做法:
const pop8tab = "" + "x00x01x01x02x01x02x02x03x01x02x02x03x02x03x03x04" + "x01x02x02x03x02x03x03x04x02x03x03x04x03x04x04x05" + "x01x02x02x03x02x03x03x04x02x03x03x04x03x04x04x05" + "x02x03x03x04x03x04x04x05x03x04x04x05x04x05x05x06" + "x01x02x02x03x02x03x03x04x02x03x03x04x03x04x04x05" + "x02x03x03x04x03x04x04x05x03x04x04x05x04x05x05x06" + "x02x03x03x04x03x04x04x05x03x04x04x05x04x05x05x06" + "x03x04x04x05x04x05x05x06x04x05x05x06x05x06x06x07" + "x01x02x02x03x02x03x03x04x02x03x03x04x03x04x04x05" + "x02x03x03x04x03x04x04x05x03x04x04x05x04x05x05x06" + "x02x03x03x04x03x04x04x05x03x04x04x05x04x05x05x06" + "x03x04x04x05x04x05x05x06x04x05x05x06x05x06x06x07" + "x02x03x03x04x03x04x04x05x03x04x04x05x04x05x05x06" + "x03x04x04x05x04x05x05x06x04x05x05x06x05x06x06x07" + "x03x04x04x05x04x05x05x06x04x05x05x06x05x06x06x07" + "x04x05x05x06x05x06x06x07x05x06x06x07x06x07x07x08" func popcnt2(n uint64) uint64 { var count uint64 for n > 0 { count += uint64(pop8tab[n&0xff]) n >>= 8 } return count }
最後是 bitset 庫中的演演算法:
func popcnt3(x uint64) (n uint64) { x -= (x >> 1) & 0x5555555555555555 x = (x>>2)&0x3333333333333333 + x&0x3333333333333333 x += x >> 4 x &= 0x0f0f0f0f0f0f0f0f x *= 0x0101010101010101 return x >> 56 }
對以上三種實現進行效能測試:
goos: windows goarch: amd64 pkg: github.com/darjun/go-daily-lib/bitset/popcnt cpu: Intel(R) Core(TM) i7-7700 CPU @ 3.60GHz BenchmarkPopcnt1-8 52405 24409 ns/op BenchmarkPopcnt2-8 207452 5443 ns/op BenchmarkPopcnt3-8 1777320 602 ns/op PASS ok github.com/darjun/go-daily-lib/bitset/popcnt 4.697s
popcnt3 相對 popcnt1 有 40 倍的效能提升。在學習上我們可以自己嘗試造輪子,以此來加深自己對技術的理解。但是在工程上,通常更傾向於使用穩定的、高效的庫。
本文藉著 bitset 庫介紹了位集合的使用。並且應用 bitset 解決了農夫過河問題。最後我們討論了為什麼要使用庫?
大家如果發現好玩、好用的 Go 語言庫,歡迎到 Go 每日一庫 GitHub 上提交 issue
相關文章
<em>Mac</em>Book项目 2009年学校开始实施<em>Mac</em>Book项目,所有师生配备一本<em>Mac</em>Book,并同步更新了校园无线网络。学校每周进行电脑技术更新,每月发送技术支持资料,极大改变了教学及学习方式。因此2011
2021-06-01 09:32:01
综合看Anker超能充系列的性价比很高,并且与不仅和iPhone12/苹果<em>Mac</em>Book很配,而且适合多设备充电需求的日常使用或差旅场景,不管是安卓还是Switch同样也能用得上它,希望这次分享能给准备购入充电器的小伙伴们有所
2021-06-01 09:31:42
除了L4WUDU与吴亦凡已经多次共事,成为了明面上的厂牌成员,吴亦凡还曾带领20XXCLUB全队参加2020年的一场音乐节,这也是20XXCLUB首次全员合照,王嗣尧Turbo、陈彦希Regi、<em>Mac</em> Ova Seas、林渝植等人全部出场。然而让
2021-06-01 09:31:34
目前应用IPFS的机构:1 谷歌<em>浏览器</em>支持IPFS分布式协议 2 万维网 (历史档案博物馆)数据库 3 火狐<em>浏览器</em>支持 IPFS分布式协议 4 EOS 等数字货币数据存储 5 美国国会图书馆,历史资料永久保存在 IPFS 6 加
2021-06-01 09:31:24
开拓者的车机是兼容苹果和<em>安卓</em>,虽然我不怎么用,但确实兼顾了我家人的很多需求:副驾的门板还配有解锁开关,有的时候老婆开车,下车的时候偶尔会忘记解锁,我在副驾驶可以自己开门:第二排设计很好,不仅配置了一个很大的
2021-06-01 09:30:48
不仅是<em>安卓</em>手机,苹果手机的降价力度也是前所未有了,iPhone12也“跳水价”了,发布价是6799元,如今已经跌至5308元,降价幅度超过1400元,最新定价确认了。iPhone12是苹果首款5G手机,同时也是全球首款5nm芯片的智能机,它
2021-06-01 09:30:45