【问题标题】:Removing items from unevenly distributed set从不均匀分布的集合中删除项目
【发布时间】:2012-03-07 09:29:06
【问题描述】:

我有一个网站,用户可以在该网站上提交问题(每天零个、一个或多个),对它们进行投票并每天回答一个问题(更多详情 here)。用户只能通过提交、投票或回答来查看问题一次。

我有很多玩家已经看过的问题。我每个月需要从题库中删除 30 个问题。我需要选择要删除的问题,以使池中剩余的可用问题数量最大化,以供可用问题最少的玩家使用。

示例包含 5 个问题(需要删除 3 个):

  • 玩家 A 看过问题 1、3 和 5
  • 玩家 B 看过问题 1 和 4
  • 玩家 C 看过问题 2 和 4

我虽然想删除顶级玩家看到的问题,但位置会改变。按照上面的例子,玩家 A 只剩下 2 个问题要玩(2 和 4)。但是,如果我删除 1、3 和 5,情况将是:

  • 玩家 A 可以玩问题 2 和 4
  • 玩家 B 可以玩问题 2
  • 玩家 C 不能玩任何东西,因为 1,3,5 被移除了,他已经看到了 2 和 4。

此解决方案的分数为零,即可用问题最少的玩家有零个可用问题可玩。

在这种情况下,最好删除 1、3 和 4,给出:

  • 玩家 A 可以玩问题 2
  • 玩家 B 可以玩问题 2 和 5
  • 玩家 C 可以玩第 5 题

此解决方案的得分为 1,因为可玩问题最少的两名玩家有一个可用问题。

如果数据量很小,我将能够暴力破解解决方案。但是,我有数百名玩家和问题,所以我正在寻找一些算法来解决这个问题。

【问题讨论】:

  • 为什么要删除问题?为什么每个月恰好删除 30 个问题?
  • 玩家帐号分为两组:为排名而玩的玩家和为娱乐而玩的玩家。为排名而战的玩家需要得到独特的问题,这就是为什么需要隐藏的原因。每个月有 30 个问题从排名玩家池中删除到公共娱乐池中。因此,实际上并不是 30 天,而是与给定月份的天数一样多。
  • 我怀疑你会通过重复删除剩余集合中免费问题最少的玩家回答的问题来获得一个非常好的、快速和简单的近似值。如果有多个可用选项,可能会选择答案最多的问题。
  • @JoelCornett,将问题限制在少于 60 个问题的用户是没有用的。但是将问题限制在与剩余问题数量最少的用户相差小于 30 的用户上将是一个很好的优化。
  • @JoelCornett,没有要求为下个月保留 30 个问题。这样的要求要么太硬(因为用户可能会在下个月初收到很多新问题),要么太软(以防未来 32 天没​​有新问题)。

标签: algorithm combinatorics


【解决方案1】:

假设您有一个通用的高效算法。专注于剩下的问题,而不是删除的问题。

您可以使用这样的算法来解决问题 - 您可以选择最多 T 个问题,使得每个用户至少有一个问题要回答吗?我认为这是http://en.wikipedia.org/wiki/Set_cover,我认为解决你的问题通常可以让你解决集合覆盖,所以我认为它是NP-complete。

至少存在线性规划松弛。将每个问题与 0= X,它在 Qj 和 X 中是线性的,因此您可以使用线性变量 X 和 Qj 最大化目标函数 X。不幸的是,结果不需要给您整数 Qj - 例如,考虑所有可能的问题对与某个用户相关联并且您希望每个用户能够回答至少 1 个问题,最多使用一半问题的情况。最优解是所有 i 的 Qi = 1/2。

(但考虑到线性规划松弛,您可以将其用作http://en.wikipedia.org/wiki/Branch_and_bound 中的界限)。

或者,如果你手头有一个,你也可以把问题写下来,然后扔到一个整数线性规划包中。

【讨论】:

  • 我之前使用过 GNU GLPK (glpsol),我很乐意以 .cplex 格式编写问题,但我似乎无法写下问题。让我们看看我在哪里犯了错误。我相信 Qi = 0 意味着问题被删除,而 Qi = 1 意味着问题被保留。但是如何编写最大化函数呢?我想最大限度地为问题最少的用户留下问题。我不知道如何把它写成线性规划问题。
  • 请注意,我定义了一个额外的变量 X。我的目标函数就是 X 的值:Objective(X, Q1, Q2...) = X。X 是问题的数量最少的用户,这是由约束 SUM_j Uij Qj >= X 强制执行的。这是aimms.com/aimms/download/manuals/…中的第一个技巧(第 6.2 节)
  • 维基百科文章说,已经证明 Set Cover 不能在多项式时间内比简单的贪婪方法更好地逼近。这不是也在这里发挥作用,使基于 LP 的近似过度杀伤吗?
  • LP within branch and bound,或 ILP,如果你能负担得起的话,比贪婪的集合覆盖更有优势,它可以提供一个已知的最佳答案。当您对廉价的近似答案感到满意时,网络搜索会通过 web.njit.edu/~yang/research/covering/covering.pdf 显示 ILP 可能确实是矫枉过正 - 谢谢。
  • Branch and Bound 可以使用任何近似值。当然,假设这实际上是设置封面。 LPR 是一个不错的选择 :)
【解决方案2】:

为了线程的完整性,这里是一个简单的贪婪的近似方法。

将已解决的问题放在前面讨论的矩阵形式中:

Q0    X
Q1  XX
Q2    X
Q3  X  
Q4   XX
    223

按解决的问题数量排序:

Q0  X  
Q1   XX
Q2  X  
Q3    X
Q4  XX 
    322

在解决问题最多的玩家中删除Xs 最多的问题。 (如果有的话,这保证会减少我们的测量值):

=======
Q1   XX
Q2  X  
Q3    X
Q4  XX 
    222

再次排序:

=======
Q1   XX
Q2  X  
Q3    X
Q4  XX 
    222

再次罢工:

=======
=======
Q2  X  
Q3    X
Q4  XX 
    211

再次排序:

=======
=======
Q2  X  
Q3    X
Q4  XX 
    211

再次罢工:

=======
=======
Q2  X  
Q3    X
=======
    101

它是O(n^2logn),没有经过优化,因此对于数百个问题来说速度非常快。它也很容易实现。

从这个带有 2 次打击的反例中可以看出,这不是最优的:

Q0 X     
Q1      X
Q2 XXX
Q3    XXX
Q4  XXXX
Q5 222222

这里的贪婪方法将删除Q5Q2(或Q3),而不是Q2Q3,这对于我们的测量来说是最佳的。

【讨论】:

    【解决方案3】:

    我基于这样的想法提出了一系列优化,即您确实希望为问题最少的玩家最大化未见问题的数量,并且不在乎是否有 1 个玩家的问题数量最少或10000 名玩家有同样数量的问题。

    第 1 步:找到问题最少的玩家(在您的示例中,这将是玩家 A)呼叫该玩家 p。

    第 2 步:找出所有玩家 p 未看过的问题数量在 30 以内的所有玩家。称这组为 P。P 是唯一需要考虑的玩家,因为从任何其他玩家那里移除 30 个看不见的问题仍然会使他们比玩家 p 有更多的看不见的问题,因此玩家 p 的情况仍然会更糟。

    步骤 3:找到玩家在 P 中看到的所有问题集的交集,您可以删除该集中的所有问题,希望将您从 30 个减少到要删除的较小数量的问题,我们将其称为 r。 r

    第四步:找到P中玩家看到的所有问题集合的并集,称这个集合为U。如果U的大小

    您现在只剩下最初的问题了,但很可能是更小的集合。
    你的问题集是 U,你的玩家集是 P,你必须移除 r 个问题。

    蛮力方法需要时间(大小(U)选择r)*大小(P)。如果这些数字是合理的,你可以暴力破解它。这种方法是从 U 中选择每组 r 个问题,并针对 P 中的所有参与者进行评估。

    由于您的问题确实看起来是 NP 完全的,因此您可能希望的最好结果是一个近似值。最简单的方法是设置一些最大尝试次数,然后随机选择和评估要删除的问题集。因此,需要一个随机执行 U 选择 r 的函数。这可以在O(r)的时间内完成,(其实今天早些时候我已经回答了如何做到这一点!)

    Select N random elements from a List<T> in C#

    您还可以通过权衡每个问题被选中的机会,将其他用户建议的任何启发式方法放入您的选择中,我相信上面的链接显示了如何在所选答案中做到这一点。

    【讨论】:

      【解决方案4】:

      线性规划模型。

      变体 1。

      Sum(Uij * Qj) - Sum(Dij * Xj) + 0     =  0 (for each i)
      0             + Sum(Dij * Xj) - Score >= 0 (for each i)
      Sum(Qj) = (Number of questions - 30)
      Maximize(Score)
      

      如果用户i没有看到问题j,则Uij1,否则是0

      Dij是单位矩阵的元素(Dij=1如果i=j,否则是0

      Xj 是辅助变量(每个用户一个)

      变体 2。

      Sum(Uij * Qj) >= Score (for each i)
      Sum(Qj) = (Number of questions - 30)
      No objective function, just check feasibility
      

      在这种情况下,LP问题更简单,但Score应该通过二分法和线性搜索来确定。将当前范围设置为 [0 .. 用户未见问题的最少数量],将 Score 设置为范围的中间,应用整数 LP 算法(时间限制较小)。如果没有找到解决方案,则将范围设置为 [begin ..Score],否则将其设置为 [Score..end] 并继续二分查找。

      (可选)使用二分搜索来确定精确解的上限Score

      从最好的Score开始,通过二分查找,对Score应用整数LP算法,增加1、2、... (并根据需要限制计算时间)。最后,您要么得到精确解,要么得到一些好的近似值。

      这是 GNU GLPK 的 C 示例代码(用于变体 1):

      #include <stdio.h>
      #include <stdlib.h>
      #include <glpk.h>
      
      int main(void)
      {
        int ind[3000];
        double val[3000];
        int row;
        int col;
        glp_prob *lp;
      
        // Parameters
        int users = 120;
        int questions = 10000;
        int questions2 = questions - 30;
        int time = 30; // sec.
      
        // Create GLPK problem
        lp = glp_create_prob();
        glp_set_prob_name(lp, "questions");
        glp_set_obj_dir(lp, GLP_MAX);
      
        // Configure rows
        glp_add_rows(lp, users*2 + 1);
        for (row = 1; row <= users; ++row)
        {
          glp_set_row_bnds(lp, row, GLP_FX, 0.0, 0.0);
          glp_set_row_bnds(lp, row + users, GLP_LO, 0.0, 0.0);
        }
        glp_set_row_bnds(lp, users*2 + 1, GLP_FX, questions2, questions2);
      
        // Configure columns
        glp_add_cols(lp, questions + users + 1);
        for (col = 1; col <= questions; ++col)
        {
          glp_set_obj_coef(lp, col, 0.0);
          glp_set_col_kind(lp, col, GLP_BV);
        }
        for (col = 1; col <= users; ++col)
        {
          glp_set_obj_coef(lp, questions + col, 0.0);
          glp_set_col_kind(lp, questions + col, GLP_IV);
          glp_set_col_bnds(lp, questions + col, GLP_FR, 0.0, 0.0);
        }
        glp_set_obj_coef(lp, questions+users+1, 1.0);
        glp_set_col_kind(lp, questions+users+1, GLP_IV);
        glp_set_col_bnds(lp, questions+users+1, GLP_FR, 0.0, 0.0);
      
        // Configure matrix (question columns)
        for(col = 1; col <= questions; ++col)
        {
          for (row = 1; row <= users*2; ++row)
          {
            ind[row] = row;
            val[row] = ((row <= users) && (rand() % 2))? 1.0: 0.0;
          }
          ind[users*2 + 1] = users*2 + 1;
          val[users*2 + 1] = 1.0;
          glp_set_mat_col(lp, col, users*2 + 1, ind, val);
        }
      
        // Configure matrix (user columns)
        for(col = 1; col <= users; ++col)
        {
          for (row = 1; row <= users*2; ++row)
          {
            ind[row] = row;
            val[row] = (row == col)? -1.0: ((row == col + users)? 1.0: 0.0);
          }
          ind[users*2 + 1] = users*2 + 1;
          val[users*2 + 1] = 0.0;
          glp_set_mat_col(lp, questions + col, users*2 + 1, ind, val);
        }
      
        // Configure matrix (score column)
        for (row = 1; row <= users*2; ++row)
        {
          ind[row] = row;
          val[row] = (row > users)? -1.0: 0.0;
        }
        ind[users*2 + 1] = users*2 + 1;
        val[users*2 + 1] = 0.0;
        glp_set_mat_col(lp, questions + users + 1, users*2 + 1, ind, val);
      
        // Solve integer GLPK problem
        glp_iocp param;
        glp_init_iocp(&param);
        param.presolve = GLP_ON;
        param.tm_lim = time * 1000;
        glp_intopt(lp, &param);
        printf("Score = %g\n", glp_mip_obj_val(lp));
      
        glp_delete_prob(lp);
        return 0;
      }
      

      时间限制在我的测试中无法可靠运行。看起来像是 GLPK 中的一些错误...

      变体 2 的示例代码(仅 LP 算法,不会自动搜索 Score):

      #include <stdio.h>
      #include <stdlib.h>
      #include <glpk.h>
      
      int main(void)
      {
        int ind[3000];
        double val[3000];
        int row;
        int col;
        glp_prob *lp;
      
        // Parameters
        int users = 120;
        int questions = 10000;
        int questions2 = questions - 30;
        double score = 4869.0 + 7;
      
        // Create GLPK problem
        lp = glp_create_prob();
        glp_set_prob_name(lp, "questions");
        glp_set_obj_dir(lp, GLP_MAX);
      
        // Configure rows
        glp_add_rows(lp, users + 1);
        for (row = 1; row <= users; ++row)
        {
          glp_set_row_bnds(lp, row, GLP_LO, score, score);
        }
        glp_set_row_bnds(lp, users + 1, GLP_FX, questions2, questions2);
      
        // Configure columns
        glp_add_cols(lp, questions);
        for (col = 1; col <= questions; ++col)
        {
          glp_set_obj_coef(lp, col, 0.0);
          glp_set_col_kind(lp, col, GLP_BV);
        }
      
        // Configure matrix (question columns)
        for(col = 1; col <= questions; ++col)
        {
          for (row = 1; row <= users; ++row)
          {
            ind[row] = row;
            val[row] = (rand() % 2)? 1.0: 0.0;
          }
          ind[users + 1] = users + 1;
          val[users + 1] = 1.0;
          glp_set_mat_col(lp, col, users + 1, ind, val);
        }
      
        // Solve integer GLPK problem
        glp_iocp param;
        glp_init_iocp(&param);
        param.presolve = GLP_ON;
        glp_intopt(lp, &param);
      
        glp_delete_prob(lp);
        return 0;
      }
      

      似乎变体 2 可以很快找到相当好的近似值。 并且近似值优于变体 1。

      【讨论】:

      • 据我了解,我应该只将 rand() 替换为 0 或 1,具体取决于用户是否看到了问题。内部 for() 循环上升到 users*2 所以我不知道该放什么。假设我有一个返回 1 或 0 的函数,如下所示:int user_seen_question(int user, int question),我将如何用这个函数替换 rand()?
      • 变体 2 的问题:4869.0 来自哪里?
      • (rand() % 2)? 1.0: 0.0 替换为user_seen_question(int user, int question)? 0.0: 1.0。您也可以替换 questions2 和(对于变体 2)score。所有其他值是oneszeros,用于单位矩阵、忽略变量和问题计数,不要更改它们。实际上,样本中的所有矩阵系数与我答案顶部的模型描述中的顺序完全相同。
      • 4869.0 和 4869.0+7 是二分查找的结果,我是手动完成的。 4869.0 是近似的最佳分数。 4869.0+7是最低分,这绝对让问题不可行。确切的值介于两者之间。如果你有不同的 RNG 实现,你会得到不同的分数值。
      • 在您的示例中,变量是“row”和“col”,而不是“user”和“question”。在 variant2 中,这很容易,而不是 rand(),我写的是 user_seen_question(row, col)。但是,当行可以是 2*users 时,变量 1 中应该写什么?
      【解决方案5】:

      假设您想从池中删除Y 问题。简单的算法是按照问题的浏览量对问题进行排序。然后,您删除查看次数最多的问题中的Y。对于您的示例:1:2、2:1、3:1、4:2、5:1。显然,您最好删除问题1和4。但是这种算法没有达到目标。然而,这是一个很好的起点。要改进它,您需要确保每个用户在“清理”之后至少会收到X 的问题。

      除了上面的数组(我们可以称之为“分数”)之外,您还需要第二个包含问题和用户的数组,如果用户看到了问题,cross 将有 1,如果他没有看到,则为 0。然后,对于每个用户,您需要找到得分最低的X 问题编辑:他还没有看过(他们的得分越低越好,因为看到问题的人越少,越多“有价值的”它是对整个系统而言的)。您将所有从每个用户找到的X 问题组合到第三个数组中,我们称之为“安全”,因为我们不会从中删除任何问题。

      作为最后一步,您只需删除不在“安全”数组中的Y 热门问题(得分最高的问题)。

      该算法还实现了如果删除说 30 个问题会使某些用户查看的问题少于X,它不会删除所有 30 个问题。我想这对系统有好处。

      编辑:对此的良好优化将不是跟踪每个用户,而是有一些活动基准来过滤只看到几个问题的人。因为如果有太多人只看到了 1 个罕见的不同问题,那么什么都不能删除。过滤这类用户或改进安全数组功能即可解决。

      如果我对这个想法的描述不够深入,请随时提出问题。

      【讨论】:

      • 有趣的想法。假设 Y 为 30,我如何确定 X 的值?
      • 您指定X,未确定。例如,您可以从X = 1 开始,看看结果如何,然后通过运行X = 2, 3, 4 的算法,您将看到可以多快删除问题。附带说明一下,您确实需要过滤掉“不太活跃”的用户才能让这个想法发挥作用。
      【解决方案6】:

      您是否考虑过从动态编程解决方案的角度来看待这个问题?

      我认为您可以通过最大限度地增加未决问题的数量来做到这一点 给所有玩家,这样没有一个玩家的开放问题为零。

      以下链接很好地概述了如何构造dynamic programming 这类问题的解决方案。

      【讨论】:

        【解决方案7】:

        以仍然可以玩的问题的形式提出这一点。我将问题编号从 0 到 4 而不是 1 到 5,因为这样在编程中更方便。

                  01234
                  -----
        player A   x x   - player A has just 2 playable questions
        player B   xx x  - player B has 3 playable questions
        player C  x x x  - player C has 3 playable questions
        

        我将首先描述一个看似非常幼稚的算法,但最后我将展示如何显着改进它。

        对于 5 个问题中的每一个问题,您都需要决定是保留还是放弃它。这将需要一个深度为 5 的递归函数。

        vector<bool> keep_or_discard(5); // an array to store the five decisions
        
        void decide_one_question(int question_id) {
            // first, pretend we keep the question
            keep_or_discard[question_id] = true;
            decide_one_question(question_id + 1); // recursively consider the next question
        
            // then, pretend we discard this question
            keep_or_discard[question_id] = false;
            decide_one_question(question_id + 1); // recursively consider the next question
        }
        
        decide_one_question(0); // this call starts the whole recursive search
        

        第一次尝试将陷入无限递归下降并跑过数组的末尾。我们需要做的第一件事是在 question_id == 5 时立即返回(即当所有问题 0 到 4 都已确定时。我们将此代码添加到 decision_one_question 的开头:

        void decide_one_question(int question_id) {
            {
                if(question_id == 5) {
                    // no more decisions needed.
                    return;
                }
            }
            // ....
        

        接下来,我们知道我们可以保留多少问题。打电话给allowed_to_keep。在这种情况下,这是 5-3,这意味着我们要保留两个问题。您可以在某处将其设置为全局变量。

        int allowed_to_keep; // set this to 2
        

        现在,我们必须在decision_one_question 的开头添加进一步的检查,并添加另一个参数:

        void decide_one_question(int question_id, int questions_kept_so_far) {
            {
                if(question_id == 5) {
                    // no more decisions needed.
                    return;
                }
                if(questions_kept_so_far > allowed_to_keep) {
                    // not allowed to keep this many, just return immediately
                    return;
                }
                int questions_left_to_consider = 5 - question_id; // how many not yet considered
                if(questions_kept_so_far + questions_left_to_consider < allowed_to_keep) {
                    // even if we keep all the rest, we'll fall short
                    // may as well return. (This is an optional extra)
                    return;
                }
            }
        
            keep_or_discard[question_id] = true;
            decide_one_question(question_id + 1, questions_kept_so_far + 1);
        
            keep_or_discard[question_id] = false;
            decide_one_question(question_id + 1, questions_kept_so_far );
        }
        
        decide_one_question(0,0);
        

        (注意这里的一般模式:我们允许递归函数调用“太深”一层。我发现在函数开始时检查“无效”状态比试图避免无效更容易首先是函数调用。)

        到目前为止,这看起来很幼稚。这是检查每一个组合。忍受我!

        我们需要开始跟踪分数,以便记住最好的(并为以后的优化做准备)。首先是编写一个函数calculate_score。并拥有一个名为best_score_so_far 的全局变量。我们的目标是最大化它,因此应该在算法开始时将其初始化为-1

        int best_score_so_far; // initialize to -1 at the start
        
        void decide_one_question(int question_id, int questions_kept_so_far) {
            {
                if(question_id == 5) {
                    int score = calculate_score();
                    if(score > best_score_so_far) {
                        // Great!
                        best_score_so_far = score;
                        store_this_good_set_of_answers();
                    }
                    return;
                }
                // ...
        

        接下来,最好在我们递归关卡时跟踪分数的变化情况。让我们从乐观开始;假设我们可以保留每个问题并计算分数并将其命名为upper_bound_on_the_score。每次递归调用自身时,都会将其副本传递给函数,并且每次决定丢弃问题时都会在本地更新

        void decide_one_question(int question_id
                               , int questions_kept_so_far
                               , int upper_bound_on_the_score) {
             ... the checks we've already detailed above
        
            keep_or_discard[question_id] = true;
            decide_one_question(question_id + 1
                  , questions_kept_so_far + 1
                  , upper_bound_on_the_score
                );
        
            keep_or_discard[question_id] = false;
        
            decide_one_question(question_id + 1
                  , questions_kept_so_far
                  , calculate_the_new_upper_bound()
                );
        

        在最后一个代码 sn-p 的末尾附近,根据丢弃问题“question_id”的决定,计算了一个新的(较小的)上限。

        在递归的每一层,这个上限都在变小。每个递归调用要么保留问题(不改变这个乐观界限),要么决定丢弃一个问题(导致这部分递归搜索的界限更小)。

        优化

        现在我们知道了一个上限,我们可以在函数一开始就进行以下检查,无论此时已经决定了多少个问题

        void decide_one_question(int question_id
                               , int questions_kept_so_far
                               , upper_bound_on_the_score) {
                if(upper_bound_on_the_score < best_score_so_far) {
                    // the upper bound is already too low,
                    // therefore, this is a dead end.
                    return;
                }
                if(question_id == 5) // .. continue with the rest of the function.
        

        此检查确保一旦找到“合理”解决方案,该算法将迅速放弃所有“死胡同”搜索。然后它会(希望)迅速找到越来越好的解决方案,然后它可以更加积极地修剪死枝。我发现这种方法在实践中非常适合我。

        如果它不起作用,还有许多进一步优化的途径。我不会尝试将它们全部列出,您当然可以尝试完全不同的方法。但我发现这在极少数情况下我必须进行类似的搜索时工作。

        【讨论】:

          【解决方案8】:

          这是一个整数程序。如果玩家i 没有看到问题j0,则让常量unseen(i, j)1。如果要保留问题j,则令变量kept(j)1,否则为0。让变量score 成为目标。

          maximize score                                       # score is your objective
          subject to
          
          for all i,  score <= sum_j (unseen(i, j) * kept(j))  # score is at most
                                                               # the number of questions
                                                               # available to player i
          
          sum_j (1 - kept(j)) = 30                             # remove exactly
                                                               # 30 questions
          
          for all j,  kept(j) in {0, 1}                        # each question is kept
                                                               # or not kept (binary)
          
          (score has no preset bound; the optimal solution chooses score
           to be the minimum over all players of the number of questions
           available to that player)
          

          【讨论】:

            【解决方案9】:

            如果暴力破解的选项太多,并且可能有许多接近最优的解决方案(听起来确实如此),请考虑使用蒙特卡罗方法。

            您有一个明确定义的适应度函数,因此只需进行一些随机分配即可对结果进行评分。冲洗并重复,直到时间用完或满足其他条件。

            【讨论】:

              【解决方案10】:

              这个问题一开始看起来很简单,但是深入思考之后你会发现它的难度。

              最简单的选择是删除最多用户看到的问题。但这并没有考虑每个用户的剩余问题数。删除后可能给一些用户留下的问题太少了。

              更复杂的解决方案是在删除问题后计算每个用户的剩余问题数。您需要为每个问题和每个用户计算它。如果您有很多用户和问题,此任务可能会很耗时。然后您可以总结所有用户留下的问题数量。并选择总和最高的问题。

              我认为将用户剩余问题的数量限制在一个合理的值是明智的。你可以想“好的,如果这个用户有超过 X 个问题,他有足够的问题可以查看”。您需要这样做,因为在删除问题后,活动用户可能只剩下 15 个问题,而很少访问的用户可能会留下 500 个问题。将 15 和 500 相加是不公平的。相反,您可以将阈值定义为 100。

              为了更容易计算,您可以只考虑查看超过 X 个问题的用户。

              【讨论】:

              • 事实上,您只需要考虑剩下 60 个或更少问题的用户。
              猜你喜欢
              • 2022-11-13
              • 1970-01-01
              • 1970-01-01
              • 2021-04-23
              • 1970-01-01
              • 2021-07-07
              • 1970-01-01
              • 2012-10-11
              • 2013-01-17
              相关资源
              最近更新 更多