0
1

More than 1 year has passed since last update.

paizaラーニング レベルアップ問題集 二分探索メニュー JavaScript 効率よく盗もう

Posted at

効率よく盗もう (paizaランク A 相当)

方針

盗み出す財宝を適切に選んだ結果、平均価値が最大でいくつになるかを答えてください。

以上の問題文より、平均価値で二分探索ができそうです。


k個の財宝の平均価値がx以上なら、それ以下の平均価値y(y<=x)も、もちろん達成できます。なので、二分探索ができます。

k個の平均価値は問題文の定義より(v_1 + v_2 + ... + v_k) / (w_1 + w_2 + ... + w_k)で求められます。

平均価値がx以上になるなら、
(v_1 + v_2 + ... + v_k) / (w_1 + w_2 + ... + w_k) ≧ x
と表現できます。

このままだと考えにくいので、これを、財宝1つ1つの表現にできないか考えます。

財宝1つ1つの表現を目指して、式変形をします。
(v_1 + v_2 + ... + v_k) - x * (w_1 + w_2 + ... + w_k) ≧ 0
(v_1 - x*w_1) + (v_2 - x*w_2) + ... + (v_k - x*w_k) ≧ 0

こうすることで、財宝1つ1つについて、適切に選ぶことができます。
すなわち、n個財宝1つ1つについて(v_i - x*w_i)を求め、この値が大きい財宝からk個選んで、足し合わせたときに、0以上であれば、
n個の財宝から適切にk個選んだ最大の平均価値はx以上、ということです。


二分探索で、探索範囲[left, right)のとき、平均価値が mid = (left+right)/2 以上となるように 財宝を k 個選ぶことができるかどうかをチェックします。

平均価値が mid 以上となるように財宝を選べるなら、探索範囲の左端を left から mid にします。逆に選べないならば、探索範囲の右端を right から mid にします。

解き方の流れまとめ

平均価値の探索範囲[left,right]で、mid=(left+right)/2とした時、
k個の財宝を適切に選んで、最大にした平均価値が、mid以上になるか調べます。

式にすると、
(v_1 + v_2 + ... + v_k) / (w_1 + w_2 + ... + w_k) ≧ mid
が成り立つか調べるが、このままだと、k個の財宝を適切に選んで最大の平均価値にできないので、式変形して財宝1つ1つの表現にします。
(v_1 - mid*w_1) + (v_2 - mid*w_2) + ... + (v_k - mid*w_k) ≧ 0
こうすると、左辺が財宝1つ1つから求められる値になり、大きい順に並べ替えができ、大きい順に選ぶことができるようになり、適切に選んで最大の平均価値にできるようになります。

"その左辺の最大値が0以上"ならば、
"k個の財宝を適切に選んで、最大にした平均価値が、mid以上"なので、
mid以下の平均価値も、もちろん成立します。

よって、範囲の左端leftがmidになります。

また、成立しなかったら、mid以上に高い平均価値はもちろん成立しないので、右端のrightをmidにします。

以上を50回以上繰り返せば、求まります。

さらに流れまとめ

平均価値を二分探索すればよい。
平均価値の範囲[left, right]の中間をmidとした時、k個の財宝を適切に選んだ最大の平均価値を探索する。
平均価値がmid以上なら、mid以下はもちろん成り立つので、範囲の左端leftをmidに狭める。
平均価値がmid以下なら、mid以上はもちろん成り立たないので、範囲の右端rightをmidに狭める。
これを繰り返して、平均価値の境目を見つける。

ここでポイントは、k個の財宝を適切に選ぶ方法。
財宝1つ1つに何か値があって、それが大きい順にk個選べればよい。
与えられた定義(k 個の財宝の価値の和) ÷ (k 個の財宝の重さの和)がmid以上か判定する式がある。
(v_1 + v_2 + ... + v_k) / (w_1 + w_2 + ... + w_k) ≧ mid
これを変形すると、
(v_1 - mid*w_1) + (v_2 - mid*w_2) + ... + (v_k - mid*w_k) ≧ 0
が得られる。この左辺は財宝1つ1つの値の形になっているので、全部のn個の財宝の値を求めて大きい順にソートでき、大きい順に選ぶことができる。

解答コード例(Python3の場合参考)

const fs = require("fs");
const input = fs.readFileSync("/dev/stdin", "utf-8").trim();
const lines = input.split("\n");

const [n, k] = lines[0].split(" ").map(Number);
const W = lines[1].split(" ").map(Number);
const V = lines[2].split(" ").map(Number);

//平均価値について二分探索する
//平均価値の範囲は1 ≦ W_i ≦ 5,000 (1 ≦ i ≦ n), 1 ≦ V_i ≦ 5,000 (1 ≦ i ≦ n)より
let [left, right] = [0, 5001];
for (let i = 0; i < 100; i++) { //万全を期すため100回
  /* 解説より、k個の財宝の最大の平均価値がmid以上かは、
  k個の財宝のtmp_iの和が0以上か、で判定できる */
  let mid = (left + right) / 2;
  let tmp = Array(n).fill(0);//ここに各財宝の(v_i - x*w_i)を入れる
  for (let i = 0; i <= n; i++) {
    tmp[i] = V[i] - W[i] * mid;//tmp_iとする。
  }
  //財宝のtmp_iの値を大きい順にソート
  tmp.sort((a, b) => b - a);
  
  //k個の財宝のtmp_iの和が0以上か=k個の財宝の最大の平均価値がmid以上か
  if (tmp.slice(0, k).reduce((a, b) => a + b) >= 0) {//最大の平均価値がmid以上なら
    left = mid;//mid以下に最大の平均価値はない
  } else { //最大の平均価値がmid以下なら
    right = mid; //mid以上に最大の平均価値はない
  }
  
}
console.log(left);
0
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
1