6

2 つのリストがあるとします。各項目が 1 回だけ表示されるように、すべてのサブリストのすべての可能な組み合わせを反復するにはどうすればよいでしょうか。

例としては、従業員と仕事があり、それらをチームに分割したい場合が考えられます。各従業員は 1 つのチームにしか所属できず、各仕事は 1 つのチームにしか所属できません。例えば

List<string> employees = new List<string>() { "Adam", "Bob"}  ;
List<string> jobs      = new List<string>() { "1", "2", "3"};

私が欲しい

Adam       : 1
Bob        : 2 , 3

Adam       : 1 , 2
Bob        : 3

Adam       : 1 , 3
Bob        : 2

Adam       : 2 
Bob        : 1 , 3

Adam       : 2 , 3
Bob        : 1

Adam       : 3
Bob        : 1 , 2

Adam, Bob  : 1, 2, 3

このstackoverflowの質問への回答を使用して、可能な従業員のすべての組み合わせとジョブのすべての可能な組み合わせのリストを生成し、各リストからそれぞれから1つの項目を選択しようとしましたが、それは私が得た限りです.

リストの最大サイズはわかりませんが、確かに 100 未満であり、他の制限要因がある可能性があります (各チームに 5 人を超える従業員を配置できないなど)。

アップデート

これをもっと整理したり単純化したりできるかどうかはわかりませんが、これが私がこれまでに得たものです。

Yorye が提供する Group アルゴリズムを使用します (以下の彼の回答を参照)。

var employees = new List<string>() { "Adam", "Bob"  } ;
var jobs      = new List<string>() { "1", "2", "3"  };

int c= 0;
foreach (int noOfTeams in Enumerable.Range(1, employees.Count))
{   
    var hs = new HashSet<string>();

    foreach( var grouping in Group(Enumerable.Range(1, noOfTeams).ToList(), employees))
    {   
        // Generate a unique key for each group to detect duplicates.   
        var key = string.Join(":" , 
                      grouping.Select(sub => string.Join(",", sub)));           

        if (!hs.Add(key))
            continue;

        List<List<string>> teams = (from r in grouping select r.ToList()).ToList();

        foreach (var group in Group(teams, jobs))
        {
            foreach (var sub in group)
            {               
                Console.WriteLine(String.Join(", " , sub.Key )   + " : " + string.Join(", ", sub));
            }
            Console.WriteLine();
            c++;
        }
    }

}           
Console.WriteLine(String.Format("{0:n0} combinations for {1} employees and {2} jobs" , c , employees.Count, jobs.Count));  

結果の順序は気にしていないので、これで必要なものが得られるようです。

4

5 に答える 5

5

良い質問。

まず、コードを書き留める前に、質問の根底にある組み合わせ論を理解しましょう。

基本的に、セット A の任意のパーティションに対して、セット B に同じ数のパーツが必要である必要があります。

たとえば、セット A を 3 つのグループに分割する場合、セット B も 3 つのグループに分割する必要があります。そうしないと、少なくとも 1 つの要素が他のセットに対応するグループを持たなくなります。
例 (Adam, Bob : 1, 2, 3) のように、セット B から 1 つのグループを作成する必要があります。

これで、集合 A にはn 個の要素があり、集合 B にはk 個の要素があることがわかりました。
したがって、当然のことながら、任意のセットをそれ以上分割するように要求することはできませんMin(n,k)

両方のセットをそれぞれ 2 つのグループ (パーティション) に分割すると1*2=2!、2 つのセット間に一意のペアができたとします。
もう 1 つの例は、それぞれが1*2*3=3!一意のペアを提供する 3 つのグループです。

ただし、まだ完了していません。任意のセットがいくつかのサブセット (グループ) に分割された後でも、多くの組み合わせで要素を並べ替えることができます。
したがって、セットの分割については、要素を分割mに配置する組み合わせの数を見つける必要があります。これは、第 2 種スターリング数公式を使用して見つけることができます。nm


(式 1)第 2 種スターリング数

この式は、n要素のセットをk空でないセットに分割する方法の数を示します。

したがって、1 から 1 までのすべてのパーティションのペアの組み合わせの総数min(n,k)は次のとおりです。

(式 2)すべての組み合わせ

つまり、両方のセットのすべてのパーティションの組み合わせの合計に、ペアのすべての組み合わせを掛けたものです。

これで、データを分割してペアリングする方法を理解できたので、コードを書き留めることができます。

コード:

基本的に、最終的な式 (2) を見ると、ソリューションには 4 つの部分のコードが必要であることがわかります。
1. 合計 (またはループ)
2. 両方のセットからスターリング セットまたはパーティション
を取得する方法 3. 両方のスターリング セットのデカルト積を取得する方法。
4. セットの項目を並べ替える方法。(ン!)

StackOverflow では、アイテムを並べ替える方法とデカルト積を見つける方法の多くの方法を見つけることができます。ここに例を示します (拡張メソッドとして):

 public static IEnumerable<IEnumerable<T>> Permute<T>(this IEnumerable<T> list)
    {
        if (list.Count() == 1)
            return new List<IEnumerable<T>> { list };

        return list.Select((a, i1) => Permute(list.Where((b, i2) => i2 != i1)).Select(b => (new List<T> { a }).Union(b)))
                   .SelectMany(c => c);
    }

これはコードの簡単な部分でした。
より難しい部分 (imho) はn、セットのすべての可能なパーティションを見つけることです。
したがって、これを解決するために、最初に (サイズ n だけでなく) セットのすべての可能なパーティションを見つける方法というより大きな問題を解決しました。

私はこの再帰関数を思いつきました:

public static List<List<List<T>>> AllPartitions<T>(this IEnumerable<T> set)
    {
        var retList = new List<List<List<T>>>();

        if (set == null || !set.Any())
        {
            retList.Add(new List<List<T>>());
            return retList;
        }
        else
        {
            for (int i = 0; i < Math.Pow(2, set.Count()) / 2; i++)
            {
                var j = i;

                var parts = new [] { new List<T>(), new List<T>() };


                foreach (var item in set)
                {
                    parts[j & 1].Add(item);
                    j >>= 1;
                }

                foreach (var b in AllPartitions(parts[1]))
                {
                    var x = new List<List<T>>();

                    x.Add(parts[0]);

                    if (b.Any())
                        x.AddRange(b);

                    retList.Add(x);
                }
            }
        }
        return retList;
    }

: の戻り値はList<List<List<T>>>、パーティションがセットのリストであり、セットが要素のリストであるすべての可能なパーティションのリストを意味します。
ここで List 型を使用する必要はありませんが、インデックス作成が簡単になります。

それでは、すべてをまとめてみましょう。

メインコード

//Initialize our sets
        var employees = new [] { "Adam", "Bob" };
        var jobs = new[] {  "1", "2", "3" };

        //iterate to max number of partitions (Sum)
        for (int i = 1; i <= Math.Min(employees.Length, jobs.Length); i++)
        {
            Debug.WriteLine("Partition to " + i + " parts:");

            //Get all possible partitions of set "employees" (Stirling Set)
            var aparts = employees.AllPartitions().Where(y => y.Count == i);
            //Get all possible partitions of set "jobs" (Stirling Set)
            var bparts = jobs.AllPartitions().Where(y => y.Count == i);

            //Get cartesian product of all partitions 
            var partsProduct = from employeesPartition in aparts
                      from jobsPartition in bparts
                               select new {  employeesPartition,  jobsPartition };

            var idx = 0;
            //for every product of partitions
            foreach (var productItem in partsProduct)
            {
                //loop through the permutations of jobPartition (N!)
                foreach (var permutationOfJobs in productItem.jobsPartition.Permute())
                {
                    Debug.WriteLine("Combination: "+ ++idx);

                    for (int j = 0; j < i; j++)
                    {
                        Debug.WriteLine(productItem.employeesPartition[j].ToArrayString() + " : " + permutationOfJobs.ToArray()[j].ToArrayString());
                    }
                }
            }
        }

出力:

Partition to 1 parts:
Combination: 1
{ Adam , Bob } : { 1 , 2 , 3 }
Partition to 2 parts:
Combination: 1
{ Bob } : { 2 , 3 }
{ Adam } : { 1 }
Combination: 2
{ Bob } : { 1 }
{ Adam } : { 2 , 3 }
Combination: 3
{ Bob } : { 1 , 3 }
{ Adam } : { 2 }
Combination: 4
{ Bob } : { 2 }
{ Adam } : { 1 , 3 }
Combination: 5
{ Bob } : { 3 }
{ Adam } : { 1 , 2 }
Combination: 6
{ Bob } : { 1 , 2 }
{ Adam } : { 3 }

結果を数えるだけでアウトプットを簡単に確認できます。
この例では、2 つの要素のセットと 3 つの要素のセットがあります。式 2 は、S(2,1)S(3,1)1!+S(2,2)S(3,2) が必要であることを示しています。 )2! = 1+6 = 7
これは、得られた組み合わせの数です。

参考までに、第 2 種スターリング数の例を次に示します:
S(1,1) = 1

S(2,1) = 1
S(2,2) = 1

S(3,1) = 1
S(3,2) = 3
S(3,3) = 1

S(4,1) = 1
S(4,2) = 7
S(4,3) = 6
S(4,4) = 1

編集 19.6.2012

public static String ToArrayString<T>(this IEnumerable<T> arr)
    {
        string str = "{ ";

        foreach (var item in arr)
        {
            str += item + " , ";
        }

        str = str.Trim().TrimEnd(',');
        str += "}";

        return str;
    }

編集 24.6.2012

このアルゴリズムの主要な部分は、スターリング集合を見つけることです。私は非効率な順列法を使用しました。ここでは、QuickPerm アルゴリズムに基づくより高速な方法を示します。

public static IEnumerable<IEnumerable<T>> QuickPerm<T>(this IEnumerable<T> set)
    {
        int N = set.Count();
        int[] a = new int[N];
        int[] p = new int[N];

        var yieldRet = new T[N];

        var list = set.ToList();

        int i, j, tmp ;// Upper Index i; Lower Index j
        T tmp2;

        for (i = 0; i < N; i++)
        {
            // initialize arrays; a[N] can be any type
            a[i] = i + 1; // a[i] value is not revealed and can be arbitrary
            p[i] = 0; // p[i] == i controls iteration and index boundaries for i
        }
        yield return list;
        //display(a, 0, 0);   // remove comment to display array a[]
        i = 1; // setup first swap points to be 1 and 0 respectively (i & j)
        while (i < N)
        {
            if (p[i] < i)
            {
                j = i%2*p[i]; // IF i is odd then j = p[i] otherwise j = 0

                tmp2 = list[a[j]-1];
                list[a[j]-1] = list[a[i]-1];
                list[a[i]-1] = tmp2;

                tmp = a[j]; // swap(a[j], a[i])
                a[j] = a[i];
                a[i] = tmp;

                //MAIN!

               // for (int x = 0; x < N; x++)
                //{
                //    yieldRet[x] = list[a[x]-1];
                //}
                yield return list;
                //display(a, j, i); // remove comment to display target array a[]

                // MAIN!

                p[i]++; // increase index "weight" for i by one
                i = 1; // reset index i to 1 (assumed)
            }
            else
            {
                // otherwise p[i] == i
                p[i] = 0; // reset p[i] to zero
                i++; // set new index value for i (increase by one)
            } // if (p[i] < i)
        } // while(i < N)
    }

これにより、時間が半分に短縮されます。
ただし、CPU サイクルのほとんどは、この例で特に必要な文字列の構築に費やされます。
これは少し速くなります:

             results.Add(productItem.employeesPartition[j].Aggregate((a, b) => a + "," + b) + " : " + permutationOfJobs.ToArray()[j].Aggregate((a, b) => a + "," + b));

これらの文字列は多くのメモリを消費するため、x64 でコンパイルする方が適切です。

于 2012-06-13T06:16:55.827 に答える
3

別のライブラリを使用しても問題ありませんか?組み合わせ用の一般的なライブラリを次に示します (当然、繰り返しのないライブラリが必要です)。次に、従業員リストに対して foreach を実行し、両方で組み合わせを実行するだけです。

大きなOの観点から、あなたは自分に有利に働いているとは思いませんが、ここでは効率が優先されますか?

これは腰からのものですが、これは(そのライブラリを使用して)必要なものを取得するためのコードである必要があります。

Combinations<string> combinations = new Combinations<string>(jobs, 2);

foreach(IList<string> c in combinations) {
  Console.WriteLine(String.Format("{{{0} {1}}}", c[0], c[1]));
}

そして、それを各従業員に適用する必要があります

于 2012-06-04T20:09:53.143 に答える
1

私の答えでは、あなたが持っていた最後の結果を無視します: Adam, Bob: 1, 2, 3、これは論理的に例外であるためです。最後までスキップして、出力を確認してください。

説明:

アイデアは、「要素が属するグループ」の組み合わせを繰り返すことです。

要素「a、b、c」があり、グループ「1、2」があるとします。要素の数としてサイズ3の配列があり、グループ「1、2」のすべての可能な組み合わせが含まれます。 、繰り返しあり:

{1, 1, 1} {1, 1, 2} {1, 2, 1} {1, 2, 2}
{2, 1, 1} {2, 1, 2} {2, 2, 1} {2, 2, 2}

次に、各グループを取得し、次のロジックを使用して、そこからキーと値のコレクションを作成します。

のグループはelements[i]の値になりますcomb[i]

Example with {1, 2, 1}:
a: group 1
b: group 2
c: group 1

And in a different view, the way you wanted it:
group 1: a, c
group 2: b

結局のところ、すべてのグループに少なくとも 1 つの値を持たせたかったので、すべてのグループを含まないすべての組み合わせをフィルタリングする必要があります。

したがって、すべてのグループが特定の組み合わせで表示されるかどうかを確認し、一致しないグループをフィルタリングする必要があるため、次のものが残ります。

{1, 1, 2} {1, 2, 1} {1, 2, 2}
{2, 1, 2} {2, 2, 1} {2, 1, 1}

結果は次のようになります。

1: a, b
2: c

1: a, c
2: b

1: a
2: b, c

1: b
2: a, c

1: c
2: a, b

1: b, c
2: a

このグループ分割の組み合わせロジックは、より多くの要素とグループに対しても機能します。これが私の実装です。コーディング中に少し迷子になったので(実際には直感的な問題ではありません)、うまく機能します。

実装:

public static IEnumerable<ILookup<TValue, TKey>> Group<TKey, TValue>
    (List<TValue> keys,
     List<TKey> values,
     bool allowEmptyGroups = false)
{
    var indices = new int[values.Count];
    var maxIndex = values.Count - 1;
    var nextIndex = maxIndex;
    indices[maxIndex] = -1;

    while (nextIndex >= 0)
    {
        indices[nextIndex]++;

        if (indices[nextIndex] == keys.Count)
        {
            indices[nextIndex] = 0;
            nextIndex--;
            continue;
        }

        nextIndex = maxIndex;

        if (!allowEmptyGroups && indices.Distinct().Count() != keys.Count)
        {
            continue;
        }

        yield return indices.Select((keyIndex, valueIndex) =>
                                    new
                                        {
                                            Key = keys[keyIndex],
                                            Value = values[valueIndex]
                                        })
            .OrderBy(x => x.Key)
            .ToLookup(x => x.Key, x => x.Value);
    }
}

使用法:

var employees = new List<string>() { "Adam", "Bob"};
var jobs      = new List<string>() { "1", "2", "3"};
var c = 0;

foreach (var group in CombinationsEx.Group(employees, jobs))
{
    foreach (var sub in group)
    {
        Console.WriteLine(sub.Key + ": " + string.Join(", ", sub));
    }

    c++;
    Console.WriteLine();
}

Console.WriteLine(c + " combinations.");

出力:

Adam: 1, 2
Bob: 3

Adam: 1, 3
Bob: 2

Adam: 1
Bob: 2, 3

Adam: 2, 3
Bob: 1

Adam: 2
Bob: 1, 3

Adam: 3
Bob: 1, 2

6 combinations.

アップデート

結合されたキーの組み合わせのプロトタイプ:

public static IEnumerable<ILookup<TKey[], TValue>> GroupCombined<TKey, TValue>
    (List<TKey> keys,
     List<TValue> values)
{
    // foreach (int i in Enumerable.Range(1, keys.Count))
    for (var i = 1; i <= keys.Count; i++)
    {
        foreach (var lookup in Group(Enumerable.Range(0, i).ToList(), keys))
        {
            foreach (var lookup1 in
                     Group(lookup.Select(keysComb => keysComb.ToArray()).ToList(),
                           values))
            {
                yield return lookup1;
            }
        }
    }

    /*
    Same functionality:

    return from i in Enumerable.Range(1, keys.Count)
           from lookup in Group(Enumerable.Range(0, i).ToList(), keys)
           from lookup1 in Group(lookup.Select(keysComb =>
                                     keysComb.ToArray()).ToList(),
                                 values)
           select lookup1;
    */
}

重複の問題はまだ少しありますが、すべての結果が生成されます。

一時的な解決策として、重複を削除するために使用するものは次のとおりです。

var c = 0;
var d = 0;

var employees = new List<string> { "Adam", "Bob", "James" };
var jobs = new List<string> {"1", "2"};

var prevStrs = new List<string>();

foreach (var group in CombinationsEx.GroupCombined(employees, jobs))
{
    var currStr = string.Join(Environment.NewLine,
                              group.Select(sub =>
                                           string.Format("{0}: {1}",
                                               string.Join(", ", sub.Key),
                                               string.Join(", ", sub))));

    if (prevStrs.Contains(currStr))
    {
        d++;
        continue;
    }

    prevStrs.Add(currStr);

    Console.WriteLine(currStr);
    Console.WriteLine();
    c++;
}

Console.WriteLine(c + " combinations.");
Console.WriteLine(d + " duplicates.");

出力:

Adam, Bob, James: 1, 2

Adam, Bob: 1
James: 2

James: 1
Adam, Bob: 2

Adam, James: 1
Bob: 2

Bob: 1
Adam, James: 2

Adam: 1
Bob, James: 2

Bob, James: 1
Adam: 2

7 combinations.
6 duplicates.

結合されていないグループも生成されることに注意してください (可能な場合 - 空のグループは許可されないため)。結合されたキーのみを生成するには、これを置き換える必要があります。

for (var i = 1; i <= keys.Count; i++)

これとともに:

for (var i = 1; i < keys.Count; i++)

GroupCombined メソッドの先頭。3 人の従業員と 3 つのジョブでメソッドをテストし、正確にどのように機能するかを確認します。

別の編集:

より良い重複処理は、GroupCombined レベルで重複するキーの組み合わせを処理することです。

public static IEnumerable<ILookup<TKey[], TValue>> GroupCombined<TKey, TValue>
    (List<TKey> keys,
     List<TValue> values)
{
    for (var i = 1; i <= keys.Count; i++)
    {
        var prevs = new List<TKey[][]>();

        foreach (var lookup in Group(Enumerable.Range(0, i).ToList(), keys))
        {
            var found = false;
            var curr = lookup.Select(sub => sub.OrderBy(k => k).ToArray())
                .OrderBy(arr => arr.FirstOrDefault()).ToArray();

            foreach (var prev in prevs.Where(prev => prev.Length == curr.Length))
            {
                var isDuplicate = true;

                for (var x = 0; x < prev.Length; x++)
                {
                    var currSub = curr[x];
                    var prevSub = prev[x];

                    if (currSub.Length != prevSub.Length ||
                        !currSub.SequenceEqual(prevSub))
                    {
                        isDuplicate = false;
                        break;
                    }
                }

                if (isDuplicate)
                {
                    found = true;
                    break;
                }
            }

            if (found)
            {
                continue;
            }

            prevs.Add(curr);

            foreach (var group in
                     Group(lookup.Select(keysComb => keysComb.ToArray()).ToList(),
                           values))
            {
                yield return group;
            }
        }
    }
}

思われるように、メソッドに制約を追加するのが賢明であり、そうTKeyなる可能ICompareable<TKey>性もありIEquatable<TKey>ます。

これにより、最終的に重複が0になります。

于 2012-06-06T12:18:03.527 に答える
0

他の制約 ( team の最大サイズなど) を無視して、セットPのパーティションとセットQのパーティションを同じ数のサブセットで作成し、セットの 1 つのすべての組み合わせを見つけて最初のセットをマッピングします。秒に分割します。

Michael Orlov の論文には、この論文で各反復が一定のスペースを使用するパーティションを生成するための単純なアルゴリズムのように見えるものがあります。彼はまた、特定のサイズのパーティションを一覧表示するためのアルゴリズムも提供しています。

P = { A, B } およびQ = { 1, 2, 3 } で開始すると、サイズ 1 のパーティションは[ P ]および[ Q ]であるため、唯一のペアは( [ P ], [ Q ] ) です。

サイズ 2 のパーティションの場合、Pには 2 つの要素しかないため、サイズ 2 の 1 つのパーティションのみ[ { A }, { B } ]Qにはサイズ 2 の 3 つのパーティション[ { 1 }, { 2, 3 } ]、[ { 1 , 2 }, { 3 } ], [ { 1, 3 }, { 2 } ] .

Qの各パーティションには 2 つのサブセットが含まれているため、各パーティションには 2 つの組み合わせがあり、6 つのペアリングが得られます。

( [ { A }, { B } ], [ { 1 }, { 2, 3 } ] )
( [ { A }, { B } ], [ { 2, 3 }, { 1 } ] )
( [ { A }, { B } ], [ { 1, 2 }, { 3 } ] )
( [ { A }, { B } ], [ { 3 }, { 1, 2 } ] )
( [ { A }, { B } ], [ { 1, 3 }, { 2 } ] ) 
( [ { A }, { B } ], [ { 2 }, { 1, 3 } ] ) 

元のセットの 1 つのサイズが 2 だったので、サイズ 3 のパーティションが存在しないため、停止します。

于 2012-06-06T10:14:04.590 に答える
0

A と B の 2 つのセットがあるとします。
A = {a1, a2, a3} ; B = {b1, b2, b3}

まず、サブセットを含むタプルとその補数からなるセットを取得しましょう。

{a1} {a2 a3} 
{a2} {a1 a3}
{a3} {a1 a2}
{a2, a3} {a1}
...

これには、上記の Rikon のライブラリを使用するか、独自のコードを記述できます。これを行う必要があります:

  1. すべてのサブセットのセットを取得します (パワー セットと呼ばれます)
  2. サブセットごとに、より大きなセットからそのサブセットの補数または残りの要素を取得します。
  3. これらをタプルまたはペアに結合します。例 (サブセット、補足)

ここでは順序が重要です。{a1} {a2 a3} and {a2 a3} {a1}結局のところ、異なるタプルです。

次に、B について同様のセットを取得します。次に、2 つのセット間で交差結合を実行して、次を取得します。

{a1} {a2 a3} | {b1} {b2 b3}

{a2} {a1 a3} | {b2} {b1 b3}
...

これは、上記の説明とほぼ一致します。{Bob, Adam} を 1 つのセットとして、{1, 2, 3} を別のセットとして見てください。要件に応じて、空のセットの一部をダンプする必要があります (パワー セットには空のサブセットも含まれるため)。ただし、これは私が知る限り、一般的な要点です。コードが不足していて申し訳ありません。寝なきゃ :P

于 2012-06-04T22:28:56.560 に答える