Алгоритм генерации антигреевских комбинаций по запросу из k элементов из n

Я пытаюсь реализовать алгоритм для получения всех комбинаций k элементов из набора n элементов, где разница между двумя последовательными комбинациями максимальна (такого рода обратные коды Грея). Другими словами, комбинации должны быть упорядочены таким образом, чтобы элементы не появлялись дважды подряд и чтобы ни один элемент не выделялся без необходимости.

В идеале алгоритм также НЕ будет предварительно вычислять все комбинации и сохранять их в памяти, а будет доставлять комбинации по запросу. Я тщательно искал это и нашел несколько подробных ответов, таких как https://stackoverflow.com/a/127856/1226020, но я не могу применить это. Кроме того, многие статьи, на которые есть ссылки в этом ответе, являются платным контентом.

Чтобы проиллюстрировать, что я имею в виду:

Из набора [0, 1, 2, 3, 4] найдите все комбинации двух элементов. Используя простой алгоритм, который пытается увеличить самый правый элемент до тех пор, пока это становится невозможным, затем перемещаясь влево, увеличивая предыдущую цифру и т. д., я получаю следующие результаты:

[0, 1]
[0, 2]
[0, 3]
[0, 4]
[1, 2]
[1, 3]
[1, 4]
[2, 3]
[2, 4]
[3, 4]

Я получаю этот результат со следующим кодом Java:

public class CombinationGenerator {
    private final int mNrElements;
    private final int[] mCurrentCombination;

    public CombinationGenerator(int n, int k) {
        mNrElements = n;
        mCurrentCombination = new int[k];

        initElements(0, 0);
        // fake initial state in order not to miss first combination below
        mCurrentCombination[mCurrentCombination.length - 1]--;
    }

    private void initElements(int startPos, int startValue) {
        for (int i = startPos; i < mCurrentCombination.length; i++) {
            mCurrentCombination[i] = i + startValue - startPos;
        }
    }

    public int[] getNextCombination() {
        for (int i = 0; i < mCurrentCombination.length; i++) {
            int pos = mCurrentCombination.length - 1 - i;

            if (mCurrentCombination[pos] < mNrElements - 1 - i) {
                initElements(pos, mCurrentCombination[pos] + 1);
                return mCurrentCombination;
            }
        }

        return null;
    }

    public static void main(String[] args) {
        CombinationGenerator cg = new CombinationGenerator(5, 2);
        int[] c;

        while ((c = cg.getNextCombination()) != null) {
            System.out.println(Arrays.toString(c));
        }
    }

}

Это не то, чего я хочу, потому что я хочу, чтобы каждая последующая комбинация максимально отличалась от предыдущей. В настоящее время элемент «1» появляется четыре раза подряд и больше никогда. Для этого конкретного примера одним из решений будет:

[0, 1]
[2, 3]
[0, 4]
[1, 2]
[3, 4]
[0, 2]
[1, 3]
[2, 4]
[0, 3]
[1, 4]

Мне действительно удалось добиться этого результата для этого конкретного (7,4) путем применения алгоритма сортировки после создания комбинаций, но это не соответствует моему требованию генерации комбинаций по запросу, поскольку весь набор комбинаций должен быть сгенерирован сразу, затем отсортирован и сохранен в памяти. Я не уверен, что это работает и для произвольных значений k и n. И, наконец, я почти уверен, что это не самый эффективный способ, поскольку алгоритм сортировки в основном перебирает набор комбинаций, пытаясь найти ту, которая не имеет общих элементов с предыдущей комбинацией. Я также подумал о том, чтобы вести таблицу «счетчиков попаданий» для каждого элемента и использовать ее, чтобы всегда получать следующую комбинацию, содержащую наименьшее комбинированное количество попаданий. Мой несколько эмпирический вывод состоит в том, что можно будет избежать полного появления элементов в двух последовательных комбинациях, если n > 2k. В противном случае, по крайней мере, должна быть возможность избежать появления элементов более двух раз подряд и т. д.

Вы можете сравнить эту проблему с тем, что достигается для k = 2, используя стандартную круговую схему для футбольных игр и т. д., но мне нужно решение для произвольных значений k. Мы можем представить это как турнир по какой-то игре, в которой у нас есть n игроков, которые должны играть против всех других игроков в наборе игр, в каждой игре участвуют k игроков. Игрокам не следует, насколько это возможно, играть две игры подряд, но им также не следует слишком долго ждать между двумя появлениями в игре.

Любые указатели на то, как решить эту проблему либо с помощью надежного алгоритма сортировки после генерации, либо, предпочтительно, по требованию, были бы потрясающими!

Примечание. Обычно предполагается, что n ‹= 50, k ‹= 5

Спасибо


person JHH    schedule 10.03.2015    source источник
comment
Я подозреваю, что запуск алгоритма локального поиска Angluin--Valiant для гамильтонова цикла на графе комбинаций был бы эффективен для небольших настроек параметров, хотя и несколько жестковат в использовании ресурсов.   -  person David Eisenstat    schedule 10.03.2015
comment
@DavidEisenstat, как вы гарантируете, что в конечном результате нет близких краев? Если я правильно понимаю проблему, желаемая цель состоит в том, чтобы все соседние комбинации были разделены по крайней мере X позициями - сделать многие из них очень разными, а некоторые очень близкими не будет считаться хорошим.   -  person tucuxi    schedule 10.03.2015
comment
@tucuxi Граф комбинаций имеет ребра везде, где это нормально, чтобы комбинации отображались подряд в выводе.   -  person David Eisenstat    schedule 10.03.2015
comment
Как мы можем узнать максимальное минимальное расстояние, достижимое при заданных n и k? Верхний предел равен min(k, n-k), но я не уверен, что он всегда достижим. Если это так, то идея @DavidEisenstat о построении графа с допустимыми ребрами и поиске гамильтонового пути выглядит хорошо (хотя и дорого).   -  person tucuxi    schedule 10.03.2015
comment
Большое спасибо, ребята. Должен признать, что я давно не изучал комбинаторику, так что, вероятно, мне придется приложить немало усилий, чтобы понять это. Большое спасибо за то, что дали мне несколько советов!   -  person JHH    schedule 11.03.2015


Ответы (2)


Быстрый и грязный рабочий код, работающий над предложением @DavidEisenstat:

public static void main(String[] args) {
    ArrayList<int[]> all = new ArrayList<int[]>();
    // output is 0 if distance(i, j) != max, and 1 otherwise
    int[][] m = buildGraph(7, 4, all);
    HamiltonianCycle hc = new HamiltonianCycle();
    int path[] = hc.findHamiltonianCycle(m);
    if (path != null) {
        // I have no proof that such a path will always exist
        for (int i : path) {
            System.out.println(Arrays.toString(all.get(i)));
        }
    }
}

Вывод для приведенного выше кода (7,4); расстояние (как длина - размер_пересечения) всегда равно 3; попытка использовать 4 приведет к отключенному графу:

    [0, 1, 2, 3]
    [0, 4, 5, 6]
    [1, 2, 3, 4]
    [0, 1, 5, 6]
    [0, 2, 3, 4]
    [1, 2, 5, 6]
    [0, 1, 3, 4]
    [0, 2, 5, 6]
    [1, 3, 4, 5]
    [0, 1, 2, 6]
    [0, 3, 4, 5]
    [1, 2, 3, 6]
    [0, 1, 4, 5]
    [0, 2, 3, 6]
    [1, 4, 5, 6]
    [0, 2, 3, 5]
    [1, 2, 4, 6]
    [0, 3, 5, 6]
    [1, 2, 4, 5]
    [0, 3, 4, 6]
    [1, 2, 3, 5]
    [0, 2, 4, 6]
    [1, 3, 5, 6]
    [0, 2, 4, 5]
    [1, 3, 4, 6]
    [0, 1, 2, 5]
    [2, 3, 4, 6]
    [0, 1, 3, 5]
    [2, 4, 5, 6]
    [0, 1, 3, 6]
    [2, 3, 4, 5]
    [0, 1, 4, 6]
    [2, 3, 5, 6]
    [0, 1, 2, 4]
    [3, 4, 5, 6]

Недостающие биты кода:

// uses JHH's code to build sequences, stores it in 'all'
public static int[][] buildGraph(int n, int k, ArrayList<int[]> all) {
    SequenceGenerator sg = new SequenceGenerator(n, k);
    int[] c;
    while ((c = sg.getNextCombination()) != null) {
        all.add(c.clone());         
    }
    int best = Math.min(n-k, k);
    System.out.println("Best is " + best);
    int matrix[][] = new int[all.size()][];
    for (int i=0; i<matrix.length; i++) {
        matrix[i] = new int[all.size()];
        for (int j=0; j<i; j++) {
            int d = distance(all.get(j), all.get(i));
            matrix[i][j] = matrix[j][i] = (d != best)? 0 : 1;
        }           
    }
    return matrix;
}

Расстояние: (совсем неэффективно, но затмевается стоимостью гамильтоновых вычислений)

public static int distance(int[] a, int[] b) {
        HashSet<Integer> ha = new HashSet<Integer>();
        HashSet<Integer> hb = new HashSet<Integer>();
        for (int i=0; i<a.length; i++) {
                ha.add(a[i]);
                hb.add(b[i]);
        }
        ha.retainAll(hb);
        return a.length - ha.size();
}

А для нахождения гамильтониана я изменил код с http://www.sanfoundry.com/java-program-find-hamiltonian-cycle-unweighted-graph/:

import java.util.Arrays;

public class HamiltonianCycle {

    private int V, pathCount;
    private int[] path;
    private int[] answer;
    private int[][] graph;

    public int[] findHamiltonianCycle(int[][] g) {
        V = g.length;
        path = new int[V];

        Arrays.fill(path, -1);
        graph = g;
        path[0] = 0;
        pathCount = 1;
        if (solve(0)) {
            return path;
        } else {
            return null;
        }
    }

    public boolean solve(int vertex) {
        if (graph[vertex][0] == 1 && pathCount == V) {
            return true;
        }
        if (pathCount == V) {
            return false;
        }

        for (int v = 0; v < V; v++) {
            if (graph[vertex][v] == 1) {
                path[pathCount++] = v;
                graph[vertex][v] = 0;
                graph[v][vertex] = 0;

                if (!isPresent(v)) {
                    if (solve(v)) {
                        answer = path.clone();
                        return true;
                    }
                }

                graph[vertex][v] = 1;
                graph[v][vertex] = 1;
                path[--pathCount] = -1;
            }
        }
        return false;
    }

    public boolean isPresent(int v) {
        for (int i = 0; i < pathCount - 1; i++) {
            if (path[i] == v) {
                return true;
            }
        }
        return false;
    }
}

Имейте в виду: это будет очень медленно для большого количества комбинаций...

person tucuxi    schedule 10.03.2015
comment
Спасибо за развернутый ответ! Однако, глядя на результаты вашего примера (7,4), я должен спросить, действительно ли это соответствует требованию иметь каждую комбинацию как можно более отличной от предыдущей? Я бы ожидал что-то вроде [0, 1, 2, 3], за которым следует [0, 4, 5, 6], [1, 2, 3, 4], [5, 6, 0, 1], ... Сделал Я неправильно понимаю? - person JHH; 11.03.2015
comment
Для наглядности предположим, что это турнир какой-то игры, где в каждом раунде 4 игрока встречаются друг с другом, и мы хотим, чтобы расписание было максимально справедливым, когда речь идет о последовательных играх и времени ожидания между играми. В вашем примере игрок 2 начнет с того, что сыграет 8 игр подряд, а игроку 6 придется подождать три раунда, прежде чем он вообще сможет играть. Я предполагаю, что можно было бы найти порядок, при котором не более 1 элемента является общим для двух последовательных комбинаций? - person JHH; 11.03.2015
comment
После изменения расчета расстояния (теперь включенного в ответ) теперь генерируется предложенная вами последовательность. - person tucuxi; 11.03.2015
comment
Еще раз спасибо @tucuxi, очень ценю ваши усилия. Для (7, 3) это действительно дает желаемые результаты (хотя комбинации не создаются по запросу, но это было необязательным бонусным требованием :)). Но я получаю неожиданные результаты при изменении n и k. Для (4,2) я вообще не получаю никакого вывода (solve() возвращает false), а для (6,2) я действительно получаю достойную вариацию элементов в том смысле, что элементы не присутствуют в двух последовательных комбинациях. , но тем не менее некоторые элементы дискриминируются, например элемент 5 не встречается ни в одной из 6 первых комбинаций. - person JHH; 11.03.2015
comment
Я чувствую, что мне следует более внимательно изучить ваше решение, чтобы понять основную теорию, прежде чем я буду больше экспериментировать. :) - person JHH; 11.03.2015
comment
Основное объяснение: соединяйте точки (= допустимые комбинации 7,4) только в том случае, если они достаточно различны, чтобы можно было разместить их рядом. Затем найдите путь, который соединяет все точки в одном пути, никогда не используя одну и ту же точку дважды (= гамильтонов цикл). - person tucuxi; 11.03.2015

Хотя я действительно ценю усилия @tucuxi и @David Eisenstadt, я обнаружил некоторые проблемы с гамильтоновым подходом, а именно то, что он не решался для определенных значений n и k, а некоторые элементы также были излишне дискриминированы.

Я решил попробовать идеи, перечисленные в моем вопросе (таблица количества обращений), и, похоже, это дало довольно хорошие результаты. Это решение, однако, также требует сортировки после генерации, что не соответствует требованию бонуса по требованию. Однако для разумных n и k это должно быть осуществимо.

Правда, я обнаружил, что мой алгоритм иногда, по-видимому, предпочитает комбинации, которые приводят к последовательному появлению одного элемента, который, вероятно, можно настроить. Но на данный момент мой алгоритм может уступать алгоритму Тукукси для (7,4  ) конкретно. Однако он представляет решение для каждого n, k и, кажется, меньше различает элементы.

Мой код указан ниже.

Спасибо еще раз.

public class CombinationGenerator {
    private final int N;
    private final int K;
    private final int[] mCurrentCombination;

    public CombinationGenerator(int n, int k) {
        N = n;
        K = k;
        mCurrentCombination = new int[k];

        setElementSequence(0, 0);
        mCurrentCombination[K - 1]--; // fool the first iteration
    }

    private void setElementSequence(int startPos, int startValue) {
        for (int i = startPos; i < K; i++) {
            mCurrentCombination[i] = i + startValue - startPos;
        }
    }

    public int[] getNextCombination() {
        for (int i = K - 1; i >= 0; i--) {
            if (mCurrentCombination[i] < i + N - K) {
                setElementSequence(i, mCurrentCombination[i] + 1);
                return mCurrentCombination.clone();
            }
        }

        return null;
    }   
}

public class CombinationSorter {
    private final int N;
    private final int K;

    public CombinationSorter(int n, int k) {
        N = n;
        K = k;
    }

    public List<int[]> getSortedCombinations(List<int[]> combinations) {
        List<int[]> sortedCombinations = new LinkedList<int[]>();
        int[] combination = null;
        int[] hitCounts = new int[N]; // how many times each element has been
                                      // used so far

        // Note that this modifies (empties) the input list
        while (!combinations.isEmpty()) {
            int index = findNextCombination(combinations, hitCounts, combination);
            combination = combinations.remove(index);
            sortedCombinations.add(combination);

            addHitCounts(combination, hitCounts);
        }

        return sortedCombinations;
    }

    private int findNextCombination(List<int[]> combinations, int[] hitCounts,
            int[] previousCombination) {
        int lowestHitCount = Integer.MAX_VALUE;
        int foundIndex = 0;

        // From the remaining combinations, find the one with the least used
        // elements
        for (int i = 0; i < combinations.size(); i++) {
            int[] comb = combinations.get(i);
            int hitCount = getHitCount(comb, hitCounts);

            if (hitCount < lowestHitCount) {
                lowestHitCount = hitCount;
                foundIndex = i;
            } else if (hitCount == lowestHitCount
                    && previousCombination != null
                    && getNrCommonElements(comb, previousCombination) < getNrCommonElements(
                            combinations.get(foundIndex), previousCombination)) {
                // prefer this combination if hit count is equal but it's more
                // different from the previous combination in our sorted list
                // than what's been found so far (avoids consecutive element
                // appearances)
                foundIndex = i;
            }
        }

        return foundIndex;
    }

    private int getHitCount(int[] combination, int[] hitCounts) {
        int hitCount = 0;

        for (int i = 0; i < K; i++) {
            hitCount += hitCounts[combination[i]];
        }

        return hitCount;
    }

    private void addHitCounts(int[] combination, int[] hitCounts) {
        for (int i = 0; i < K; i++) {
            hitCounts[combination[i]]++;
        }
    }

    private int getNrCommonElements(int[] c1, int[] c2) {
        int count = 0;

        for (int i = 0; i < K; i++) {
            for (int j = 0; j < K; j++) {
                if (c1[i] == c2[j]) {
                    count++;
                }
            }
        }
        return count;
    }
}

public class Test {
    public static void main(String[] args) {
        final int n = 7;
        final int k = 3;

        CombinationGenerator cg = new CombinationGenerator(n, k);
        List<int[]> combinations = new LinkedList<int[]>();
        int[] nc;

        while ((nc = cg.getNextCombination()) != null) {
            combinations.add(nc);
        }

        for (int[] c : combinations) {
            System.out.println(Arrays.toString(c));
        }

        System.out.println("Sorting...");

        CombinationSorter cs = new CombinationSorter(n, k);
        List<int[]> sortedCombinations = cs.getSortedCombinations(combinations);

        for (int[] sc : sortedCombinations) {
            System.out.println(Arrays.toString(sc));
        }
    }

}

Результаты для (7,4):

[0, 1, 2, 3]
[0, 4, 5, 6]
[1, 2, 3, 4]
[0, 1, 5, 6]
[2, 3, 4, 5]
[0, 1, 2, 6]
[3, 4, 5, 6]
[0, 1, 2, 4]
[0, 3, 5, 6]
[1, 2, 4, 5]
[0, 1, 3, 6]
[2, 4, 5, 6]
[0, 1, 3, 4]
[2, 3, 5, 6]
[0, 1, 4, 5]
[0, 2, 3, 6]
[1, 3, 4, 5]
[0, 2, 4, 6]
[1, 2, 3, 5]
[0, 1, 4, 6]
[0, 2, 3, 5]
[1, 2, 4, 6]
[1, 3, 5, 6]
[0, 2, 3, 4]
[1, 2, 5, 6]
[0, 3, 4, 5]
[1, 2, 3, 6]
[0, 2, 4, 5]
[1, 3, 4, 6]
[0, 2, 5, 6]
[0, 1, 3, 5]
[2, 3, 4, 6]
[1, 4, 5, 6]
[0, 1, 2, 5]
[0, 3, 4, 6]

Результаты для (5,2):

[0, 1]
[2, 3]
[0, 4]
[1, 2]
[3, 4]
[0, 2]
[1, 3]
[2, 4]
[0, 3]
[1, 4]
person JHH    schedule 11.03.2015