如何以随机顺序生成所有集合组合

时间:2016-08-30 23:18:19

标签: algorithm random set combinations combinatorics

首先,我甚至不确定术语是否正确,因为我没有找到类似的东西(特别是因为我甚至不知道要使用哪些关键词)

问题: 有一群人,我想把他们分成小组。我有一套规则可以为每个作业分配一个分数。我想找到最好的一个(或者至少是一个非常好的)。

例如,如果人口为4 {A,B,C,D}并分配给两组,则可能的分配是:

{A,B},{C,D}

{A,C},{B,D}

{A,D},{B,C}

例如,{B,A},{C,D}{C,D},{A,B}都与第一个相同(我不关心组内的顺序和组本身的顺序)。

每个群体的人数,群体数量和适合人数都是投入。

我的想法是列出每个可能的分配,计算他们的分数并跟踪最佳分配。也就是说,蛮力。由于人口可能很大,我想以随机顺序浏览它们并返回时间用完时找到的最好的一个(可能是当用户感到无聊或认为它是一个足够好的发现时)。人口可以从非常小(列出的四个)到非常大(可能超过200个)变化,所以只是尝试随机的而不关心重复与小的一起分解,可能有蛮力(加上我不知道什么时候)如果我使用普通的随机排列,就停止了。)

人口足够大,列出能够改变它们的所有分配都不适合记忆。所以我需要一个方法来以随机顺序查找所有可能的赋值,或者一个方法,给定一个索引,生成相应的赋值,并使用索引数组和shuffle(第二个会更好因为我可以很容易将任务分配到多个服务器中。)

6 个答案:

答案 0 :(得分:2)

用于生成这些配对的简单递归算法是将第一个元素与每个剩余元素配对,并且对于每个这些耦合,递归地生成剩余元素的所有配对。对于组,生成由第一个元素和剩余元素的所有组合组成的所有组,然后递归剩余元素。

您可以计算出这样的组的可能组数:

public static int numGroupingCombinations(int n, int groupSize)
{
    if(n % groupSize != 0)
        return 0;   // n must be a multiple of groupSize

    int count = 1;
    while(n > groupSize)
    {
        count *= nCr(n - 1, groupSize - 1);
        n -= groupSize;
    }
    return count;
}

public static int nCr(int n, int r)
{
    int ret = 1;
    for (int k = 0; k < r; k++) {
        ret = ret * (n-k) / (k+1);
    }
    return ret; 
}
  

所以我需要一个方法来按随机顺序查找所有可能的赋值,或者一个方法,给定索引,生成相应的赋值,并使用索引数组并随机播放(第二个会更好,因为我可以轻松地将任务分配到多个服务器中。)

要从索引生成分组,请选择要与第一个元素分组的项目组合,方法是使用索引的模数和可能的组合数,然后使用this algorithm从结果生成组合。然后将索引除以相同的数字,并递归生成集合的其余部分。

public static void generateGrouping(String[] elements, int groupSize, int start, int index)
{
    if(elements.length % groupSize != 0)
        return;

    int remainingSize = elements.length - start;
    if(remainingSize == 0)
    {
        // output the elements:
        for(int i = 0; i < elements.length; i += groupSize)
        {
            System.out.print("[");
            for(int j = 0; j < groupSize; j++)
                System.out.print(((j==0)?"":",")+elements[i+j]);
            System.out.print("]");
        }
        System.out.println("");
        return; 
    }

    int combinations = nCr(remainingSize - 1, groupSize - 1);

    // decide which combination of remaining elements to pair the first element with:
    int[] combination = getKthCombination(remainingSize - 1, groupSize - 1, index % combinations);

    // swap elements into place
    for(int i = 0; i < groupSize - 1; i++)
    {
        String temp = elements[start + 1 + i];
        elements[start + 1 + i] = elements[start + 1 + combination[i]];
        elements[start + 1 + combination[i]] = temp;
    }

    generateGrouping(elements, groupSize, start + groupSize, index / combinations);

    // swap them back:
    for(int i = groupSize - 2; i >= 0; i--)
    {
        String temp = elements[start + 1 + i];
        elements[start + 1 + i] = elements[start + 1 + combination[i]];
        elements[start + 1 + combination[i]] = temp;
    }
}

public static void getKthCombination(int n, int r, int k, int[] c, int start, int offset)
{
    if(r == 0)
        return;
    if(r == n)
    {
        for(int i = 0; i < r; i++)
            c[start + i] = i + offset;
        return;
    }
    int count = nCr(n - 1, r - 1);
    if(k < count)
    {
        c[start] = offset;
        getKthCombination(n-1, r-1, k, c, start + 1, offset + 1);
        return;
    }
    getKthCombination(n-1, r, k-count, c, start, offset + 1);
}

public static int[] getKthCombination(int n, int r, int k)
{
    int[] c = new int[r];
    getKthCombination(n, r, k, c, 0, 0);

    return c;
}

Demo

start参数就是列表中的距离,因此在顶层调用函数时传递零。该函数可以很容易地重写为迭代。如果交换对象的开销很大,您也可以传递索引数组而不是要分组的对象数组。

答案 1 :(得分:2)

假设我们总共有N个元素要在G组的E组织中组织(G * E = N)。组内的顺序和组内元素的顺序都不重要。最终目标是以随机顺序生成每个解决方案,因为我们知道我们不能同时存储每个解决方案。

首先,让我们考虑如何制作一个解决方案。由于顺序无关紧要,我们可以通过第一个元素对组内的元素以及组本身进行排序来规范化任何解决方案。

例如,如果我们使用{A, B, C, D}考虑人口N = 4, G = 2, E = 2,则可以将解{B,D}, {C,A}规范化为{A,C}, {B,D}。元素在每个组(C之前的A)中排序,并且组被排序(A之前的A)。

当解决方案规范化时,第一组的第一个元素始终是总体的第一个元素。第二个元素是剩余的N-1之一,第三个元素是剩余的N-2之一,依此类推,除了这些元素必须保持排序。因此第一组有(N-1)!/((N-E)!*(E-1)!)种可能性。

类似地,下一组的第一个元素是固定的:它们是每个组创建后的第一个剩余元素。因此,第(n + 1)组(n从0到G-1)的可能性的数量是(N-nE-1)!/((N-(n+1)E)!*(E-1)!) = ((G-n)E-1)!/(((G-n-1)E)!*(E-1)!)

这为我们提供了一种索引解决方案的可能方法。索引不是一个整数,而是一个G整数数组,整数n(仍然从0到G-1)在1到(N-nE-1)!/((N-nE-E)!*(E-1)!)的范围内,并代表组n(或“(n) +1)解决方案的“小组”。这很容易随机生成并检查重复。

我们需要找到的最后一件事是从相应的整数n生成一个组的方法。我们需要从剩下的N-nE-1中选择E-1元素。此时,您可以想象列出每个组合并选择第(n + 1)个组合。当然,这可以在不生成每个组合的情况下完成:see this question

对于好奇心,解决方案的总数为(GE)!/(G!*(E!)^G) 在您的示例中,它是(2*2)!/(2!*(2!)^2) = 3 对于N = 200和E = 2,有6.7e186解决方案 对于N = 200和E = 5,有6.6e243个解决方案(我发现200个元素的最大值)。

另外,对于N = 200且E> 1。在图13中,第一组的可能性的数量大于2 ^ 64(因此它不能存储在64位整数中),这对于表示索引是有问题的。但只要您不需要包含13个以上元素的组,就可以使用64位整数数组作为索引。

答案 2 :(得分:2)

你叫什么&#34;任务&#34;是partitions具有固定数量的相同大小的部分。好吧,主要是。如果(组的数量)*(每组的大小)小于或大于您的人口规模,您没有说明会发生什么。

以非特定顺序生成每个可能的分区并不太困难,但它只适用于小群体或过滤和查找符合某些独立标准的任何分区。如果您需要优化或最小化某些内容,您最终会查看整个分区集,这可能是不可行的。

根据您对实际问题的描述,您希望阅读local searchoptimization算法,其中aforementioned模拟退火就是这样一种技术。

总而言之,这是一个简单的递归Python函数,它生成具有相同大小的部分的固定长度分区,没有特定的顺序。这是我answer对类似分区问题的专门化,而答案本身就是this answer的特化。转换为JavaScript(使用ES6生成器)应该相当简单。

def special_partitions(population, num_groups, group_size):
    """Yields all partitions with a fixed number of equally sized parts.

    Each yielded partition is a list of length `num_groups`, 
    and each part a tuple of length `group_size.
    """
    assert len(population) == num_groups * group_size
    groups = []  # a list of lists, currently empty 

    def assign(i):
        if i >= len(population): 
            yield list(map(tuple, groups))
        else:
            # try to assign to an existing group, if possible
            for group in groups:
                if len(group) < group_size:
                    group.append(population[i])
                    yield from assign(i + 1)
                    group.pop()

            # assign to an entirely new group, if possible
            if len(groups) < num_groups:
                groups.append([population[i]])
                yield from assign(i + 1)
                groups.pop()

    yield from assign(0)

for partition in special_partitions('ABCD', 2, 2):
    print(partition)

print()

for partition in special_partitions('ABCDEF', 2, 3):
    print(partition)

执行时,会打印:

[('A', 'B'), ('C', 'D')]
[('A', 'C'), ('B', 'D')]
[('A', 'D'), ('B', 'C')]

[('A', 'B', 'C'), ('D', 'E', 'F')]
[('A', 'B', 'D'), ('C', 'E', 'F')]
[('A', 'B', 'E'), ('C', 'D', 'F')]
[('A', 'B', 'F'), ('C', 'D', 'E')]
[('A', 'C', 'D'), ('B', 'E', 'F')]
[('A', 'C', 'E'), ('B', 'D', 'F')]
[('A', 'C', 'F'), ('B', 'D', 'E')]
[('A', 'D', 'E'), ('B', 'C', 'F')]
[('A', 'D', 'F'), ('B', 'C', 'E')]
[('A', 'E', 'F'), ('B', 'C', 'D')]

答案 3 :(得分:1)

也许simulated annealing方法可行。您可以从非最佳初始解决方案开始,并使用启发式方法进行迭代以改进。

您的评分标准可能有助于您选择初始解决方案,例如:你能得到最好的得分第一组,然后剩下的就是第二组得分最好,等等。

您的评分标准可能暗示了“邻国”的良好选择,但至少,如果它们因单一交换而不同,您可以考虑两个邻国。

因此算法的迭代部分将尝试一堆交换,随机抽样,并根据退火计划选择一个改善全局得分的交换。

我希望你能找到更好的邻国选择!也就是说,我希望你能根据你的评分标准找到更好的迭代改进规则。

答案 4 :(得分:0)

如果你有足够大的人口,你不能适应内存中的所有分配,并且不太可能测试所有可能的分配,那么最简单的方法就是随机选择测试分配。例如:

repeat 
    randomly shuffle population
    put 1st n/2 members of the shuffled pop into assig1 and 2nd n/2 into assig2
    score assignation and record it if best so far
until bored

如果您的人口众多,由于重复测试不太可能造成很多效率损失,因为您不太可能再次进行相同的分配。

根据您的评分规则,选择下一个要测试的分配可能更有效,例如在目前为止找到的最佳分配之间交换一对成员,但是您没有提供足够的信息来确定是否是这样的。

答案 5 :(得分:0)

以下是针对优化问题的方法(并忽略了基于排列的方法)。

我将问题表述为混合整数问题,并使用专门的解算器来计算出良好的解决方案。

由于您的问题没有得到很好的解决,可能需要进行一些修改。但一般的信息是:这种方法很难被击败!

代码

import numpy as np
from cvxpy import *

""" Parameters """
N_POPULATION = 50
GROUPSIZES = [3, 6, 12, 12, 17]
assert sum(GROUPSIZES) == N_POPULATION
N_GROUPS = len(GROUPSIZES)
OBJ_FACTORS = [0.4, 0.1, 0.15, 0.35]  # age is the most important

""" Create fake data """
age_vector = np.clip(np.random.normal(loc=35.0, scale=10.0, size=N_POPULATION).astype(int), 0, np.inf)
height_vector = np.clip(np.random.normal(loc=180.0, scale=15.0, size=N_POPULATION).astype(int), 0, np.inf)
weight_vector = np.clip(np.random.normal(loc=85, scale=20, size=N_POPULATION).astype(int), 0, np.inf)
skill_vector = np.random.randint(0, 100, N_POPULATION)

""" Calculate a-priori stats """
age_mean, height_mean, weight_mean, skill_mean = np.mean(age_vector), np.mean(height_vector), \
                                                 np.mean(weight_vector), np.mean(skill_vector)


""" Build optimization-model """
# Variables
X = Bool(N_POPULATION, N_GROUPS)  # 1 if part of group
D = Variable(4, N_GROUPS)  # aux-var for deviation-norm

# Constraints
constraints = []

# (1) each person is exactly in one group
for p in range(N_POPULATION):
    constraints.append(sum_entries(X[p, :]) == 1)

# (2) each group has exactly n (a-priori known) members
for g_ind, g_size in enumerate(GROUPSIZES):
    constraints.append(sum_entries(X[:, g_ind]) == g_size)

# Objective: minimize deviation from global-statistics within each group
#   (ugly code; could be improved a lot!)
group_deviations = [[], [], [], []]  # age, height, weight, skill
for g_ind, g_size in enumerate(GROUPSIZES):
    group_deviations[0].append((sum_entries(mul_elemwise(age_vector, X[:, g_ind])) / g_size) - age_mean)
    group_deviations[1].append((sum_entries(mul_elemwise(height_vector, X[:, g_ind])) / g_size) - height_mean)
    group_deviations[2].append((sum_entries(mul_elemwise(weight_vector, X[:, g_ind])) / g_size) - weight_mean)
    group_deviations[3].append((sum_entries(mul_elemwise(skill_vector, X[:, g_ind])) / g_size) - skill_mean)

for i in range(4):
    for g in range(N_GROUPS):
        constraints.append(D[i,g] >= abs(group_deviations[i][g]))

obj_parts = [sum_entries(OBJ_FACTORS[i] * D[i, :]) for i in range(4)]

objective = Minimize(sum(obj_parts))

""" Build optimization-problem & solve """
problem = Problem(objective, constraints)
problem.solve(solver=GUROBI, verbose=True, TimeLimit=120)  # might need to use non-commercial solver here
print('Min-objective: ', problem.value)

""" Evaluate solution """
filled_groups = [[] for g in range(N_GROUPS)]
for g_ind, g_size in enumerate(GROUPSIZES):
    for p in range(N_POPULATION):
        if np.isclose(X[p, g_ind].value, 1.0):
            filled_groups[g_ind].append(p)
for g_ind, g_size in enumerate(GROUPSIZES):
    print('Group: ', g_ind, ' of size: ', g_size)
    print(' ' + str(filled_groups[g_ind]))

group_stats = []
for g in range(N_GROUPS):
    age_mean_in_group = age_vector[filled_groups[g]].mean()
    height_mean_in_group = height_vector[filled_groups[g]].mean()
    weight_mean_in_group = weight_vector[filled_groups[g]].mean()
    skill_mean_in_group = skill_vector[filled_groups[g]].mean()
    group_stats.append((age_mean_in_group, height_mean_in_group, weight_mean_in_group, skill_mean_in_group))
print('group-assignment solution means: ')
for g in range(N_GROUPS):
    print(np.round(group_stats[g], 1))

""" Compare with input """
input_data = np.vstack((age_vector, height_vector, weight_vector, skill_vector))
print('input-means')
print(age_mean, height_mean, weight_mean, skill_mean)
print('input-data')
print(input_data)

输出(2分钟的时限;商业解算器)

Time limit reached
Best objective 9.612058823514e-01, best bound 4.784117647059e-01, gap 50.2280%
('Min-objective: ', 0.961205882351435)
('Group: ', 0, ' of size: ', 3)
 [16, 20, 27]
('Group: ', 1, ' of size: ', 6)
 [26, 32, 34, 45, 47, 49]
('Group: ', 2, ' of size: ', 12)
 [0, 6, 10, 12, 15, 21, 24, 30, 38, 42, 43, 48]
('Group: ', 3, ' of size: ', 12)
 [2, 3, 13, 17, 19, 22, 23, 25, 31, 36, 37, 40]
('Group: ', 4, ' of size: ', 17)
 [1, 4, 5, 7, 8, 9, 11, 14, 18, 28, 29, 33, 35, 39, 41, 44, 46]
group-assignment solution means: 
[  33.3  179.3   83.7   49. ]
[  33.8  178.2   84.3   49.2]
[  33.9  178.7   83.8   49.1]
[  33.8  179.1   84.1   49.2]
[  34.   179.6   84.7   49. ]
input-means
(33.859999999999999, 179.06, 84.239999999999995, 49.100000000000001)
input-data
[[  22.   35.   28.   32.   41.   26.   25.   37.   32.   26.   36.   36.
    27.   34.   38.   38.   38.   47.   35.   35.   34.   30.   38.   34.
    31.   21.   25.   28.   22.   40.   30.   18.   32.   46.   38.   38.
    49.   20.   53.   32.   49.   44.   44.   42.   29.   39.   21.   36.
    29.   33.]
 [ 161.  158.  177.  195.  197.  206.  169.  182.  182.  198.  165.  185.
   171.  175.  176.  176.  172.  196.  186.  172.  184.  198.  172.  162.
   171.  175.  178.  182.  163.  176.  192.  182.  187.  161.  158.  191.
   182.  164.  178.  174.  197.  156.  176.  196.  170.  197.  192.  171.
   191.  178.]
 [  85.  103.   99.   93.   71.  109.   63.   87.   60.   94.   48.  122.
    56.   84.   69.  162.  104.   71.   92.   97.  101.   66.   58.   69.
    88.   69.   80.   46.   74.   61.   25.   74.   59.   69.  112.   82.
   104.   62.   98.   84.  129.   71.   98.  107.  111.  117.   81.   74.
   110.   64.]
 [  81.   67.   49.   74.   65.   93.   25.    7.   99.   34.   37.    1.
    25.    1.   96.   36.   39.   41.   33.   28.   17.   95.   11.   80.
    27.   78.   97.   91.   77.   88.   29.   54.   16.   67.   26.   13.
    31.   57.   84.    3.   87.    7.   99.   35.   12.   44.   71.   43.
    16.   69.]]

解决方案备注

  • 此解决方案看起来相当不错(关于平均偏差)并且它只花了 2分钟(我们决定了a-priori的时间限制)
  • 我们也有紧张的界限:0.961 is our solution; we know it can't be lower than 4.784

重复性

  • 代码使用 numpy cvxpy
  • 使用商业解算器
    • 您可能需要使用非商业MIP求解器(支持早期堕胎的时间限制;采用当前最佳解决方案)
    • cvxpy支持的有效开源MIP解算器有: cbc (暂时无法设置时间限制)和 glpk (查看文档的时间 - 限制支持)

模型决策

  • 该代码使用L1范数惩罚,导致 MIP问题
  • 根据您的问题,使用L2范数惩罚可能是明智的(一个大的偏差比许多较小的偏差更痛),这将导致更难的问题( MIQP / MISOCP