Немного сахара в комбинаторике

в 22:37, , рубрики: D, dlang, ranges

Доброго времени суток!

Каждый уважающий себя программист знает, что глубокие вложенности — плохой стиль. Но есть алгоритмы, которые реализуются каскадом вложенных циклов (3 и более). В этой статье я хочу рассказать, как можно справиться с проблемой вложенных циклов при переборе комбинаций на любимом языке D.

Рассмотрим самую простую ситуацию.

Дано: N элементов
Нужно: перебрать все наборы по K элементов без повторений

В комбинаторике это называется размещением из N по K.

Стандартная библиотека предоставляет функцию std.algorithm.permutations, но это немного другое — перестановка по русски.

Реализуем простой перебор N по K:

import std.range;
import std.algorithm;

auto partialPermutation(R)( R r, size_t k )
    if( isInputRange!R )
{
    static struct Result
    {
        R[] r, orig; // храним текущее состояние и исходное

        this( R[] r ) { this.r = r; orig = r.dup; }

        bool empty() @property { return r[0].empty; }

        void popFront()
        {
            foreach_reverse( ref x; r )
            {
                x.popFront();
                if( !x.empty ) break;
            }

            // восстанавливаем пустые диапазоны
            foreach( i, ref x; r[1..$] )
            {
                if( x.empty )
                    x = orig[i+1];
            }
        }

        auto front() @property { return r.map!(a=>a.front); }
    }

    auto rr = new R[](k);
    rr[] = r;

    return Result( rr );
}

Теперь мы можем использовать эту функцию для перебора всех возможных комбинаций:

foreach( v; partialPermutation( iota(6), 3 ) )
    writefln( "%d %d %d", v[0], v[1], v[2] );

При такой реализации встречаются повторения, это достаточно просто лечится:

auto uniqPartialPermutation(R)( R r, size_t k )
    if( isInputRange!R )
{
    bool noDups(T)( T v ) pure
    {
        foreach( i; 0 .. v.length )
            if( v[i+1..$].canFind( v[i] ) ) return false;
        return true;
    }
    return partialPermutation(r,k).filter!(a=>noDups(a));
}

Рассмотрим более сложный пример.

Дано: N различных диапазонов различных типов данных
Нужно: перебрать наборы из всех комбинаций этих элементов

Язык D предоставляет нам возможность внести в рабочий код лишь пару маленьких изменений
для получения желаемого результата:

auto combinations(R...)( R rngs )
    if( allSatisfy!(isInputRange,R) )
{
    static struct Result
    {
        R r, orig; // храним текущее состояние и исходное

        this( R r ) { this.r = r; orig = r; }

        bool empty() @property { return r[0].empty; }

        void popFront()
        {
            foreach_reverse( ref x; r )
            {
                x.popFront();
                if( !x.empty ) break;
            }

            foreach( i, ref x; r[1..$] )
            {
                if( x.empty )
                    x = orig[i+1];
            }
        }

        auto front() @property { return getFronts( r ); }

    }

    return Result( rngs );
}

Вспомогательная функция getFronts возвращает кортеж первых элементов:

auto getFronts(R...)( R r )
    if( allSatisfy!(isInputRange,R) )
{
    static if( R.length == 1 ) return tuple( r[0].front );
    else static if( R.length > 1 )
        return tuple( getFronts(r[0]).expand, getFronts(r[1..$]).expand );
    else static assert(0, "no ranges - no fronts" );
}

Теперь можно использовать так:

foreach( a,b,c; combinations(iota(3),["yes","no"],"xyz"))
    writeln( a,"[",b,"]",c );

Для полноты картины расширим функциональность, чтобы наша функция combinations
могла принимать не только диапазоны, но и кортежи диапазонов. Для этого переименуем её
в result и поместим внутри функции с таким же именем, но без ограничения сигнатуры и
добавим разворачивание вложенных кортежей:

auto combinations(T...)( T tpls )
{
    auto result(R...)( R rrr )
        if( allSatisfy!(isInputRange,R) )
    {
        ...
        старая функция combinations
        ...
    }

    auto wrapTuples(X...)( X t ) pure
    {
        static if( X.length == 1 )
        {
            static if( isTuple!(X[0]) )
                return wrapTuples( t[0].expand );
            else
                return tuple( t[0] );
        }
        else static if( X.length > 1 )
            return tuple( wrapTuples(t[0]).expand, wrapTuples(t[1..$]).expand );
    }

    return result( wrapTuples(tpls).expand );
}
auto tt = tuple( hCube!3(iota(2)), iota(3) );
foreach( a, b, c, d, e, f; combinations( tt, ["yes", "no", "maybe"], "abc" ) )
    writefln( "%s.%s.%s(%s) %s %s", a, b, c, d, e, f );

где функция hCube просто дублирует диапазон заданное количество раз:

auto hCube(size_t N,R)( R r )
{
    static if( N == 0 ) return tuple();
    static if( N == 1 ) return tuple(r);
    else return tuple( r, hCube!(N-1)(r).expand );
}

На этом всё. Стоит держать в голове один момент: уменьшение количества foreach не меняет сложность алгоритма в этой ситуации. Просто ещё немного сахара.

Автор: deviator

Источник


* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js