jjzjj

我说我为什么抽不到SSR,原来是这段代码在作祟...

个人博客coding3min.com 2023-03-28 原文

本文是龚国玮所写,熊哥有所新增修改删减,原文见文末。

我说我为什么抽不到SSR,原来是加权随机算法在作祟

阅读本文需要做好心理准备,建议带着深究到底的决心和毅力进行学习!

灵魂拷问

为什么有 50% 的几率获得金币?

为什么有 40% 的几率获得钻石?

为什么只有 9% 的几率获得装备?

为什么才有 1% 的几率获得极品装备?

是人性的扭曲,还是道德的沦丧,请和我一起走进今日说法

介绍

元素被选中的机会并不相等,而是由相对“权重”(或概率)被选中的,是偏心的,这就是加权随机。

举个栗子,假如现在有一个权重数组 w = {1, 2, 4, 8},它们代表如下规则。

  • $ \frac{1}{(1+2+4+8)} = \frac{1}{15} \approx 6.6$ % 几率选中第1个

  • $ \frac{2}{(1+2+4+8)} = \frac{2}{15} \approx 13.3$ % 几率选中第2个

  • $ \frac{3}{(1+2+4+8)} = \frac{4}{15} \approx 26.6$ % 几率选中第3个

  • $ \frac{1}{(1+2+4+8)} = \frac{8}{15} \approx 53.3$ % 几率选中第4个

一个小小的概率问题,居然引出了大大的思考。

方案一、笨笨的办法

所以要设计一个加权算法的程序,你会怎么写呢?

第一个方法把权重所在的位置展开,然后从该列表中随机选择。

假设现在有权重列表 {1, 2, 4, 8}

那我们得到的候选列表将是

{0, 1, 1, 2, 2, 2, 2, 3, 3, 3, 3, 3, 3, 3, 3}

然后通过 rand.Intn() ,获取一个随机数,就完成了,代码如下。

func weightedRandomS1(weights []int) int {
	if len(weights) == 0 {
		return 0
	}

	var indexList []int

	for i, weight := range weights {
		cnt := 0
		for weight > cnt {
			indexList = append(indexList, i)
			cnt++
		}
	}

	rand.Seed(time.Now().UnixNano())
	return indexList[rand.Intn(len(indexList))]
}

当权重特别大的时候,这种方案费时费力,又占空间。先别急往下看,你能想到更好的办法吗?

方案二、略显聪明

由于总权重为 15(1+2+4+8),我们可以生成一个 [0,15) 的随机整数,然后根据这个数字返回索引。代码如下。

func weightedRandomS2() int {
	rand.Seed(time.Now().UnixNano())
	r := rand.Intn(15)
	if r <= 1 {
		return 0
	} else if 1 < r && r <= 3 {
		return 1
	} else if 3 < r && r <= 7 {
		return 2
	} else {
		return 3
	}
}

妙不妙!!但你以为这就是效率最高的办法吗?

写那么多if else不痛苦吗我的宝贝。

方案三、神之一手

何必将随机数和所有的范围进行比较呢?直接遍历随机数减去权重,如果结果小于等于零,不就是我们要的结果下标吗?

func weightedRandomS3(weights []int) int {
	rand.Seed(time.Now().UnixNano())
	r := rand.Intn(len(weights))
	for i, v := range weights {
		r = r - v
		if r <= 0 {
			return i
		}
	}
	return len(weights) - 1
}

这种方法叫放弃临时名单。想不明白就评论问!

方案四、小小优化

对于方案三,怎么有效的减少遍历次数呢?

r小于等于 0 的速度越快,算法越高效。那我们就让 r 到达 0 更快。先排序这样就能先减去权重大的,减少遍历次数。

func weightedRandomS4(weights []int) int {
	sort.Sort(sort.Reverse(sort.IntSlice(weights)))
	....

有人就不服了,排序不是更浪费时间?

是的!虽然看起来减少遍历次数!但排序本身就要遍历就是更浪费时间。。。

但是一次排序,反复使用,还是能提高效率的!

方案五、不可思议!

有没有办法不用排序,而让原数组有序呢?

有人就说了,你这不是扯么?

如果每次遍历都加上上一个权重,那整个数字就是递增的!

再用二分就能加快速度了,时间复杂度从 $ O(n) $ 直接变为 $ O(log(n)) $。

func weightedRandomS5(weights []int) int {
	rand.Seed(time.Now().UnixNano())
	sum := 0
	var sumWeight []int
	for _, v := range weights {
		sum += v
		sumWeight = append(sumWeight, sum)
	}
	r := rand.Intn(sum)
	idx := sort.SearchInts(sumWeight, r)
	return weights[idx]
}

读到这里,对源码没有信心学习的朋友,可以直接撤了。 真正的高阶优化要来了。

方案六、不死不休

到目前的位置,我们的解决方案已经足够好了,但是仍然有改进的余地。

方案五中,我们使用了 Go 标准库的二分查找算法 sort.SearchInts() ,是封装了通用的 sort.Search() 函数,如下。

sort.Search() 的函数参数需要一个闭包函数,并且这个闭包函数是在 for 循环中使用的,如下。

闭包函数反复调用,在编译期会产生额外的开销。因为会产生更多的跳转,跳转会引起压栈(函数参数都是会压栈的)。

我们手动提出取函数,就可以减少编译器的内联(文末会解释)。

func weightedRandomS5(weights []int) int {
...
idx := sort.SearchInts(sumWeight, r)
	return weights[idx]
}
func searchInts(a []int, x int) int {
	i, j := 0, len(a)
	for i < j {
		h := int(uint(i+j) >> 1)
		if a[h] < x {
			i = h + 1
		} else {
			j = h
		}
	}
	return i
}

通过基准测试可以看到吞吐量提升了 33% 以上。对于大型数据集,优势越明显。

方案七,“偷鸡”取巧--轮盘赌

目前为止我们所有的方案都有一个共同点 —— 生成一个介于 0 和“权重之和”之间的随机数,并找出它属于哪个“切片”。

还有一种不同的方法。

func weightedRandomS7(weights []float64) int {
	var sum float64
	var winner int
	rand.Seed(time.Now().UnixNano())
	for i, v := range weights {
		sum += v
		f := rand.Float64()
		if f*sum < v {
			winner = i
		}
	}
	return winner
}
  • 从命中的角度去考虑。
  • 权重越大,命中率自然越大了。
  • 既然是随机,多次随机和单次随机而言都是随机的。

这个算法的一个有趣的特性是你不需要提前知道权重的数量就可以使用它。所以说,它或许可以用于某种流。

尽管这种方案很酷,但它比其他方案慢得多。相对于方案一,它也快了 25%

小结

  • 下标直接展开到列表里,随机长度取值。
  • if else 取值。
  • 遍历随机数减去权重,结果小于等于零时。
  • 先排序,再用方法三。
  • 免排序,直接加和,再二分。
  • 优化源码中的二分法。
  • 轮盘赌算法,每次都去赌。

内联:编译器的一个名词。我们的代码最终都是经过编译系统转换成可执行二进制文件。汇编阶段读取的是词法、语法单元输出的结果。而内联是编译器对词法、语法分析器对源代码做出的分析,然后产生二进制代码这个过程叫内联。

源代码

https://github.com/guowei-gong/weighted-random

原文:加权随机设计与实现

一起进步

你好,我是小熊,是一个爱技术但是更爱钱的程序员。上进且佛系自律的人。喜欢发小秘密/臭屁又爱炫耀。

奋斗的大学,激情的现在。赚了钱买了房,写了书出了名。当过面试官,带过徒弟搬过转。

大厂外来务工人员。是我,我是小熊,是不一样的烟火欢迎围观。

我的博客 机智的程序员小熊 欢迎收藏

有关我说我为什么抽不到SSR,原来是这段代码在作祟...的更多相关文章

  1. ruby - 为什么我可以在 Ruby 中使用 Object#send 访问私有(private)/ protected 方法? - 2

    类classAprivatedeffooputs:fooendpublicdefbarputs:barendprivatedefzimputs:zimendprotecteddefdibputs:dibendendA的实例a=A.new测试a.foorescueputs:faila.barrescueputs:faila.zimrescueputs:faila.dibrescueputs:faila.gazrescueputs:fail测试输出failbarfailfailfail.发送测试[:foo,:bar,:zim,:dib,:gaz].each{|m|a.send(m)resc

  2. ruby-on-rails - Rails - 子类化模型的设计模式是什么? - 2

    我有一个模型:classItem项目有一个属性“商店”基于存储的值,我希望Item对象对特定方法具有不同的行为。Rails中是否有针对此的通用设计模式?如果方法中没有大的if-else语句,这是如何干净利落地完成的? 最佳答案 通常通过Single-TableInheritance. 关于ruby-on-rails-Rails-子类化模型的设计模式是什么?,我们在StackOverflow上找到一个类似的问题: https://stackoverflow.co

  3. ruby - 如何在 buildr 项目中使用 Ruby 代码? - 2

    如何在buildr项目中使用Ruby?我在很多不同的项目中使用过Ruby、JRuby、Java和Clojure。我目前正在使用我的标准Ruby开发一个模拟应用程序,我想尝试使用Clojure后端(我确实喜欢功能代码)以及JRubygui和测试套件。我还可以看到在未来的不同项目中使用Scala作为后端。我想我要为我的项目尝试一下buildr(http://buildr.apache.org/),但我注意到buildr似乎没有设置为在项目中使用JRuby代码本身!这看起来有点傻,因为该工具旨在统一通用的JVM语言并且是在ruby中构建的。除了将输出的jar包含在一个独特的、仅限ruby​​

  4. ruby - 什么是填充的 Base64 编码字符串以及如何在 ruby​​ 中生成它们? - 2

    我正在使用的第三方API的文档状态:"[O]urAPIonlyacceptspaddedBase64encodedstrings."什么是“填充的Base64编码字符串”以及如何在Ruby中生成它们。下面的代码是我第一次尝试创建转换为Base64的JSON格式数据。xa=Base64.encode64(a.to_json) 最佳答案 他们说的padding其实就是Base64本身的一部分。它是末尾的“=”和“==”。Base64将3个字节的数据包编码为4个编码字符。所以如果你的输入数据有长度n和n%3=1=>"=="末尾用于填充n%

  5. ruby - 解析 RDFa、微数据等的最佳方式是什么,使用统一的模式/词汇(例如 schema.org)存储和显示信息 - 2

    我主要使用Ruby来执行此操作,但到目前为止我的攻击计划如下:使用gemsrdf、rdf-rdfa和rdf-microdata或mida来解析给定任何URI的数据。我认为最好映射到像schema.org这样的统一模式,例如使用这个yaml文件,它试图描述数据词汇表和opengraph到schema.org之间的转换:#SchemaXtoschema.orgconversion#data-vocabularyDV:name:namestreet-address:streetAddressregion:addressRegionlocality:addressLocalityphoto:i

  6. ruby-on-rails - Rails 源代码 : initialize hash in a weird way? - 2

    在rails源中:https://github.com/rails/rails/blob/master/activesupport/lib/active_support/lazy_load_hooks.rb可以看到以下内容@load_hooks=Hash.new{|h,k|h[k]=[]}在IRB中,它只是初始化一个空哈希。和做有什么区别@load_hooks=Hash.new 最佳答案 查看rubydocumentationforHashnew→new_hashclicktotogglesourcenew(obj)→new_has

  7. ruby - 为什么 4.1%2 使用 Ruby 返回 0.0999999999999996?但是 4.2%2==0.2 - 2

    为什么4.1%2返回0.0999999999999996?但是4.2%2==0.2。 最佳答案 参见此处:WhatEveryProgrammerShouldKnowAboutFloating-PointArithmetic实数是无限的。计算机使用的位数有限(今天是32位、64位)。因此计算机进行的浮点运算不能代表所有的实数。0.1是这些数字之一。请注意,这不是与Ruby相关的问题,而是与所有编程语言相关的问题,因为它来自计算机表示实数的方式。 关于ruby-为什么4.1%2使用Ruby返

  8. ruby - ruby 中的 TOPLEVEL_BINDING 是什么? - 2

    它不等于主线程的binding,这个toplevel作用域是什么?此作用域与主线程中的binding有何不同?>ruby-e'putsTOPLEVEL_BINDING===binding'false 最佳答案 事实是,TOPLEVEL_BINDING始终引用Binding的预定义全局实例,而Kernel#binding创建的新实例>Binding每次封装当前执行上下文。在顶层,它们都包含相同的绑定(bind),但它们不是同一个对象,您无法使用==或===测试它们的绑定(bind)相等性。putsTOPLEVEL_BINDINGput

  9. ruby - Infinity 和 NaN 的类型是什么? - 2

    我可以得到Infinity和NaNn=9.0/0#=>Infinityn.class#=>Floatm=0/0.0#=>NaNm.class#=>Float但是当我想直接访问Infinity或NaN时:Infinity#=>uninitializedconstantInfinity(NameError)NaN#=>uninitializedconstantNaN(NameError)什么是Infinity和NaN?它们是对象、关键字还是其他东西? 最佳答案 您看到打印为Infinity和NaN的只是Float类的两个特殊实例的字符串

  10. ruby-on-rails - 如果 Object::try 被发送到一个 nil 对象,为什么它会起作用? - 2

    如果您尝试在Ruby中的nil对象上调用方法,则会出现NoMethodError异常并显示消息:"undefinedmethod‘...’fornil:NilClass"然而,有一个tryRails中的方法,如果它被发送到一个nil对象,它只返回nil:require'rubygems'require'active_support/all'nil.try(:nonexisting_method)#noNoMethodErrorexceptionanymore那么try如何在内部工作以防止该异常? 最佳答案 像Ruby中的所有其他对象

随机推荐