前言
好久没有抽时间刷题了,突然发现leetcode有个random标签,就好奇这下面是什么类型的题,结果没想到碰巧看到了这个在公司游戏化教学项目也有涉及的问题,遂顺手甩一篇题解。
在本文中,我会先给出该题的题解。之后,在后文的『扩展延伸』中给出该题缩小数据范围后的另一种更高效的解法,以及我在全日制游戏项目中遇到的与该问题相似的案例。
这题主要考点是二分查找,以及能否灵活的转化问题。此外,如果权值数组w
的长度范围没那么长,权值范围没那么大的话,考点就从二分查找变为哈希了。具体可以看我后文中的扩展延伸。
题解
解析
基本思想就是把各个位置的权值表现为在一个数值范围中占据的区间。具体过程为,构造一个总长度为权值列表w
中各值之和sumWeight
的列表weightRoute
。在随机选取位置时,rand一个数值weight
,此时,若存在下标index
,使
w
e
i
g
h
t
∈
[
w
e
i
g
h
t
R
o
u
t
e
[
i
n
d
e
x
]
,
w
e
i
g
h
t
R
o
u
t
e
[
i
n
d
e
x
+
1
]
)
weight \in [weightRoute[index], weightRoute[index+1] )
weight∈[weightRoute[index],weightRoute[index+1]),则下标index
即为我们要获取的坐标。后续搜索阶段的二分查找是基本操作了,思想也很简单,这里就不赘述了。
代码
构造阶段,时间复杂度为O(n),搜索阶段时间复杂度为O(logn)。
class Solution {
protected $sumWeight;
protected $length;
protected $weightRoute = [0];
function __construct(array $w) {
// sum total
$this->sumWeight = array_sum($w);
$this->length = count($w);
for ($i = 1; $i < $this->length; $i++) {
$this->weightRoute[$i] = $w[$i-1] + $this->weightRoute[$i-1];
}
echo json_encode($this->weightRoute) . PHP_EOL;
}
/**
* @return Integer
*/
function pickIndex() {
$weight = mt_rand(0, $this->sumWeight - 1);
$pick = $this->search($weight);
return $pick;
}
/**
* 二分查找$weight在$weightRoute中对应的下标
* @var int $weight
* @return int
*/
function search(int $weight) {
$i = 0;
$j = $this->length - 1;
$middle = intval(($j - $i) / 2);
$weightRoute = $this->weightRoute;
while (true) {
if ($weightRoute[$j] <= $weight) {
// success
return $j;
} elseif ($i === $middle || $j === $middle) {
// 如果结果是$j,上面的if就已经返回了,不会走到这儿
return $i;
} elseif ($weightRoute[$middle] <= $weight) {
// to right
$i = $middle;
} else {
// to left
$j = $middle - 1;
}
$middle = intval(($j - $i) / 2) + $i;
}
}
}
运行结果如下。
不使用二分查找?
我很好奇,如果不使用二分查找,能通过所有测试用例吗?于是就将Solution::pickIndex()
改为如下的顺序查找,Solution::__construct()
的代码和上述保持一致。
/**
* @return Integer
*/
function pickIndex() {
$weight = mt_rand(0, $this->sumWeight - 1);
// echo 'w: ' . $weight . PHP_EOL;
$pick = null;
for ($i = 0;$i < $this->length; $i++) {
// echo $i . PHP_EOL;
if ($this->weightRoute[$i] > $weight) {
break;
}
$pick = $i;
}
return $pick;
}
最后结果如下。
也通过了,内存消耗和上面用了二分查找的一样,但是所有测试用例耗时4s。看到这个点我很想吐槽一下。。。。要是该题在leetcode时间限制都没有,那么,不用二分查找也能解决昂(手动哭笑脸)。
扩展延伸
在本篇中,将假定该题缩小数据范围,从而给出另一种更高效的解法,以及我在全日制游戏项目中遇到的与该问题相似的实际案例。
题目变形
这题说是二分查找,但在实际应用中,如果我们权重总和不是特别大的话,用下述办法可以另构造阶段时间复杂度O(n),搜索阶段时间复杂度O(1)。
解析
基本思想就是把各个位置的权值表现为在一个列表中占位长度。具体过程为,构造一个总长度为权值列表w
中各值之和sumWeight
的列表weightRoute
。在随机选取位置时,rand一个数值index
,此时weightRoute[index]
即为我们要获取的坐标。上文中的解析对比,会发现,这两者其实是一种办法,只不过现在的解法是以空间换时间罢了(搜索阶段从O(logn) 提升至O(1) )。
代码实现
class Solution {
protected $sumWeight;
protected $length;
protected $weightRoute = [];
function __construct($w) {
// sum total
$this->sumWeight = array_sum($w);
$this->length = count($w);
for ($i = 0; $i < $this->length;) {
if ($w[$i]) {
$this->weightRoute[] = $i;
$w[$i]--;
} else {
$i++;
}
}
echo json_encode($this->weightRoute);
}
/**
* @return Integer
*/
function pickIndex() {
$index = mt_rand(0, $this->sumWeight - 1);
return $this->weightRoute[$index];
}
}
在本题中,存在如下条件。
- 1 < = w . l e n g t h < = 10000 1 <= w.length <= 10000 1<=w.length<=10000
- 1 < = w [ i ] < = 1 0 5 1 <= w[i] <= 10^5 1<=w[i]<=105
那么,极端情况下,上述算法中的
weightRoute
总长会达到 1 0 9 10^9 109,提交以后高概率爆栈。
实际应用
在全日制游戏中,时常会有这种场景。这里以游戏化教学的数学游戏远航时代为例。(更早的游戏哪里有用到不记得了 =。 =)
远航时代中,玩家需要经营一家公司,游戏开始前玩家选择要达成的目标,在之后21回合内,达成该目标即可通关。在这一游戏中,存在这么一种情况,每回合随机产生经济危机,但每回合的经济危机概率不一样。这个问题和上述问题十分接近但又不一样。如果我们将概率看作上述问题的权值,两个问题关键区别如下。
- 我们知道概率列表为固定长度20。
- 我们在实现逻辑代码的时候就知道各个回合的权值(概率)
也就是说上述权值列表w
在这里是固定的一个数组,因此,构造阶段不需要写那么复杂的代码,我们实现定好一个常量数组作为权值列表即可(从这一点来看,这个问题就从难度中等降为简单了)。当然,由于php是无状态的,这个问题在实际实现中又有差别。这里不在赘述,大家了解这个算法有这么一个应用场景即可。