gitlab.com/yannislg/go-pulse@v0.0.0-20210722055913-a3e24e95638d/les/utils/weighted_select.go (about)

     1  // Copyright 2016 The go-ethereum Authors
     2  // This file is part of the go-ethereum library.
     3  //
     4  // The go-ethereum library is free software: you can redistribute it and/or modify
     5  // it under the terms of the GNU Lesser General Public License as published by
     6  // the Free Software Foundation, either version 3 of the License, or
     7  // (at your option) any later version.
     8  //
     9  // The go-ethereum library is distributed in the hope that it will be useful,
    10  // but WITHOUT ANY WARRANTY; without even the implied warranty of
    11  // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
    12  // GNU Lesser General Public License for more details.
    13  //
    14  // You should have received a copy of the GNU Lesser General Public License
    15  // along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
    16  
    17  package utils
    18  
    19  import "math/rand"
    20  
    21  // wrsItem interface should be implemented by any entries that are to be selected from
    22  // a WeightedRandomSelect set. Note that recalculating monotonously decreasing item
    23  // weights on-demand (without constantly calling Update) is allowed
    24  type wrsItem interface {
    25  	Weight() int64
    26  }
    27  
    28  // WeightedRandomSelect is capable of weighted random selection from a set of items
    29  type WeightedRandomSelect struct {
    30  	root *wrsNode
    31  	idx  map[wrsItem]int
    32  }
    33  
    34  // NewWeightedRandomSelect returns a new WeightedRandomSelect structure
    35  func NewWeightedRandomSelect() *WeightedRandomSelect {
    36  	return &WeightedRandomSelect{root: &wrsNode{maxItems: wrsBranches}, idx: make(map[wrsItem]int)}
    37  }
    38  
    39  // setWeight sets an item's weight to a specific value (removes it if zero)
    40  func (w *WeightedRandomSelect) setWeight(item wrsItem, weight int64) {
    41  	idx, ok := w.idx[item]
    42  	if ok {
    43  		w.root.setWeight(idx, weight)
    44  		if weight == 0 {
    45  			delete(w.idx, item)
    46  		}
    47  	} else {
    48  		if weight != 0 {
    49  			if w.root.itemCnt == w.root.maxItems {
    50  				// add a new level
    51  				newRoot := &wrsNode{sumWeight: w.root.sumWeight, itemCnt: w.root.itemCnt, level: w.root.level + 1, maxItems: w.root.maxItems * wrsBranches}
    52  				newRoot.items[0] = w.root
    53  				newRoot.weights[0] = w.root.sumWeight
    54  				w.root = newRoot
    55  			}
    56  			w.idx[item] = w.root.insert(item, weight)
    57  		}
    58  	}
    59  }
    60  
    61  // Update updates an item's weight, adds it if it was non-existent or removes it if
    62  // the new weight is zero. Note that explicitly updating decreasing weights is not necessary.
    63  func (w *WeightedRandomSelect) Update(item wrsItem) {
    64  	w.setWeight(item, item.Weight())
    65  }
    66  
    67  // Remove removes an item from the set
    68  func (w *WeightedRandomSelect) Remove(item wrsItem) {
    69  	w.setWeight(item, 0)
    70  }
    71  
    72  // Choose randomly selects an item from the set, with a chance proportional to its
    73  // current weight. If the weight of the chosen element has been decreased since the
    74  // last stored value, returns it with a newWeight/oldWeight chance, otherwise just
    75  // updates its weight and selects another one
    76  func (w *WeightedRandomSelect) Choose() wrsItem {
    77  	for {
    78  		if w.root.sumWeight == 0 {
    79  			return nil
    80  		}
    81  		val := rand.Int63n(w.root.sumWeight)
    82  		choice, lastWeight := w.root.choose(val)
    83  		weight := choice.Weight()
    84  		if weight != lastWeight {
    85  			w.setWeight(choice, weight)
    86  		}
    87  		if weight >= lastWeight || rand.Int63n(lastWeight) < weight {
    88  			return choice
    89  		}
    90  	}
    91  }
    92  
    93  const wrsBranches = 8 // max number of branches in the wrsNode tree
    94  
    95  // wrsNode is a node of a tree structure that can store wrsItems or further wrsNodes.
    96  type wrsNode struct {
    97  	items                    [wrsBranches]interface{}
    98  	weights                  [wrsBranches]int64
    99  	sumWeight                int64
   100  	level, itemCnt, maxItems int
   101  }
   102  
   103  // insert recursively inserts a new item to the tree and returns the item index
   104  func (n *wrsNode) insert(item wrsItem, weight int64) int {
   105  	branch := 0
   106  	for n.items[branch] != nil && (n.level == 0 || n.items[branch].(*wrsNode).itemCnt == n.items[branch].(*wrsNode).maxItems) {
   107  		branch++
   108  		if branch == wrsBranches {
   109  			panic(nil)
   110  		}
   111  	}
   112  	n.itemCnt++
   113  	n.sumWeight += weight
   114  	n.weights[branch] += weight
   115  	if n.level == 0 {
   116  		n.items[branch] = item
   117  		return branch
   118  	}
   119  	var subNode *wrsNode
   120  	if n.items[branch] == nil {
   121  		subNode = &wrsNode{maxItems: n.maxItems / wrsBranches, level: n.level - 1}
   122  		n.items[branch] = subNode
   123  	} else {
   124  		subNode = n.items[branch].(*wrsNode)
   125  	}
   126  	subIdx := subNode.insert(item, weight)
   127  	return subNode.maxItems*branch + subIdx
   128  }
   129  
   130  // setWeight updates the weight of a certain item (which should exist) and returns
   131  // the change of the last weight value stored in the tree
   132  func (n *wrsNode) setWeight(idx int, weight int64) int64 {
   133  	if n.level == 0 {
   134  		oldWeight := n.weights[idx]
   135  		n.weights[idx] = weight
   136  		diff := weight - oldWeight
   137  		n.sumWeight += diff
   138  		if weight == 0 {
   139  			n.items[idx] = nil
   140  			n.itemCnt--
   141  		}
   142  		return diff
   143  	}
   144  	branchItems := n.maxItems / wrsBranches
   145  	branch := idx / branchItems
   146  	diff := n.items[branch].(*wrsNode).setWeight(idx-branch*branchItems, weight)
   147  	n.weights[branch] += diff
   148  	n.sumWeight += diff
   149  	if weight == 0 {
   150  		n.itemCnt--
   151  	}
   152  	return diff
   153  }
   154  
   155  // Choose recursively selects an item from the tree and returns it along with its weight
   156  func (n *wrsNode) choose(val int64) (wrsItem, int64) {
   157  	for i, w := range n.weights {
   158  		if val < w {
   159  			if n.level == 0 {
   160  				return n.items[i].(wrsItem), n.weights[i]
   161  			}
   162  			return n.items[i].(*wrsNode).choose(val)
   163  		}
   164  		val -= w
   165  	}
   166  	panic(nil)
   167  }