Function nekolib::algo::extremum::extremum

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

三分探索で極値を探す。

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

Requirements

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

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

Implementation notes

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

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

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

Complexity

F0=1F_0 = 1, F1=2F_1 = 2, Fi=Fi1+Fi2F_i = F_{i-1} + F_{i-2} (i2i \ge 2) で定義される数列 {Fk}\{F_k\} を考える。 区間幅 nn がある kk に対して nFkn \le F_k と抑えられるとき、ff の呼び出しを高々 kk 回、関数値同士の比較を高々 k1k-1 回行う。 なお、この kklogφ(n)\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);
}