AI summary
type
status
date
slug
summary
category
tags
icon
password
带权重的随机选择
所谓「隐藏分」我不知道是不是真的,毕竟匹配机制是所有竞技类游戏的核心环节,想必非常复杂,不是简单几个指标就能搞定的。
但是如果把这个「隐藏分」机制简化,倒是一个值得思考的算法问题:系统如何以不同的随机概率进行匹配?
或者简单点说,如何带权重地做随机选择?
不要觉得这个很容易,如果给你一个长度为
n 的数组,让你从中等概率随机抽取一个元素,你肯定会做,random 一个 [0, n-1] 的数字出来作为索引就行了,每个元素被随机选到的概率都是 1/n。但假设每个元素都有不同的权重,权重地大小代表随机选到这个元素的概率大小,你如何写算法去随机获取元素呢?
528. 按权重随机选择
给你一个 下标从 0 开始 的正整数数组w,其中w[i]代表第i个下标的权重。请你实现一个函数pickIndex,它可以 随机地 从范围[0, w.length - 1]内(含0和w.length - 1)选出并返回一个下标。选取下标i的 概率 为w[i] / sum(w)。
- 例如,对于
w = [1, 3],挑选下标0的概率为1 / (1 + 3) = 0.25(即,25%),而选取下标1的概率为3 / (1 + 3) = 0.75(即,75%)。
前文 小而美的算法技巧:前缀和数组 加上 二分搜索算法核心代码模版 能够解决带权重的随机选择算法。
这个随机算法和前缀和技巧和二分搜索技巧能扯上啥关系?且听我慢慢道来。
假设给你输入的权重数组是
w = [1,3,2,1],我们想让概率符合权重,那么可以抽象一下,根据权重画出一条彩色的线段。如果我在线段上面随机丢一个石子,石子落在哪个颜色上,我就选择该颜色对应的权重索引,那么每个索引被选中的概率是不是就是和权重相关联了?
所以,你再仔细看看这条彩色的线段像什么?这不就是 [前缀和数组] 嘛:

那么接下来,如何模拟在线段上扔石子?
当然是随机数,比如上述前缀和数组
preSum,取值范围是 [1, 7],那么我生成一个在这个区间的随机数 target = 5,就好像在这条线段中随机扔了一颗石子:还有个问题,
preSum 中并没有 5 这个元素,我们应该选择比 5 大的最小元素,也就是 6,即 preSum 数组的索引 3:
如何快速寻找数组中大于等于目标值的最小元素?[二分搜索算法] 就是我们想要的。
到这里,这道题的核心思路就说完了,主要分几步:
- 根据权重数组
w生成前缀和数组preSum。
- 生成一个取值在
preSum之内的随机数,用二分搜索算法寻找大于等于这个随机数的最小元素索引。
- 最后对这个索引减一(因为前缀和数组有一位索引偏移),就可以作为权重数组的索引,即最终答案
上述思路应该不难理解,但是写代码的时候坑可就多了。
下面来抠细节,继续前面的例子:
就比如这个
preSum 数组,你觉得随机数 target 应该在什么范围取值?闭区间 [0, 7] 还是左闭右开 [0, 7)?都不是,应该在闭区间
[1, 7] 中选择,理解成一个个离散的点,不要理解成线段:- target=1 对应 preSum[1]
- target=2,3,4 对应 preSum[2]
- target=5,6 对应 preSum[3]
- target=7 对应 preSum[4]
这样不就正好与题目中要求的概率对应上了。
别忘了前缀和数组和原始数组有一位索引偏移,所以
left_bound 得到的索引应该减一。接下来,在
preSum 中寻找大于等于 target 的最小元素索引,应该用什么品种的二分搜索?搜索左侧边界的还是搜索右侧边界的?还记得搜索左侧边界的二分搜索的几种解读吗:
- 返回的这个值是
nums中大于等于target的最小元素索引。
- 返回的这个值是
target应该插入在nums中的索引位置。
- 返回的这个值是
nums中小于target的元素个数。
显然这里我们需要的是第一种。
完整代码如下:
田忌赛马
田忌赛马的故事大家应该都听说过:
田忌和齐王赛马,两人的马分上中下三等,如果同等级的马对应着比赛,田忌赢不了齐王。但是田忌遇到了孙膑,孙膑就教他用自己的下等马对齐王的上等马,再用自己的上等马对齐王的中等马,最后用自己的中等马对齐王的下等马,结果三局两胜,田忌赢了。
当然,这段历史也挺有意思的,那个讽齐王纳谏,自恋的不行的邹忌和田忌是同一时期的人,他俩后来就杠上了。不过这是题外话,我们这里就打住。
以前学到田忌赛马课文的时,我就在想,如果不是三匹马比赛,而是一百匹马比赛,孙膑还能不能合理地安排比赛的顺序,赢得齐王呢?
当时没想出什么好的点子,只觉得这里面最核心问题是要尽可能让自己占便宜,让对方吃亏。总结来说就是,打得过就打,打不过就拿自己的垃圾和对方的精锐互换。
不过,我一直没具体把这个思路实现出来,直到最近刷到力扣第 870 题「优势洗牌」,一眼就发现这是田忌赛马问题的加强版:
870. 优势洗牌
给定两个长度相等的数组nums1和nums2,nums1相对于nums2的优势可以用满足nums1[i] > nums2[i]的索引i的数目来描述。返回nums1的 任意 排列,使其相对于nums2的优势最大化。
这就像田忌赛马的情景,
nums1 就是田忌的马,nums2 就是齐王的马,数组中的元素就是马的战斗力,你就是孙膑,展示你真正的技术吧。仔细想想,这个题的解法还是有点扑朔迷离的。什么时候应该放弃抵抗去送人头,什么时候应该硬刚?这里面应该有一种算法策略来最大化「优势」。
送人头一定是迫不得已而为之的权宜之计,否则隔壁田忌就会以为你是齐王买来的演员。只有田忌的上等马比不过齐王的上等马时,才会用下等马去和齐王的上等马互换。
对于比较复杂的问题,可以尝试从特殊情况考虑。
你想,谁应该去应对齐王最快的马?肯定是田忌最快的那匹马,我们简称一号选手。
如果田忌的一号选手比不过齐王的一号选手,那其他马肯定是白给了,显然这种情况应该用田忌垫底的马去送人头,降低己方损失,保存实力,增加接下来比赛的胜率。
但如果田忌的一号选手能比得过齐王的一号选手,那就和齐王硬刚好了,反正这把田忌可以赢。
你也许说,这种情况下说不定田忌的二号选手也能干得过齐王的一号选手?如果可以的话,让二号选手去对决齐王的一号选手,不是更节约?
就好比,如果考 60 分就能过的话,何必考 90 分?每多考一分就亏一分,刚刚好卡在 60 分是最划算的。
这种节约的策略是没问题的,但是没有必要。这也是本题有趣的地方,需要绕个脑筋急转弯:
我们暂且把田忌的一号选手称为
T1,二号选手称为 T2,齐王的一号选手称为 Q1。如果
T2 能赢 Q1,你试图保存己方实力,让 T2 去战 Q1,把 T1 留着是为了对付谁?显然,你担心齐王还有战力大于
T2 的马,可以让 T1 去对付。但是你仔细想想,现在
T2 已经是可以战胜 Q1 的,Q1 可是齐王的最快的马耶,齐王剩下的那些马里,怎么可能还有比 T2 更强的马?所以,没必要节约,最后我们得出的策略就是:
将齐王和田忌的马按照战斗力排序,然后按照排名一一对比。如果田忌的马能赢,那就比赛,如果赢不了,那就换个垫底的来送人头,保存实力。
根据这个思路,我们需要对两个数组排序,但是
nums2 中元素的顺序不能改变,因为计算结果的顺序依赖 nums2 的顺序,所以不能直接对 nums2 进行排序,而是利用其他数据结构来辅助。同时,最终的解法还用到前文 双指针技巧秒杀七道数组题目 总结的双指针算法模板,用以处理「送人头」的情况:
算法的时间复杂度很好分析,也就是二叉堆和排序的复杂度 。