Function nekolib::algo::extremum

source ·
pub fn extremum<T: Ord>(
    _: Range<usize>,
    f: impl FnMut(usize) -> T
) -> (usize, T)
Expand description

三分探索で極値を探す。

離散値の区間 $[l, r)$ において、以下を満たす $i$ および $f(i)$ を返す。 $$ f(i-1) < f(i)\text{ and } f(i) > f(i+1). $$

Requirements

凸である。すなわち、ある $i$ が存在して以下の二つが成り立つ。

  • ${}^\forall j \in [l, i)$ に対して $f(j) < f(j+1)$。
  • ${}^\forall j \in [i, r-1)$ に対して $f(j) > f(j+1)$。

Implementation notes

連続値の場合における黄金比分割のように、Fibonacci 数列に基づいて区間を分割していくため、関数値を使い回すことができる。

関数 $f$ の呼び出し回数を、区間を三等分する素直な実装と比較する。 三等分の実装では $2\cdot\log_{3/2}(r-l)+O(1)$ 回(係数の $2$ に注意)であり、こちらは $\log_{\varphi}(r-l)+O(1)$ 回である。 ただし $\varphi$ は黄金比 $(1+\sqrt{5})/2 = 1.618\dots$ である。 $$ \sqrt{3/2} < 1.225 < 1.618 < \varphi $$ であり、$\log$ の底は大きい方がうれしいので、こちらの実装の方が定数倍が軽い。

コード長はやや長くなるものの、単純な例での実測では三等分のものよりわずかに高速であった。

Complexity

$F_0 = 1$, $F_1 = 2$, $F_i = F_{i-1} + F_{i-2}$ ($i \ge 2$) で定義される数列 $\{F_k\}$ を考える。 区間幅 $n$ がある $k$ に対して $n \le F_k$ と抑えられるとき、$f$ の呼び出しを高々 $k$ 回、関数値同士の比較を高々 $k-1$ 回行う。 なお、この $k$ は $\lceil\log_{\varphi}(n)\rceil$ で抑えられる。

Suggestions

引数は Range<usize> を渡すことにしているものの、実際には RangeBounds<I: {integer}> を渡せるようにする方がよさそう? ただし、両端とも Unbounded であっては困りそう(特に多倍長を視野に入れる場合?)。 多倍長だと Copy がないから、計算結果自体を使い回せても .clone() でつらい?

Examples

use nekolib::algo::extremum;

let buf = [1, 3, 4, 6, 5, 2, 0, 1, 3];
//         <------ f ------>
//                  <------ g ------>

let f = |i: usize| buf[i] * buf[i];
assert_eq!(extremum(0..6, f), (3_usize, 36));
let g = |i: usize| -buf[i];
assert_eq!(extremum(3..8, g), (6_usize, 0));
use nekolib::algo::extremum;

let n = 1500;
for k in 0..n {
    let mut count = 0;
    let f = |i| { count += 1; -(i as i32 - k as i32).abs() };
    assert_eq!(extremum(0..n, f), (k, 0));
    assert!(count <= 15);
}