Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
264 changes: 264 additions & 0 deletions tree/random-pick-with-weight/answer.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,264 @@
# 528. Random Pick with Weight

* 703. Kth Larget Element in a Streamの関連問題がDiscordにあったので、
Arai60の途中で解いてみました。

## STEP1

* 発想
* 重み付きの確率を行う方法。
* `w_0`, `w_1`, `w_2`, ...をsumで割り算し`w_0`/sum, `w_1`/sum, `w_2`/sum, ...とした後に、累積和をとる。
0から1のランダムな値がどの重みに属するかを調べることで、重みを考慮したサンプリングできそう。
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

確率だからよいかと思う一方で、浮動小数点は色々なことが起きるので、自然数で処理したほうが私は好みです。

Copy link
Owner Author

@syoshida20 syoshida20 May 5, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  • 浮動小数点の懸念事項
  • 整数で解く方法

を意識ができておりませんでした。ご指摘、ありがとうございます。

懸念項目の一例を調べました。

  • 丸誤差
    • 有効数字の桁が落ちる
    • 情報が落ちる
  • 演算の順序性が保たれない

参考:

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

6643657 で整数で解く方法を追加しました。


* 詰まったところ
* `for (const i in w)` というfor in 文を使った際に、iにstringが帰ってくることを忘れており、
numberを返すべきところをstringで返してしまっていた。

### 線形探索を用いた方法

```javascript
const Solution = function(w) {
this.cum_sum_weights = [0]
let sum = 0
for (const weight_i of w) {
sum += weight_i
}
for (let i=0; i<w.length; i++) {
n const weight_i = w[i]
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

謎の n。これを置き直してもあまりいいことないでしょう。

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

wという変数が重み( weight )を表すことは自明ということですね。

this.cum_sum_weights.push(this.cum_sum_weights[Number(i)] + weight_i / sum)
}
};

Solution.prototype.pickIndex = function() {
const random_val = Math.random()
for (let i=1; i<=this.cum_sum_weights.length; i++) {
const cum_sum_weight_i = this.cum_sum_weights[i]
if (random_val <= cum_sum_weight_i) {
return i-1
}
}
};
```

### 二分探索を用いた方法

```javascript
const Solution = function(w) {
let sum = 0
for (const weight_i of w) {
sum += weight_i
}
this.cum_sum_weights = []
this.cum_sum_weights.push(0)
for (let i=0; i<w.length; i++) {
const weight_i = w[i]
const ith_cum_sum_weight = this.cum_sum_weights[i]
this.cum_sum_weights.push(ith_cum_sum_weight + weight_i / sum)
}
};

// 見つけたいターゲット:
// i-1番目の累積和 < random_value <= i番目の累積和となるiの値
// while文の引き継ぎの条件:
// left以上で、right以下であること。
Solution.prototype.pickIndex = function() {
const random_val = Math.random()
let left = 1
let right = this.cum_sum_weights.length - 1
while (left < right) {
const middle = Math.floor(left + (right - left) / 2)
if (random_val <= this.cum_sum_weights[middle]) {
right = middle
} else {
left = middle + 1
}
}
return left - 1
};
```


## STEP2

* 線形探索を用いた方法
* `this.cum_sum_weights` が使用する直前で宣言する形に変更。sumを求める際には使用しないため。

```javascript
const Solution = function(w) {
let sum = 0
for (const weight_i of w) {
sum += weight_i
}
this.cum_sum_weights = []
// 番兵として、0を入れておく。
this.cum_sum_weights.push(0)
for (let i=0; i<w.length; i++) {
const weight_i = w[i]
const ith_cum_sum_weight = this.cum_sum_weights[i]
this.cum_sum_weights.push(ith_cum_sum_weight + weight_i / sum)
}
};

Solution.prototype.pickIndex = function() {
const random_val = Math.random()
for (let i=1; i<=this.cum_sum_weights.length; i++) {
// ith cumulative sum of weight.
const ith_cum_sum_weight = this.cum_sum_weights[i]
if (random_val <= ith_cum_sum_weight) {
return i-1
}
}
};
```

## STEP3

* 二分探索を用いた方法でやってみる。

```javascript
const Solution = function(w) {
let sum = 0
for (const weight_i of w) {
sum += weight_i
}

this.cum_sum_weights = []
this.cum_sum_weights.push(0)
for (let i=0; i<w.length; i++) {
const ith_cum_sum_weight = this.cum_sum_weights[i]
this.cum_sum_weights.push(ith_cum_sum_weight + w[i] / sum)
}
};

// Target:
// i-1 < target <= i
// while statement condition:
// left <= ith index <= right
Solution.prototype.pickIndex = function() {
let left = 0
let right = this.cum_sum_weights.length - 1
const random_val = Math.random()
while (left < right) {
const middle = left + Math.floor((right - left) / 2)
if (random_val <= this.cum_sum_weights[middle]) {
right = middle
} else {
left = middle + 1
}
}
return left - 1
};
```

## 感想

* 二分探索において、ターゲットと引き継ぎ条件を考えることで、
left,rightの設定や等号の有無について迷わなくなった。

* 前のレビュー依頼から時間が空いてしまったので、もう一度習慣化する。
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

そうですね。1週間休んでも終わるのが1週間遅れるだけですが、頻度が半分になると時間が倍になりますからね。特に問題ないので進みましょう。

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

はい、自分ができるペースで、頻度を意識してコーディング練習会に臨もうと思います。


## その他の解法

* 二分探索のターゲットを変更する。
* 見つけたいターゲット: i-1番目の累積和 <= `random_value` < i番目の累積和となるiの値 (先ほどは、 i-1 < `random_value` <= iで等号の場所を変更した)
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

等号はこっちのほうが自然でしょうね。Math.random は 0 は入って 1 は入らないので。

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  • random_valueが1を含む場合には、<=としてあげないと、1が出た際のiが見つからなくなる.
  • random_valueが1を含まない場合には、 <=としても1がそもそも出ないとで等号が使われない。

と理解しました。

* while文の引き継ぎの条件: ターゲットはleft以上で、right以下であること。

```javascript
const Solution = function(w) {
let sum = 0
for (const weight_i of w) {
sum += weight_i
}

this.cum_sum_weights = []
this.cum_sum_weights.push(0)
for (let i=0; i<w.length; i++) {
const ith_cum_sum_weight = this.cum_sum_weights[i]
this.cum_sum_weights.push(ith_cum_sum_weight + w[i] / sum)
}
};
Solution.prototype.pickIndex = function() {
const random_val = Math.random()
let left = 1
let right = this.cum_sum_weights.length - 1
while (left < right) {
const middle = Math.floor(left + (right - left) / 2)
if (random_val < this.cum_sum_weights[middle]) {
right = middle
} else {
left = middle + 1
}
}
return left - 1
};
```

## レビューを受けて

### レビュー (1) 浮動小数点と整数の違い

```javascript
const Solution = function(w) {
let sum = 0
for (const weight_i of w) {
sum += weight_i
}

this.cum_sum_weights = []
this.cum_sum_weights.push(0)
for (let i=0; i<w.length; i++) {
const ith_cum_sum_weight = this.cum_sum_weights[i]
this.cum_sum_weights.push(ith_cum_sum_weight + w[i] / sum)
}
};

// Target:
// i-1 < target <= i
// while statement condition:
// left <= ith index <= right
Solution.prototype.pickIndex = function() {
let left = 0
let right = this.cum_sum_weights.length - 1
const random_val = Math.random()
while (left < right) {
const middle = left + Math.floor((right - left) / 2)
if (random_val <= this.cum_sum_weights[middle]) {
right = middle
} else {
left = middle + 1
}
}
return left - 1
};
```

* レビューコメントは、浮動小数点は副作用があるので、整数で解いた方が好み。
* 参考 : https://github.com/shintaroyoshida20/leetcode/pull/16#discussion_r2072320084

```javascript
const Solution = function(w) {
this.cum_sum_weights = [0]
let sum_weight = 0
for (let i = 0; i < w.length; i++) {
this.cum_sum_weights[i + 1] = this.cum_sum_weights[i] + w[i]
}
};

Solution.prototype.pickIndex = function() {
const size = this.cum_sum_weights.length
const sum = this.cum_sum_weights[size - 1]

// 0 から sum - 1の値をランダムに作成する。
const random_val = Math.floor(Math.random() * sum)

// i-1 <= random_val < iとなるiを探す。
// 言い換えると、random_valより大きいiで最小のiを探す。
for (let i = 1; i <= size; i++) {
const ith_cum_sum_weight = this.cum_sum_weights[i]
if (random_val < ith_cum_sum_weight) {
return i - 1
}
}
throw new Error("unexpected behavior is occurred.")
};
```