使用LINQ“公平地”分组结果

时间:2011-04-14 09:02:29

标签: c# .net linq

我有一个等待分配帐户系统用户列表。
分配算法非常简单,分配应该尽可能公平,这意味着如果我有40个帐户和20个系统用户,我需要为每个系统用户分配2个帐户。
如果我有41个帐户和20个系统用户,我需要为每个系统用户分配2个帐户,并再次在系统用户之间拆分剩余帐户(在这种情况下,将为一个系统用户分配一个额外帐户)。 我试图在使用LINQ查询时弄清楚如何做到这一点 到目前为止,我认为应该涉及分组,我的查询如下:

from account in accounts
    let accountsPerSystemUser = accounts.Count / systemUsers.Count
    let leftover = accounts.Count % systemUsers.Count
    from systemUser in systemUsers
        group account by systemUser into accountsGroup
select accountsGroup

但我不确定如何从这里开始。
我很肯定我在这里错过了一个where子句,如果你达到了分配给系统用户的最大帐户数量,它将阻止分组。 如何正确实现查询,以便分组知道分配多少?

3 个答案:

答案 0 :(得分:2)

这是一个简单的实现,如果您可以将自己限制为IList<T> accounts(您可以随时使用ToList)。

public static IEnumerable<IGrouping<TBucket, TSource>> DistributeBy<TSource, TBucket>(
    this IEnumerable<TSource> source, IList<TBucket> buckets)
{
    var tagged = source.Select((item,i) => new {item, tag = i % buckets.Count});
    var grouped = from t in tagged
                  group t.item by buckets[t.tag];
    return grouped;
}

// ...
var accountsGrouped = accounts.DistributeBy(systemUsers);

基本上,这会抓取每个帐户的索引和“标记”,每个帐户的整数除法的余数除以系统用户的数量。这些标签是他们将属于的系统用户的索引。然后它只是由系统用户在该索引处对它们进行分组。

这可以确保您的公平性要求,因为余数将在0到1之间循环减去系统用户数。

0 % 20 = 0
1 % 20 = 1
2 % 20 = 2
...
19 % 20 = 19
20 % 20 = 0
21 % 21 = 1
22 % 22 = 2
...
39 % 20 = 19
40 % 20 = 0

答案 1 :(得分:1)

你不能使用“纯LINQ”(即使用查询理解语法)来做到这一点,说实话,LINQ可能不是最好的方法。尽管如此,这是一个如何实现它的例子:

var listB = new List<string>() { "a", "b", "c", "d", "e" };
var listA = new List<string>() { "1", "2", "3" };

var groupings = (from b in listB.Select((b, i) => new
                                        {
                                            Index = i,
                                            Element = b
                                        })
                 group b.Element by b.Index % listA.Count).Zip(listA, (bs, a) => new
                                                                      {
                                                                          A = a,
                                                                          Bs = bs
                                                                      });

foreach (var item in groupings)
{
    Console.WriteLine("{0}: {1}", item.A, string.Join(",", item.Bs));
}

输出:

1: a,d
2: b,e
3: c

答案 2 :(得分:1)

我不瘦“纯”LINQ真的很适合解决这个问题。然而,这是一个只需要两个IEnumerable的解决方案:

var users = new[] { "A", "B", "C" };
var accounts = new[] { 1, 2, 3, 4, 5, 6, 7, 8 };
var accountsPerUser = accounts.Count()/users.Count();
var leftover = accounts.Count()%users.Count();
var assignments = users
  .Select((u, i) => new {
    User = u,
    AccountsToAssign = accountsPerUser + (i < leftover ? 1 : 0),
    AccountsAlreadyAssigned =
      (accountsPerUser + 1)*(i < leftover ? i : leftover)
      + accountsPerUser*(i < leftover ? 0 : i - leftover)
  })
  .Select(x => new {
    x.User,
    Accounts = accounts
      .Skip(x.AccountsAlreadyAssigned)
      .Take(x.AccountsToAssign)
  });

要减少文字,我使用的是User而不是SystemUser

这个想法非常简单。第一个leftover用户从accountsPerUser + 1分配了accounts。其余用户仅被分配accountsPerUser

第一个Select使用提供索引来计算这些值的重载:

User | Index | AccountsAlreadyAssigned | AccountsToAssign
-----+-------+-------------------------+-----------------
A    | 0     | 0                       | 3
B    | 1     | 3                       | 3
C    | 1     | 6                       | 2

第二个Select将这些值用于SkipTake来自accounts的正确数字。

如果您愿意,可以“合并”两个Select语句,并将AccountsAlreadyAssignedAccountsToAssign替换为用于计算它们的表达式。但是,这将使查询真的很难理解。

这是“非LINQ”替代方案。它基于IList,但很容易转换为IEnumerable。或者不是将赋值作为元组返回,而是可以在循环内执行赋值。

IEnumerable<Tuple<T, IList<U>>> AssignEvenly<T, U>(IList<T> targetItems, IList<U> sourceItems) {
  var fraction = sourceItems.Count/targetItems.Count;
  var remainder = sourceItems.Count%targetItems.Count;
  var sourceIndex = 0;
  for (var targetIndex = 0; targetIndex < targetItems.Count; ++targetIndex) {
    var itemsToAssign = fraction + (targetIndex < remainder ? 1 : 0);
    yield return Tuple.Create(
      targetItems[targetIndex],
      (IList<U>) sourceItems.Skip(sourceIndex).Take(itemsToAssign).ToList()
    );
    sourceIndex += itemsToAssign;
  }
}