Перечислите все возможные комбинации k целых чисел между 1... n (n выберите k)

Исходя из каких-либо особых причин, я решил искать алгоритм, который дает все возможные варианты k целых чисел между 1... n, где порядок среди k-целого не имеет значения (n выбирает k thingy).

С той же самой причиной, которая вообще не является причиной, я также реализовал ее на С#. Мой вопрос:

Вы видите ошибку в моем алгоритме или коде? И, что более важно, вы можете предложить лучший алгоритм?

Обратите внимание на алгоритм, отличный от самого кода. Это не самый красивый код, который я когда-либо писал, хотя и скажу, если вы видите ошибку.

РЕДАКТИРОВАТЬ: Alogirthm объяснил -

  • Удерживаем k индексов.
  • Это создает k вложенных для циклов, где индекс цикла я - индексы [i].
  • Он моделирует k для циклов, где индексы [i + 1] принадлежат петле, вложенной в цикл индексов [i].
  • индексы [i] пробегают индексы [i - 1] + 1 до n - k + я + 1.

CODE:

public class AllPossibleCombination
{
    int n, k;
    int[] indices;
    List<int[]> combinations = null;

    public AllPossibleCombination(int n_, int k_)
    {
        if (n_ <= 0)
        {
            throw new ArgumentException("n_ must be in N+");
        }
        if (k_ <= 0)
        {
            throw new ArgumentException("k_ must be in N+");
        }
        if (k_ > n_)
        {
            throw new ArgumentException("k_ can be at most n_");
        }

        n = n_;
        k = k_;
        indices = new int[k];
        indices[0] = 1;
    }

    /// <summary>
    /// Returns all possible k combination of 0..n-1
    /// </summary>
    /// <returns></returns>
    public List<int[]> GetCombinations()
    {
        if (combinations == null)
        {
            combinations = new List<int[]>();
            Iterate(0);
        }
        return combinations;
    }

    private void Iterate(int ii)
    {
        //
        // Initialize
        //
        if (ii > 0)
        {
            indices[ii] = indices[ii - 1] + 1;
        }

        for (; indices[ii] <= (n - k + ii + 1); indices[ii]++)
        {
            if (ii < k - 1)
            {
                Iterate(ii + 1);
            }
            else
            {
                int[] combination = new int[k];
                indices.CopyTo(combination, 0);
                combinations.Add(combination);
            }
        }
    }
}

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

Спасибо,
Асаф

Ответ 1

В С++ задана следующая процедура:

template <typename Iterator>
inline bool next_combination(const Iterator first, Iterator k, const Iterator last)
{
   /* Credits: Thomas Draper */
   if ((first == last) || (first == k) || (last == k))
      return false;
   Iterator itr1 = first;
   Iterator itr2 = last;
   ++itr1;
   if (last == itr1)
      return false;
   itr1 = last;
   --itr1;
   itr1 = k;
   --itr2;
   while (first != itr1)
   {
      if (*--itr1 < *itr2)
      {
         Iterator j = k;
         while (!(*itr1 < *j)) ++j;
         std::iter_swap(itr1,j);
         ++itr1;
         ++j;
         itr2 = k;
         std::rotate(itr1,j,last);
         while (last != j)
         {
            ++j;
            ++itr2;
         }
         std::rotate(k,itr2,last);
         return true;
      }
   }
   std::rotate(first,k,last);
   return false;
}

Затем вы можете сделать следующее:

std::string s = "123456789";
std::size_t k = 3;
do
{
   std::cout << std::string(s.begin(),s.begin() + k) << std::endl;
}
while(next_combination(s.begin(),s.begin() + k,s.end()));

Ответ 2

Асаф,

Вы просите нас оценить ваш алгоритм, но вы не объясняете свой алгоритм - даже в комментариях кода. Итак, вы хотите, чтобы каждый потратил час или больше на обратную разработку алгоритма из кода, чтобы мы могли понять ваш вопрос, прежде чем ответить на него?

Измените свой вопрос, чтобы объяснить свой алгоритм.

Очевидна одна вещь - объем памяти вашего кода ужасен. Для даже скромных значений n число комбинаций будет легко находиться в миллиардах, что потребует большего объема памяти, чем у большинства компьютеров. Кроме того, вы используете динамически выращенные массивы, которые продолжают перераспределять и копировать себя по мере их роста. Кроме того, ваша программа генерирует подмножества в разных массивах и объединяет их. В общем, ваша программа потребует много раз объема памяти, которая была бы идеально необходима для хранения списка, и большую часть времени он будет просто копировать данные взад и вперед.

Если вы должны иметь все значения в массиве одновременно, по крайней мере, начинайте с вычисления размера массива, который вам нужен - n!/(n-k)!/k! - и затем просто заполните его.

Еще лучше будет код, который "лениво" просто вычислил каждый член последовательности по мере необходимости. См. этот вопрос из боковой панели связанных вопросов

Ответ 4

Здесь относительно простая/эффективная программа nCr, которую я написал некоторое время назад в C:

main(n,k){float t=0,r=1;for(scanf("%d, %d",&n,&k);t++<k;r*=(1+n-t)/t);printf("%.0f\n",r);}

Хорошо... читаемая версия. =] (Не уверен, что это соответствует 1:1, указанному выше.)

void nCr(int n, int k) {
    float curK = 0, r = 1;
    while(curK < k) {
        ++curK;
        printf("%.0f\n", r);
        r *= (1 + n - curK) / curK;
    }
}

Вместо того, чтобы печатать, вы могли бы yield или другое (я не знаю С#) в вашем списке.