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

news2024/10/7 6:40:54

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

我说我为什么抽不到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.SearchInts

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

sort.Search

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

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

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

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

一起进步

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

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

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

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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/362976.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【数据结构】顺序表和链表的区别和联系(详解)

顺序表和链表的区别&#xff08;详解&#xff09; 文章目录顺序表和链表的区别&#xff08;详解&#xff09;前言一、顺序表和链表的关系二、顺序表1.优点2.缺点三、链表1.优点2.缺点四、区别表格总结前言 本文给大家介绍顺序表和链表的各自的优缺点和区别与联系&#xff0c;结…

华为OD机试 - 事件推送(C++) | 附带编码思路 【2023】

刷算法题之前必看 参加华为od机试,一定要注意不要完全背诵代码,需要理解之后模仿写出,通过率才会高。 华为 OD 清单查看地址:https://blog.csdn.net/hihell/category_12199283.html 华为OD详细说明:https://dream.blog.csdn.net/article/details/128980730 华为OD机试题…

20230222 【梳理】肿瘤检测 预处理+ML+DL

一、预处理 1、形态学【使图像中的重要部分更加可见,并消除MRI图像的琐碎部分。】 形态学操作是一种非线性操作,涉及在二值图像上移动一个窗口(或结构元素),以一种方式帮助增长图像(膨胀)或缩小图像(侵蚀)[30]。这种预处理技术更有用,特别是当MRI图像中存在不需要

基于计算机视觉的智慧养老系统

基于计算机视觉的智慧养老系统 Intelligent elderly care system based on computer vision 基于计算机视觉的智慧养老系统通过&#xff08;模拟&#xff09;多组摄像头实时拍摄到的画面&#xff0c;用计算机视觉技术实时分析老人的情感、是否有人摔倒、是否有人闯入禁止区域…

我的 System Verilog 学习记录(2)

引言 从本文开始&#xff0c;就开始系统学习 System Verilog &#xff0c;不只是语法&#xff0c;还有结合 Questa Sim 的实际编程练习、Debug。 本文简单介绍 System Verilog 语言的用途以及学习的必要性。 前文链接&#xff1a; 我的 System Verilog 学习记录&#xff08…

C#从值类型、引用类型到装箱和拆箱

上一篇文章讲了C#的值类型和引用类型&#xff0c;这里再来看看值类型和引用类型最直接的使用场景&#xff1a;装箱和拆箱。 一、基本概念 装箱&#xff1a;值类型转化为引用类型的过程。从托管堆中为新生成的引用类型对象分配内存,再把值类型的实例字段拷贝到托管堆上新对象的…

面向对象的三大特征

面向对象&#xff08;OOP&#xff09;的三大特征&#xff1a;继承、封装、多态 一、封装性 为什么需要封装&#xff1f;封装的作用和含义&#xff1f; 我要用洗衣机&#xff0c;只需要按一下开关和洗涤模式就可以了。有必要了解洗衣机内 部的结构吗&#xff1f;有必要碰电动机…

C语言【atoi函数】

C语言【atoi函数】&#x1fac5;系统atoi函数&#x1fac5; 模拟实现atoi函数看到atoi函数&#xff0c;有人又会问有这个函数&#xff0c;我怎么没用过。那就说明&#xff1a;不是你刷题太少&#xff0c;就是atoi函数存在感太低。 这篇函数就带你领略atoi函数的魅力 &#x1fa…

APP测试中ios和androis的区别,有哪些注意点

目录 一、运行机制不同 二、对app内存消耗处理方式不同 三、后台制度不同 四、最高权限指令不同 五、推送机制不同 六、抓取方式不同 七、灰度发版机制不同 八、审核机制不同 总结感谢每一个认真阅读我文章的人&#xff01;&#xff01;&#xff01; 重点&#xff1a;…

Arduino-流水灯

LED流水灯实验产品介绍&#xff1a;电阻&#xff1a;电阻器通常分为三类&#xff1a;固定电阻器、可调电阻器及特殊电阻器。普通电阻器的识别电阻器阻值和允许误差常用的标志方法有下列3种。1、直接标志法将电阻器的阻值和误差等级直接用数字印在电阻器上。对小于1000W的阻值只…

C语言进阶(五)—— 多维数组

1. 一维数组 元素类型角度&#xff1a;数组是相同类型的变量的有序集合内存角度&#xff1a;连续的一大片内存空间在讨论多维数组之前&#xff0c;我们还需要学习很多关于一维数组的知识。首先让我们学习一个概念。1.1 数组名考虑下面这些声明&#xff1a;int a; int b[10];我们…

6、Fatfs系统移植

注意&#xff1a;挂载Fatfs笔记 Fatfs系统读写文件的时间是不固定的&#xff0c;随机性 搭载Fatfs的外设通信方式建议开启DMA方式&#xff0c;否则应避免中断打断时序&#xff0c;导致Fatfs出现FR_DISK_ERR&#xff08;A hard error occurred in the low level disk I/O layer&…

金三银四丨黑蛋老师带你剖析-安全开发岗

作者丨黑蛋在之前呢&#xff0c;我们聊了二进制这块的病毒岗位&#xff0c;漏洞岗位&#xff0c;逆向岗位以及CTF这块的岗位。今天我们就来聊一聊安全开发类的工作岗位。首先网络安全方向中安全开发岗位都有哪些&#xff0c;安全开发主要指安全研发工程师或安全开发工程师&…

手写线程池实例并测试

前言&#xff1a;在之前的文章中介绍过线程池的核心原理&#xff0c;在一次面试中面试官让手写线程池&#xff0c;这块知识忘记的差不多了&#xff0c;因此本篇文章做一个回顾。 希望能够加深自己的印象以及帮助到其他的小伙伴儿们&#x1f609;&#x1f609;。 如果文章有什么…

运动戴耳机哪种款式比较好、最好用的运动耳机

很多人喜欢运动时听音乐,因为在运动场景中,听歌的节奏与步频匹配的时候&#xff0c;的确是可以起到很好的激励和缓解情绪的作用。认认真真地选择一副适合自己跑步的运动耳机&#xff0c;成了很多跑步爱好者的实际需求&#xff0c;专门为运动打造的耳机也不少!那么,如何挑选一款…

macm1安装qt6

macm1安装qt6 本文目录macm1安装qt6前提下载在线安装包使用安装包进行安装QT creator测试运行环境前提 需要安装xcode以及command line tools 需要先注册账号密码 根据官方提示&#xff0c;5.15版本以上就不支持离线安装了&#xff0c;需要下载在线安装包 OFFLINE_README.txt…

Anaconda和PyCharm的一些安装问题和命令

今天更新了Windows上的Anaconda到2.3.2&#xff0c;PyCharm到2022.3。 ——发现是纯纯的犯贱orz。出了一堆问题。在这里记录一下供后来者参考。 Anaconda安装 将.\anaconda3\Scripts 和.\anaconda3\Library\bin添加到系统环境变量中。 新建环境的目录在.\anaconda3\envs下 N…

Retrofit源码分析

文章目录一、简介二、源码分析2.1Retrofit的本质流程2.2源码分析2.2.1 创建Retrofit实例步骤1步骤2步骤3步骤4步骤5总结2.2.2创建网络请求接口的实例外观模式 & 代理模式1.外观模式2. 代理模式步骤3步骤4总结2.2.3执行网络请求同步请求OkHttpCall.execute()1.发送请求过程2…

解决 NestHost requires ASM7 (shrink、kotlin metadata)

① 场景 Caused by: java.lang.RuntimeException: NestHost requires ASM7Failed to resolve class org/vigame/demo/CrashHandler$1.class[transform input:not foundproject input:not foundaar input:not found]Caused by: java.lang.UnsupportedOperationException: NestH…

flstudio21中文版下载安装图文教程

fl studio21中文版是一款免费的音乐编曲制作软件&#xff0c;有了它你可以制作出色的音乐。它为您提供了一个集成的开发环境&#xff0c;使用起来非常简单有效&#xff0c;您的工作会变得更有条理。同时FL Studio为用户提供了更先进和原创的音乐制作理念&#xff0c;用户可以轻…