Разработка на коленке

"тут должна быть красивая цитата о программировании"

Жадный алгоритм на OCaml

2016-07-20 13:12

Записался слушателем курса Algorithmic Toolbox (бесплатно халявный вариант).

Сегодня утром слушал про жадный алгоритм для поиска групп детей, у которых разница в возрасте не должна превышать год. Поскольку халявщикам нельзя отправлять свои решения, а платить за этот курс я не планирую, то написал свой жадный алгоритм на OCaml, вместо предлагаемых в курсе питона и С++.

Условия под которые писался этот код:

  • данные вводятся из терминала;
  • возраст даётся в месяцах, чтобы не возиться с преобразованием и обойтись одной функцией ввода;
  • рекурсия должна быть хвостовой.
let scan_int () = Scanf.scanf " %d" (fun x -> x)


let rec scan_ages = function (* Возраст в месяцах *)
    | 0 -> []
    | n -> scan_int () :: scan_ages (n - 1)


let print_groups lst =
    print_endline "Groups: ";

    List.iter (fun sub_list ->
        sub_list |> List.map string_of_int
                 |> String.concat " "
                 |> print_endline
    ) lst


let find_groups lst =
    let max_diff = 12 in
    let rec _find lst first group total =
        match lst with
        | [] -> (
            match group with
            | [] -> total
            | _ -> group :: total)
        | hd :: tl -> (
            match compare (hd - first) max_diff with
            | 0 | -1 -> _find tl first (hd :: group) total
            | 1 -> _find tl hd [hd] (group :: total)
            | _ -> invalid_arg "Wrong answer from compare!")
    in

    match List.sort compare lst with
    | [] -> []
    | hd :: tl -> _find tl hd [hd] []


let () =
    scan_int () |> scan_ages
                |> List.sort compare
                |> find_groups
                |> print_groups;

Тестирование

Потом, следуя рекомендациям курса, я начал писать тесты. Первым делом мне понадобился способ создавать тестовую выборку.

let make_test () =
    let rec gen n =
        match compare n 0 with
        | 0 | 1 -> (Random.int 115) + 6 :: gen (n - 1) (* Возраст от 6 месяцев до 10 лет *)
        | _ -> []
    in
    gen ((Random.int 100) + 1) (* От одного до 100 детей в группе *)

Помимо тестовых данных нужна функция проверки равенства двух списков групп:

let compare_groups g1 g2 =
    let sorted lst =
        List.map (List.sort compare) lst |> List.sort compare
    in
    let even = match compare (List.length g1) (List.length g2) with
    | 0 -> sorted(g1) = sorted(g2)
    | _ -> false
    in
    even

И ещё нужна реализация алгоритма разбиения каким-нибудь простым способом. Для этого я выбрал тот же жадный алгоритм, но в императивном стиле, потому что по поводу правильности жадного алгоритма у меня сомнений не было, а вот функциональная реализация вызывала подозрения.

let greedy_iter lst =
    let sorted_lst = List.sort compare lst in
    let groups = ref [] in
    let i = ref 0 in
    while !i < List.length sorted_lst do
        let first = List.nth sorted_lst !i in
        let group = ref [first] in
        i := !i + 1;
        while !i < List.length sorted_lst && compare 12 ((List.nth sorted_lst !i) - first) >= 0 do
            group := (List.nth sorted_lst !i) :: !group;
            i := !i +1;
        done;
        groups := !group :: !groups
    done;
    !groups

И код запуска теста

let test () =
    for i = 0 to 50 do
        let ages = make_test () in
        let g1 = greedy_iter ages in
        let g2 = find_groups ages in
        if compare_groups g1 g2 then
            print_endline "Ok"
        else begin
            print_endline "Error";
            print_endline "Ages: ";
            ages |> List.map string_of_int |> String.concat " " |> print_endline;
            print_endline "Imperative:";
            print_groups g1;
            print_endline "----";
            print_endline "Functional:";
            print_groups g2;
            print_endline "";
        end
    done


let () =
    test();

Работа обеих реализаций оказалась одинаковой.

Заключение

Писать код на OCaml становится всё проще. Эта реализация алгоритма наверняка содержит недостатки, которые можно убрать, но сейчас в этом нет смысла. Мне нужно нарабатывать практику работы с языком.

На будущее: нужно посмотреть, как принято тестировать код на OCaml.