【问题标题】:What's the fastest heuristic algorithm to split students into groups?将学生分组的最快启发式算法是什么?
【发布时间】:2019-07-31 12:11:31
【问题描述】:

我有 X 名学生,其中 X 是 6 的倍数。我现在想将学生分成 6 人一组。

我有一个函数可以衡量 6 人一组的“好”程度(假设它是一个黑匣子,目前在恒定时间内运行)。通过拆分学生,然后在每个组上调用我的函数来衡量它的好坏,然后总结每个组的好坏,我能够衡量一组特定组的“好”程度。

我正在尝试创建一种算法,该算法将以某种方式对学生进行分组,以使所有组的总优度最大化,并且没有任何组的个体优度低于某个值 y。换句话说,将学生分成 6 人一组,以在所有组的优度均高于 y 的约束下最大化总优度。

我预计运行此算法的学生人数 (X) 约为 36。

问题似乎是 NP-Complete,所以我可以接受启发式算法。我对此没有太多经验,但我认为某种遗传算法或模拟退火甚至贪心算法可能会起作用,但我不确定从哪里开始我的研究。

有人能指出我正确的方向吗?我做了一些研究,这个问题似乎与旅行推销员问题几乎相同(问题空间是学生/节点的所有排列)但我认为我不能将 TSP 算法应用于此,因为“节点的数量"(大约 36)对于任何有效的东西来说都是相当大的。

【问题讨论】:

  • 我在这里能看到的唯一捷径是记忆(或相反),因为这仍然需要至少 2^(n-12) 个条目,运行时间必须至少为 O( 2^n),所以绝对是指数的。
  • @RBarryYoung 是的,它本质上是 TSP,所以即使是动态编程(记忆)也会给你指数级的时间。我将不得不接受启发式算法,但问题空间太大,我认为我什至无法使用遗传或退火等传统技术得到一个好的解决方案。
  • 我不相信它等同于 TSP,但它肯定至少是指数级的。
  • 善良是个别学生的特征,还是纯粹的群体特征?换句话说,是否可以通过了解成员的好坏来构建团体,还是必须先验提出一个团队来确定它的好坏?
  • @pjs 善良是一个群体的特征。善不存在于个别成员,只存在于他们的群体中。

标签: algorithm computer-science genetic-algorithm greedy simulated-annealing


【解决方案1】:

我们以 36 名学生分成 6 个小组为例。检查所有组合是不切实际的,因为有 3,708,580,189,773,818,399,040。但是,通过检查成对组之间学生的每个分布来进行反复改进的策略应该是可行的。

有 462 种方法可以将 12 名学生分成 2 组,因此找到最优的 12→2 分布只需要 924 次调用“组质量”函数。 6 个组中有 15 种可能的组配对,因此 13,860 次调用将揭示配对组的最佳方式,并在配对之间重新分配学生以获得最大的改进。

从随机初始分布开始,算法计算所有 15 对组的最佳分布:AB,CD,EF,BC,DE,FA,AC,BD,CE,DF,EA,FB,AD,BE,CF

然后它比较所有 15 对组合的分数,以找到总分最高的组合,例如DE+AC+FB

然后它重新分配学生,并返回新的总分。这构成了一个改进步骤。然后重复此过程多次,直到找不到更多改进,或者直到您用完时间。从不同的随机初始分布开始多次运行算法也可能很有用。

该算法可以在配对和配对组合阶段进行微调。优化一对组时,您必须选择例如学生在两组中的分布是否比两组都增加的分布更可取他们的分数提高了+1,综合提高了+2。

再次在配对组合阶段,您必须决定是否需要对所有三对组合进行改进,或者是否选择组合改进最高的组合。

我假设允许一个小组在一个步骤之后获得较低的分数,如果这可以提高整体得分,将允许学生在小组之间进行更多的移动,并可能导致探索更多的组合。


为了能够编写代码来测试这个策略,需要一个虚拟的“群体质量”函数,所以我将学生编号从 1 到 36,并使用乘以相邻学生之间距离的函数数字。所以例如[2,7,15,16,18,30] 组的得分为 5*8*1*2*12 = 960。如果把编号想象成对学生能力的排名,那么优质组就是混合能力组。最优分布是:

A组:[1, 7, 13, 19, 25, 31] B组:[2、8、14、20、26、32] C组:[3, 9, 15, 21, 27, 33] D组:[4、10、16、22、28、34] E组:[5、11、17、23、29、35] F组:[6, 12, 18, 24, 30, 36]

每组得分6*6*6*6*6 = 7776,总得分46656。在实践中,我发现使用Log(score) 可以得到更好的结果,因为它有利于所有组的小改进,而不是一个或两个组的大改进。 (支持对几个组或质量最低的组进行改进,或者只是选择最佳的整体改进,是您必须根据特定的“组质量”功能进行微调的部分。)


令我惊讶的是,该算法总是设法找到最佳解决方案,并且只需 4 到 7 步,这意味着进行了不到 100,000 次“组质量”函数调用。我使用的“组质量”算法当然很简单,所以你必须用真实的东西来检查它,以衡量这种方法在你的具体情况下的有用性。但很明显,该算法只需几个步骤即可彻底重新排列分布。

(为简单起见,下面的代码示例针对 36 个学生和 6 个组的情况进行了硬编码。对每个组中的学生进行排序是为了简化质量函数。)

function improve(groups) {
    var pairs = [[0,1],[0,2],[0,3],[0,4],[0,5],[1,2],[1,3],[1,4],[1,5],[2,3],[2,4],[2,5],[3,4],[3,5],[4,5]];
    var combi = [[0,9,14],[0,10,13],[0,11,12],[1,6,14],[1,7,13],[1,8,12],[2,5,14],[2,7,11],[2,8,10],[3,5,13],[3,6,11],[3,8,9],[4,5,12],[4,6,10],[4,7,9]];
    // FIND OPTIMAL DISTRIBUTION FOR ALL PAIRS OF GROUPS
    var optim = [];
    for (var i = 0; i < 15; i++) {
        optim[i] = optimise(groups[pairs[i][0]], groups[pairs[i][1]]);
    }
    // FIND BEST COMBINATION OF PAIRS
    var best, score = -1;
    for (var i = 0; i < 15; i++) {
        var current = optim[combi[i][0]].score + optim[combi[i][1]].score + optim[combi[i][2]].score;
        if (current > score) {
            score = current;
            best = i;
        }
    }
    // REDISTRIBUTE STUDENTS INTO GROUPS AND RETURN NEW SCORE
    groups[0] = optim[combi[best][0]].group1.slice();
    groups[1] = optim[combi[best][0]].group2.slice();
    groups[2] = optim[combi[best][1]].group1.slice();
    groups[3] = optim[combi[best][1]].group2.slice();
    groups[4] = optim[combi[best][2]].group1.slice();
    groups[5] = optim[combi[best][2]].group2.slice();
    return score;
}

// FIND OPTIMAL DISTRIBUTION FOR PAIR OF GROUPS
function optimise(group1, group2) {
    var optim = {group1: [], group2: [], score: -1};
    var set = group1.concat(group2).sort(function(a, b) {return a - b});
    var distr = [0,0,0,0,0,1,1,1,1,1,1];
    // TRY EVERY COMBINATION
    do {
        // KEEP FIRST STUDENT IN FIRST GROUP TO AVOID SYMMETRIC COMBINATIONS
        var groups = [[set[0]], []];
        // DISTRIBUTE STUDENTS INTO GROUP 0 OR 1 ACCORDING TO BINARY ARRAY
        for (var j = 0; j < 11; j++) {
            groups[distr[j]].push(set[j + 1]);
        }
        // CHECK SCORE OF GROUPS AND STORE IF BETTER
        var score = quality(groups[0]) + quality(groups[1]);
        if (score > optim.score) {
            optim.group1 = groups[0].slice();
            optim.group2 = groups[1].slice();
            optim.score = score;
        }
    } while (increment(distr));
    return optim;

    // GENERATE NEXT PERMUTATION OF BINARY ARRAY
    function increment(array) {
        var digit = array.length, count = 0;
        while (--digit >= 0) {
            if (array[digit] == 1) ++count
            else if (count) {
                array[digit] = 1;
                for (var i = array.length - 1; i > digit; i--) {
                    array[i] = --count > 0 ? 1 : 0;
                }
                return true;
            }
        }
        return false;
    }
}

// SCORE FOR ONE GROUP ; RANGE: 0 ~ 8.958797346140275
function quality(group) {
    // LOGARITHM FAVOURS SMALL IMPROVEMENTS TO ALL GROUPS OVER LARGE IMPROVEMENT TO ONE GROUP
    return Math.log((group[5] - group[4]) * (group[4] - group[3]) * (group[3] - group[2]) * (group[2] - group[1]) * (group[1] - group[0]));
}

// SUM OF SCORES FOR ALL 6 GROUPS ; RANGE: 0 ~ 53.75278407684165
function overallQuality(groups) {
    var score = 0;
    for (var i = 0; i < 6; i++) score += quality(groups[i]);
    return score;
}

// PREPARE RANDOM TEST DATA
var students = [1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31,32,33,34,35,36];
var groups = [[],[],[],[],[],[]];
for (var i = 5; i >=0; i--) {
    for (var j = 5; j >= 0; j--) {
        var pick = Math.floor(Math.random() * (i * 6 + j));
        groups[i].push(students[pick]);
        students[pick] = students[i * 6 + j];
    }
    groups[i].sort(function(a, b) {return a - b});
}

// DISPLAY INITIAL SCORE AND DISTRIBUTION
var score = overallQuality(groups);
document.write("<PRE>Initial: " + score.toFixed(2) + " " + JSON.stringify(groups) + "<BR>");

// IMPROVE DISTRIBUTION UNTIL SCORE NO LONGER INCREASES
var prev, step = 0;
do {
    prev = score;
    score = improve(groups);
    document.write("Step " + ++step + " : " + score.toFixed(2) + " " + JSON.stringify(groups) + "<BR>");
} while (score > prev && score < 53.75278407684165);
if (score >= 53.75278407684165) document.write("Optimal solution reached.</PRE>");

注意:在选择了最佳的配对组合并重新分配了这些配对中的学生之后,您当然知道这三对现在具有最佳的学生分布。因此,您可以在接下来的步骤中跳过检查这三对,并将它们的当前分数用作最佳分数。

【讨论】:

  • 你用的这个算法是什么?是 K 表示算法吗?
  • 你能解释一下这个排列代码在做什么吗?
  • @ThilinaDinithFonseka 我从二进制字符串 000000111111 开始,这意味着前 6 名学生进入第一组,后 6 名进入第二组。排列代码生成二进制字符串的下一个排列:000001011111,然后是 000001101111,依此类推,直到 011111100000(保留第一组中的第一个学生以避免重复,因为例如 000000111111 和 111111000000 都会导致相同的两个6 人一组)。
  • @ThilinaDinithFonseka 这是我自己的混合物,尽管它可能只是对现有算法的重新发明。但这不是 K-means 聚类,它只是对优化所有组对的所有可能方法进行详尽检查,然后进行详尽检查以找到对组进行配对并重新分配学生的最佳方法。这是重复完成的,并且每次分布都得到改进,同时只需要对质量函数进行有限数量的调用(与暴力破解所有可能的分布相比)。它似乎运作良好,但可能只是为了这个虚拟质量功能。
  • 不,我认为您的解决方案很棒。因为这就是我想要的。我感谢您为解决此问题所做的努力。也非常感谢您的解释。我对你的代码有一个解释。这种组合是如何创建的? var combi = [[0,2,4],[1,3,5],[0,3,14],[1,4,12],[2,5,13]... 我的意思是这个。有没有办法根据组数生成它?
【解决方案2】:

我将从一个非常简单的“随机搜索”算法开始:

start from a random solution (a partition of X to groups), call it S[0]

score[0] = black_box_socre(S[0])

i = 0

while (some condition):
    i++
    S[i] = some small permutation on S[i-1]  # (1)
    score[i] = black_box_score(S[i])
    if score[i] < score[i-1]:  # (2)  
        S[i] = S[i-1]
        score[i] = score[i-1]

(1) - 你的情况可能是小排列,在组之间切换 2 人。

(2) - 如果我们做出的改变使我们的解决方案变得更糟(较低的分数),我们会拒绝它。您可以稍后将其替换为也以一定概率接受更差的解决方案,以使该算法成为模拟退火

首先简单地运行 1000 次左右的迭代,然后将 score[i] 绘制为 i 的函数,以了解您的解决方案改进的速度。运行几次(尝试不同的随机起点)。

然后您可以使用不同的排列 (1),减少算法的贪婪 (2),或者添加一些花哨的自动逻辑来停止搜索(例如,在最后一次 T 迭代中没有进展)。

【讨论】:

  • 嗯,这个问题是我不确定模拟退火是否适用于如此大的解决方案空间(假设 36 名学生)。我认为任何合理的时间(甚至几天)都不会给出一个接近好的解决方案。这个空间太大了,你可以搜索很长时间都找不到任何好的东西。
  • @user5738917 模拟退火非常适合此类大型问题空间。
猜你喜欢
  • 2011-01-17
  • 2022-10-05
  • 2017-09-08
  • 2023-03-13
  • 1970-01-01
  • 1970-01-01
  • 2017-06-27
  • 1970-01-01
  • 2019-02-26
相关资源
最近更新 更多