【问题标题】:Algorithm for generating "anti-Gray" on-demand combinations of k elements from n从 n 中生成 k 个元素的“反灰色”按需组合的算法
【发布时间】:2015-03-10 14:08:08
【问题描述】:

我正在尝试实现一种算法,以从一组 n 个元素中获取 k 个元素的所有组合,其中两个连续组合之间的差异被最大化(这种反向格雷码)。换句话说,应该对组合进行排序,以避免元素连续出现两次,从而不会对任何元素进行不必要的区分。

理想情况下,该算法也不会预先计算所有组合并将它们存储到内存中,而是按需提供组合。 我对此进行了广泛搜索,并找到了一些详细的答案,例如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]

对于这个特定的 案例,我确实设法通过在生成组合后应用排序算法来完成这个结果,但这并不能满足我对按需组合生成的要求,因为整个组合必须一次生成,然后排序并保存在内存中。我不确定它是否适用于任意 k 和 n 值。最后,我很确定这不是最有效的方法,因为排序算法基本上会遍历一组组合,试图找到一个与前一个组合不共享任何元素的组合。我还考虑为每个元素保留一个“命中计数”表,并使用它来始终获得包含最低组合命中计数的下一个组合。 我的一些经验性结论是,如果 n > 2k,则可以避免元素完全出现在两个连续的组合中。否则,至少应该可以避免元素连续出现两次以上等。

您可以将此问题与使用足球比赛等的标准循环方案在 k = 2 时实现的问题进行比较,但我需要一个任意 k 值的解决方案。我们可以把它想象成某种游戏的锦标赛,我们有 n 个玩家在一组游戏中与所有其他玩家对战,每场游戏有 k 个玩家。玩家应尽可能不必连续打两场比赛,但也不必在两场比赛之间不必要地等待太久。

任何关于如何使用可靠的排序算法生成后解决这个问题的指针,或者 - 最好是 - 按需的,都会很棒!

注意:我们通常假设 n

谢谢

【问题讨论】:

  • 我怀疑在组合图上为哈密顿循环运行 Angluin--Valiant 局部搜索算法对于小参数设置是有效的,如果在资源使用方面有些残酷的话。
  • @DavidEisenstat 你如何保证最终输出中没有“关闭”边缘?如果我正确理解了这个问题,那么期望的目标是让所有相邻的组合由 至少 X 个位置分隔 - 使其中许多组合非常不同,而有一些非常接近则不会被认为是好的。
  • @tucuxi 组合图有边,只要组合可以在输出中背靠背出现。
  • 我们如何知道给定特定 n 和 k 可实现的最大最小距离?上限是 min(k, n-k),但我不确定它是否总是可以实现的。如果是这样,那么@DavidEisenstat 构建具有可接受边的图并找到哈密顿路径的想法看起来不错(如果昂贵的话)。
  • 非常感谢大家。我不得不承认我学习组合学已经有一段时间了,所以我可能需要付出相当大的努力才能掌握这一点。非常感谢您给我一些指点!

标签: algorithm sorting combinations combinatorics gray-code


【解决方案1】:

按照@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);距离(长度 - size_of_intersection)始终为 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;
    }
}

请注意:对于大量组合,这将非常缓慢...

【讨论】:

  • 感谢您的广泛回答!但是,在查看您的 (7,4) 示例结果时,我不得不问这是否真的满足了使每个组合与前一个组合尽可能不同的要求?我希望像 [0, 1, 2, 3] 之后是 [0, 4, 5, 6], [1, 2, 3, 4], [5, 6, 0, 1], ...我误会了吗?
  • 为了形象化,假设这是一场某种比赛的锦标赛,每轮有 4 名玩家面对面,我们希望在连续比赛时赛程尽可能公平以及游戏之间的等待时间。在您的示例中,玩家 2 将首先连续玩 8 场游戏,而玩家 6 必须等待三轮才能开始玩。我想有可能找到在两个连续组合之间最多共享 1 个元素的排序?
  • 更改距离计算后(现在包含在答案中),它现在会生成您建议的序列。
  • 再次感谢@tucuxi,非常感谢您的努力。对于 (7, 3),这似乎确实产生了预期的结果(尽管组合不是按需产生的,但这是一个可选的奖励要求 :))。但是当改变 n 和 k 时,我确实得到了一些意想不到的结果。对于 (4,2),我根本没有得到任何输出(solve() 返回 false),对于 (6,2),我确实得到了一个不错的元素变化,因为元素不存在于两个连续的组合中,但尽管如此,有些元素是“有区别的”,例如元素 5 没有出现在前 6 个组合中的任何一个中。
  • 我确实觉得我应该更仔细地研究您的解决方案,以便在我进行更多实验之前了解基本理论。 :)
【解决方案2】:

虽然我非常感谢 @tucuxi 和 @David Eisenstadt 的努力,但我发现哈密顿方法存在一些问题,即它不能解决 n 和 k 的某些值,并且某些元素也被不必要地区分。

我决定试一试我的问题中列出的想法(命中计数表),它似乎给出了很好的结果。然而,该解决方案还需要不满足按需奖励要求的生成后排序。不过,对于合理的 n 和 k,这应该是可行的。

诚然,我发现我的算法有时似乎更喜欢导致一个元素连续出现的组合,这可能是可以调整的。但截至目前,我的算法可能不如 tucuxi 的 具体。然而,它确实为每个 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));
        }
    }

}

的结果:

[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]

的结果:

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

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2013-01-15
    • 2010-09-12
    • 1970-01-01
    • 1970-01-01
    • 2023-01-12
    相关资源
    最近更新 更多