Итерация през различно подмножество с размер k

Имам масив от n цели числа (не непременно различни!) и бих искал да обходя всички подмножества с размер k. Въпреки това бих искал да изключа всички дублиращи се подгрупи.

e.g.

array = {1,2,2,3,3,3,3}, n = 7, k = 2

тогава подгрупите, които искам да повторя (всеки веднъж), са:

{1,2},{1,3},{2,2},{2,3},{3,3}

Какъв е ефективен алгоритъм за извършване на това? Дали рекурсивният подход е най-ефективният/елегантен?

В случай, че имате специфичен за езика отговор, аз използвам C++.


person Alex    schedule 28.05.2015    source източник
comment
Защо не можете първо да унифицирате оригиналния масив и след това просто да използвате вашето стандартно решение, за да изброите всички подмножества?   -  person Kerrek SB    schedule 28.05.2015
comment
@KerrekSB Това ще отпадне {2,2} и {3,3}.   -  person Barry    schedule 28.05.2015
comment
@KerrekSB няма ли да пропусна {2,2} и {3,3}? РЕДАКТИРАНЕ: о, ти беше по-бърз. Също и противници. какво не е наред с въпроса ми?   -  person Alex    schedule 28.05.2015
comment
@Alex: Разбирам, добра точка.   -  person Kerrek SB    schedule 28.05.2015
comment
Хм, извън темата, принадлежи към CS?   -  person Kerrek SB    schedule 28.05.2015
comment
@KerrekSB Съжалявам, не знаех за този уебсайт. Ако никой модератор не премести този въпрос, ще го изтрия и ще го публикувам отново там.   -  person Alex    schedule 28.05.2015
comment
Не мисля, че е напълно извън темата, защото може да е покрито частично или изцяло от алгоритъм на стандартна библиотека. напр. за малко по-различен въпрос std::next_permutation можеше да е част от отговор. Отговорът на този въпрос включва броене по ограничен начин (само увеличаващи се последователности от цифри), но не мисля, че стандартната библиотека помага с това.   -  person Cheers and hth. - Alf    schedule 28.05.2015
comment
Може би дубликат на това? stackoverflow.com/questions/127704/   -  person Barry    schedule 28.05.2015


Отговори (4)


Същият (или почти същият) алгоритъм, който се използва за генериране на комбинации от набор от уникални стойности в лексикографски ред, може да се използва за генериране на комбинации от множество в лексикографски ред. Правейки го по този начин, избягвате необходимостта от дедупликация, което е ужасно скъпо, и също така избягвате необходимостта от поддържане на всички генерирани комбинации. Това изисква първоначалният списък със стойности да бъде сортиран.

Следната проста реализация намира следващата k-комбинация от множество от n стойности за средно (и в най-лошия случай) време O(n) . Той очаква два диапазона: първият диапазон е сортирана k-комбинация, а вторият диапазон е сортираният множествен набор. (Ако някой от диапазоните не е сортиран или стойностите в първия диапазон не съставляват под(мулти)набор от втория диапазон, тогава поведението е недефинирано; не се правят проверки за разумност.)

Всъщност се използва само крайният итератор от втория диапазон, но си помислих, че това прави конвенцията за извикване малко странна.

template<typename BidiIter, typename CBidiIter,
         typename Compare = std::less<typename BidiIter::value_type>>
int next_comb(BidiIter first, BidiIter last,
              CBidiIter /* first_value */, CBidiIter last_value,
              Compare comp=Compare()) {
  /* 1. Find the rightmost value which could be advanced, if any */
  auto p = last;
  while (p != first && !comp(*(p - 1), *--last_value)) --p;
  if (p == first) return false;
  /* 2. Find the smallest value which is greater than the selected value */
  for (--p; comp(*p, *(last_value - 1)); --last_value) { }
  /* 3. Overwrite the suffix of the subset with the lexicographically smallest
   *    sequence starting with the new value */
  while (p != last) *p++ = *last_value++;
  return true;
}

Трябва да е ясно, че комбинираните стъпки 1 и 2 правят най-много O(n) сравнения, тъй като всяка от n стойностите се използва в най-много едно сравнение. Стъпка 3 копира най-много O(k) стойности и знаем, че kn.

Това може да се подобри до O(k) в случай, че не се повтарят стойности, като се поддържа текущата комбинация като контейнер от итератори в списъка със стойности, а не като действителни стойности. Това също ще избегне копирането на стойности, с цената на допълнителни дереференции. Ако в допълнение кешираме функцията, която свързва всеки итератор на стойност с итератор към първия екземпляр на следващата най-голяма стойност, можем да елиминираме стъпка 2 и да намалим алгоритъма до O(k) дори за повтарящи се стойности. Това може да си струва, ако има голям брой повторения и сравненията са скъпи.

Ето един прост пример за използване:

std::vector<int> values = {1,2,2,3,3,3,3};
/* Since that's sorted, the first subset is just the first k values */
const int k = 2;
std::vector<int> subset{values.cbegin(), values.cbegin() + k};

/* Print each combination */
do {
  for (auto const& v : subset) std::cout << v << ' ';
  std::cout << '\n';
} while (next_comb(subset.begin(),  subset.end(),
                   values.cbegin(), values.cend()));

На живо на coliru

person rici    schedule 28.05.2015
comment
Благодаря. В крайна сметка кеширах индексите на следващите по големина цели числа, както предложихте. Харесва ми, че това не зависи от „набор“. - person Alex; 29.05.2015

Харесвам превъртане на битове за този проблем. Разбира се, ограничава ви само до 32 елемента във вашия вектор, но все пак е страхотно.

Първо, при дадена битова маска, определете следващата пермутация на битовата маска (източник):

uint32_t next(uint32_t v) {
    uint32_t t = v | (v - 1);
    return (t + 1) | (((~t & -~t) - 1) >> (__builtin_ctz(v) + 1));  
}

След това, като имате vector и битова маска, дайте нов vector въз основа на тази маска:

std::vector<int> filter(const std::vector<int>& v, uint32_t mask) {
    std::vector<int> res;
    while (mask) {
        res.push_back(v[__builtin_ctz(mask)]);
        mask &= mask - 1;
    }   
    return res;
}

И с това имаме нужда само от цикъл:

std::set<std::vector<int>> get_subsets(const std::vector<int>& arr, uint32_t k) {   
    std::set<std::vector<int>> s;
    uint32_t max = (1 << arr.size());
    for (uint32_t v = (1 << k) - 1; v < max; v = next(v)) {
        s.insert(filter(arr, v));
    }
    return s;
}

int main()
{
    auto s = get_subsets({1, 2, 2, 3, 3, 3, 3}, 2);
    std::cout << s.size() << std::endl; // prints 5
}
person Barry    schedule 28.05.2015
comment
Вмъкнете коментар относно използването на битова маска за бързо повторение, но след това все пак залепване на резултатите в set<vector> тук. - person Barry; 28.05.2015
comment
Доста готино, изглежда работи! И без това не мисля, че имам нужда от повече от 32 елемента. Предполагам, че това изисква моят масив да бъде сортиран предварително (в случай че не е)? - person Alex; 28.05.2015
comment
@Alex Er, предполагам, че логиката за дедупиране е съвсем правилна. Както и да е, единственото хубаво нещо на това решение е, че е страхотно. Определено можете да се справите много по-добре. - person Barry; 28.05.2015

Основната идея на това решение е функция като next_permutation, но която генерира следващата възходяща последователност от "цифри". Тук се нарича ascend_ordered.

template< class It >
auto ascend_ordered( const int n_digits, const It begin, const It end )
    -> bool
{
    using R_it = reverse_iterator< It >;
    const R_it r_begin  = R_it( end );
    const R_it r_end    = R_it( begin );

    int max_digit = n_digits - 1;
    for( R_it it = r_begin ; it != r_end; ++it )
    {
        if( *it < max_digit )
        {
            ++*it;
            const int n_further_items = it - r_begin;
            for( It it2 = end - n_further_items; it2 != end; ++it2 )
            {
                *it2 = *(it2 - 1) + 1;
            }
            return true;
        }
        --max_digit;
    }
    return false;
}

Основна програма за конкретния случай:

auto main() -> int
{
    vector<int> a = {1,2,2,3,3,3,3};
    assert( is_sorted( begin( a ), end( a ) ) );
    const int k = 2;
    const int n = a.size();
    vector<int> indices( k );
    iota( indices.begin(), indices.end(), 0 );      // Fill with 0, 1, 2 ...
    set<vector<int>> encountered;
    for( ;; )
    {
        vector<int> current;
        for( int const i : indices ) { current.push_back( a[i] ); }
        if( encountered.count( current ) == 0 )
        {
            cout << "Indices " << indices << " -> values " << current << endl;
            encountered.insert( current );
        }
        if( not ascend_ordered( n, begin( indices ), end( indices ) ) )
        {
            break;
        }
    }
}

Поддръжката включва и вход/изход:

#include <algorithm>
using std::is_sorted;

#include <assert.h>

#include <iterator>
using std::reverse_iterator;

#include <iostream>
using std::ostream; using std::cout; using std::endl;

#include <numeric>
using std::iota;

#include <set>
using std::set;

#include <utility>
using std::begin; using std::end;

#include <vector>
using std::vector;

template< class Container, class Enable_if = typename Container::value_type >
auto operator<<( ostream& stream, const Container& c )
    -> ostream&
{
    stream << "{";
    int n_items_outputted = 0;
    for( const int x : c )
    {
        if( n_items_outputted >= 1 ) { stream << ", "; }
        stream << x;
        ++n_items_outputted;
    }
    stream << "}";
    return stream;
}
person Cheers and hth. - Alf    schedule 28.05.2015
comment
За {1,2,2,3,3,3,4} и k=3 той генерира {1,2,3} два пъти. - person Alex; 28.05.2015
comment
Благодаря! и съжалявам, че беше грешка. Погрешно си бях представял, че наборите винаги ще се генерират във възходящ ред, но това е вярно само за индексите... Поправено чрез проследяване на всички срещнати набори. - person Cheers and hth. - Alf; 28.05.2015
comment
Също така е коригиран проблем със заглавките: std::iota компилиран с g++ чрез включване само на <algorithm>, но Visual C++ очевидно следва стандарта по-точно тук, така че е необходимо да се включи <numeric>. - person Cheers and hth. - Alf; 28.05.2015

За разлика от предишния отговор, това не е толкова ефективно и не прави нищо толкова фантастично, колкото многото въртене на битове. Това обаче не ограничава размера на вашия масив или размера на подмножеството.

Това решение използва std::next_permutation за генериране на комбинациите и се възползва от свойството за уникалност на std::set.

#include <algorithm>
#include <vector>
#include <set>
#include <iostream>
#include <iterator>

using namespace std;

std::set<std::vector<int>> getSubsets(const std::vector<int>& vect, size_t numToChoose)
{
    std::set<std::vector<int>> returnVal;
    // return the whole thing if we want to
    // choose everything 
    if (numToChoose >= vect.size())
    {
        returnVal.insert(vect);
        return returnVal;
    }

    // set up bool vector for combination processing
    std::vector<bool> bVect(vect.size() - numToChoose, false);

    // stick the true values at the end of the vector
    bVect.resize(bVect.size() + numToChoose, true); 

    // select where the ones are set in the bool vector and populate
    // the combination vector
    do
    {
        std::vector<int> combination;
        for (size_t i = 0; i < bVect.size() && combination.size() <= numToChoose; ++i)
        {
            if (bVect[i])
                combination.push_back(vect[i]);
        }
        // sort the combinations
        std::sort(combination.begin(), combination.end());

        // insert this new combination in the set
        returnVal.insert(combination);
    } while (next_permutation(bVect.begin(), bVect.end()));
    return returnVal;
}

int main()
{
    std::vector<int> myVect = {1,2,2,3,3,3,3};

    // number to select
    size_t numToSelect = 3;

    // get the subsets
    std::set<std::vector<int>> subSets = getSubsets(myVect, numToSelect);

    // output the results
    for_each(subSets.begin(), subSets.end(), [] (const vector<int>& v) 
    { cout << "subset "; copy(v.begin(), v.end(), ostream_iterator<int>(cout, " ")); cout << "\n"; });
}

Пример на живо: http://coliru.stacked-crooked.com/a/beb800809d78db1a

По принцип ние настройваме bool вектор и попълваме вектор със стойностите, които съответстват на позицията на true елемента в bool вектора. След това сортираме и вмъкваме това в набор. std::next_permutation разбърква true стойностите в bool масива и ние просто повтаряме.

Разбира се, не толкова сложен и повече от вероятно по-бавен от предишния отговор, но трябва да свърши работата.

person PaulMcKenzie    schedule 28.05.2015