雑記:AtCoderでF#を試そうとして色々躓いた


概要

関数型言語に興味を持ち
Copilotに色々聞きながらF#で↓を解こうとしたら色々躓き学びを得た話

試したこと

#[allow(unused_imports)]
use proconio::{fastout, input, marker::Chars};

pub fn bin_sch<F>(edge_l: usize, edge_r: usize, mut judge: F) -> isize
where
    F: FnMut(usize) -> bool,
{
    let mut ok: isize = edge_l as isize - 1isize;
    let mut ng: isize = (edge_r + 1) as isize;
    while ng - ok > 1 {
        let mid: isize = ((ok + ng) / 2) as isize;
        if judge(mid as usize) {
            ok = mid;
        } else {
            ng = mid;
        }
    }
    ok
}

#[fastout]
fn main() {
    input! {
        n: usize,
        mut l: [usize; n],
    }
    l.sort();
    let mut ans = 0;
    for i in 0..n {
        for j in i + 1..n {
            let a = l[i];
            let b = l[j];
            // c > a - b この時点で a <= b なので無視していい
            // c > b - a この時点で c >= b なので無視していい
            // c < a + b
            let k = bin_sch(0, n - 1, |mid| l[mid] < a + b);
            if k > j as isize {
                ans += k as usize - j;
            }
        }
    }
    println!("{ans}");
}

Rustで上記のように解けるので、F#に変換しようとした

let n = stdin.ReadLine() |> int
let l = stdin.ReadLine().Split() |> Array.toList |> List.map int |> List.sort

let binarySearch leftEdge rightEdge judge =
    let rec binarySearchHelper leftOuterEdge rightOuterEdge judge =
        if rightOuterEdge - leftOuterEdge > 1 then 
            let mid = (leftOuterEdge + rightOuterEdge) / 2
            if judge mid then
                binarySearchHelper mid rightOuterEdge judge
            else
                binarySearchHelper leftOuterEdge mid judge
        else
            leftOuterEdge
    binarySearchHelper (leftEdge-1) (rightEdge+1) judge


let rec solveFirst l cnt =
    let rec solveSecond l a cnt =
        match l with
        | [] -> cnt
        | b :: tail ->
            let judge mid = tail.[mid] < a + b
            let searchRes = binarySearch 0 ((List.length tail) - 1) judge
            let cnt = cnt + if searchRes >= 0 then (searchRes + 1) else 0
            solveSecond tail a cnt

    match l with
    | [] -> cnt
    | a :: tail ->
        let cnt = solveSecond tail a cnt
        solveFirst tail cnt

let ans = solveFirst l 0
printfn "%d" ans

上記のようになったが、TLE

学んだこと

関数の内部で関数を定義

例えば↓の二分探索関数では、関数の内部で関数を定義している。

let binarySearch leftEdge rightEdge judge =
    let rec binarySearchHelper leftOuterEdge rightOuterEdge judge =
        if rightOuterEdge - leftOuterEdge > 1 then 
            let mid = (leftOuterEdge + rightOuterEdge) / 2
            if judge mid then
                binarySearchHelper mid rightOuterEdge judge
            else
                binarySearchHelper leftOuterEdge mid judge
        else
            leftOuterEdge
    binarySearchHelper (leftEdge-1) (rightEdge+1) judge

高階関数(Higher-Order Functions)と呼ばれる概念の一部らしい。
関数型言語において関数は値と同様に扱える?
要調査。

普段AtCoderではRustを使用しており、
そこでは下記のような自作二分探索関数を使用している。
先ほどのF#の二分探索はこれを元にしている。

fn bin_sch<F>(edge_l: usize, edge_r: usize, mut judge: F) -> isize
where
    F: FnMut(usize) -> bool,
{
    let mut ok: isize = edge_l as isize - 1isize;
    let mut ng: isize = (edge_r + 1) as isize;
    while ng - ok > 1 {
        let mid: isize = ((ok + ng) / 2) as isize;
        if judge(mid as usize) {
            ok = mid;
        } else {
            ng = mid;
        }
    }
    ok
}

リストのパターンマッチ

F#以外の関数型言語も同様かはわからないが、
リストに対して下記のようなパターンマッチができる

// l はリスト
match l with
| [] -> ()
| head :: tail -> // headはlの先頭、tailはlからheadを除いた残り

これを使ってリスト全体に対して繰り返し操作を行う場合、
例えば下記のような再帰関数でできる

let sum list =
    let rec sumHelper list sum =
        match list with
        | [] -> sum
        | head :: tail ->
            head + sumHelper tail sum
    sumHelper list 0

※度々使用している〇〇Helperという関数名は私が勝手にそうしているだけで、実際にそのように書かれているとは限らない。

List.lengthの計算量について

そもそも大前提としてListは値と次の値へのポインタを持つデータ構造である。
Listはその構造上、特定の位置のデータを取得するためには先頭からポインタを辿る必要がある。
対して、配列やベクタの場合はメモリ上の連続したアドレスにデータを格納する。
メモリ上のデータの開始位置さえわかればそこからの差分を指定することで配列上の特定の位置のデータを取得できる。
つまりlist[i]の計算量はO(i)、array[i]の計算量はO(1)である。

配列やベクタの場合は連続したデータの長さを保持しているため、array.length()の計算量はO(1)。
対してListはそうではないため先頭から末尾までの長さを捜査する必要がある。
list.length()の計算量はO(list.length())

TLEの原因は、↓の処理を何度も呼び出していたから

let searchRes = binarySearch 0 ((List.length tail) - 1) judge

半分くらいRustのベクタみたいなノリで使っていたからListの構造を忘れていた。
基本的にArrayを使うようにすべきかなと思った。

おまけ

最終的にこうなった

let n = stdin.ReadLine() |> int
let l = stdin.ReadLine().Split() |> Array.map int |> Array.sort

let binarySearch leftEdge rightEdge judge =
    let rec binarySearchHelper leftOuterEdge rightOuterEdge judge =
        if rightOuterEdge - leftOuterEdge > 1 then 
            let mid = (leftOuterEdge + rightOuterEdge) / 2
            if judge mid then
                binarySearchHelper mid rightOuterEdge judge
            else
                binarySearchHelper leftOuterEdge mid judge
        else
            leftOuterEdge
    binarySearchHelper (leftEdge-1) (rightEdge+1) judge


let solve (array: int array) =
    let rec solveHelper (array: int array) aIdx cnt =
        if aIdx >= Array.length array then
            cnt
        else
            let rec solveHelperSecond (array: int array) a bIdx cnt =
                if bIdx >= Array.length array then
                    cnt
                else
                    let b = array.[bIdx]
                    let judge mid = array.[mid] < a + b
                    let cIdxMax = binarySearch 0 (Array.length array - 1) judge
                    let cnt = cnt + if cIdxMax > bIdx then cIdxMax - bIdx else 0
                    solveHelperSecond array a (bIdx + 1) cnt
            let a = array.[aIdx]
            let cnt = solveHelperSecond array a (aIdx + 1) cnt
            solveHelper array (aIdx + 1) cnt
    solveHelper array 0 0


let ans = solve l
printfn "%d" ans

この記事が気に入ったらサポートをしてみませんか?