【问题标题】:Shuffle list with some conditions带有某些条件的随机播放列表
【发布时间】:2011-08-19 10:45:05
【问题描述】:

我有一个可以使用Equals() 轻松比较的元素列表。我必须洗牌,但洗牌必须满足一个条件:

第 i 个元素 shuffledList[i] 不能等于 i +/- 1 的元素,也不能等于 i +/- 2 的元素。该列表应被视为循环;也就是说,列表中的最后一个元素后面跟着第一个元素,反之亦然。

另外,如果可能的话,我想检查一下洗牌是否可能。

注意:

我使用的是 c# 4.0。

编辑:

根据一些回复,我再解释一下:

  • 该列表不会有超过 200 个元素,因此没有真正需要好的性能。如果计算它需要 2 秒,这不是最好的事情,但也不是世界末日。打乱列表会被保存,除非真实列表发生变化,否则将使用打乱列表。

  • 是的,这是一种“受控”随机性,但我希望在此方法上运行的多个随机数会返回不同的随机列表。

  • 在尝试以下一些回复后,我会做进一步的修改。

编辑 2:

  • Sehe 实施失败的两个示例列表(都有解决方案):

样品1:

`List<int> list1 = new List<int>{0,1,1,1,2,2,2,3,3,3,4,4,4,5,5,6,6,6,7,7,7,7,8,8,8,8,9,9,9,9,9,10};`

可能的解决方案:

List<int> shuffledList1 = new List<int> {9,3,1,4,7,9,2,6,8,1,4,9,2,0,6,5,7,8,4,3,10,9,6,7,8,5,3,9,1,2,7,8}

示例 2:

`List<int> list2 = new List<int> {0,1,1,2,2,2,3,3,4,4,4,4,5,5,5,6,6,6,7,7,7,7,8,8,8,8,8,9,9,9,9,10};`
  • 验证:我正在使用这种方法,它不是我编写的最有效或最优雅的代码,但它确实有效:

    public bool TestShuffle<T>(IEnumerable<T> input)
    {
        bool satisfied = true;
        int prev1 = 0; int prev2 = 0;
        int next1 = 0; int next2 = 0;
        int i = 0;
        while (i < input.Count() && satisfied)
        {
            prev1 = i - 1; prev2 = i - 2; next1 = i + 1; next2 = i + 2;
            if (i == 0)
            {
                prev1 = input.Count() - 1;
                prev2 = prev1 - 1;
            }
            else if (i == 1)
                prev2 = input.Count() - 1;
            if (i == (input.Count() - 1))
            {
                next1 = 0;
                next2 = 1;
            }
            if (i == (input.Count() - 2))
                next2 = 0;
            satisfied =
                    (!input.ElementAt(i).Equals(input.ElementAt(prev1)))
                && (!input.ElementAt(i).Equals(input.ElementAt(prev2)))
                && (!input.ElementAt(i).Equals(input.ElementAt(next1)))
                && (!input.ElementAt(i).Equals(input.ElementAt(next2)))
            ;
            if (satisfied == false)
                Console.WriteLine("TestShuffle fails at " + i);
            i++;
        }
        return satisfied;
    }
    

编辑 3:

另一个有时会失败的测试输入:

List<int> list3 = new List<int>(){0,1,1,2,2,3,3,3,4,4,4,5,5,5,5,6,6,6,6,7,7,7,8,8,8,8,9,9,9,9,10,10};

【问题讨论】:

  • 洗牌是否需要公平,即每个元素的所有目的地都同样可能?例如费雪-耶茨洗牌。
  • @Eric:这不可能是真正的公平。想象一个包含 4 或 5 个条目的输入列表。有了这些条件,如果你问我,结果顺序几乎是固定的
  • 是的,仔细想想,需要一个更好的解释。你能明确说明哪些索引指的是原始列表,哪些指的是它的改组版本?
  • @sehe:即使相对顺序完全固定,条件也可以通过同样可能的循环换位来满足。
  • @sehe:我同意这个要求。

标签: c# algorithm sorting


【解决方案1】:

优化版

令我失望的是,我优化的函数的运行速度只比 LINQ 的“直截了​​当”版本快 7 倍。未优化的 LINQ 1m43s 已优化 14.7s

  • linux 32 位
  • 带有-optimize+ 的Mono 2.11 (C# 4.0) 编译器,
  • 1,000,000 TESTITERATIONS
  • VERBOSE 不是#define-d

优化了什么:

  • 假设输入和输出数组
  • 在输入数组上就地工作
  • “手动”分析相同值的运行,而不是使用 GroupBy(使用 ValueRun 结构)
  • 将这些ValueRun 结构体放在一个数组而不是可枚举(列表)中;就地排序/随机播放
  • 使用unsafe 块和指针(没有明显区别...
  • 使用模索引而不是MAGIC Linq 代码
  • 通过迭代追加而不是嵌套 LINQ 生成输出。 这可能是最有效的。实际上,如果我们可以将具有 countruns 集合的 ValueRuns 快捷方式按此计数排序会更好,这似乎很容易做到;但是,转置索引(循环约束所需)使事情复杂化。无论如何,以某种方式应用此优化的收益会随着更大的输入和许多唯一值以及一些高度重复的值而更大。

这是优化版本的代码。 _可以通过移除RNG的种子来获得额外的速度增益;这些只是为了使回归测试输出成为可能。

[... old code removed as well ...]


原始回复(部分)

如果我没听错的话,您正在尝试设计一种随机播放,以防止重复项在输出中连续结束(最少交错 2 个元素)。

这在一般情况下是无法解决的。想象一下只有相同元素的输入:)

更新:麻烦的事态

就像我在笔记中提到的那样,我认为我并没有一直走在正确的轨道上。要么我应该调用图论(任何人?),要么使用简单的“蛮力”算法,这很长 Erick 的建议。

无论如何,你可以看到我一直在做什么,以及问题是什么(让随机样本快速看到问题):

#define OUTPUT       // to display the testcase results
#define VERIFY       // to selfcheck internals and verify results
#define SIMPLERANDOM
// #define DEBUG        // to really traces the internals
using System;
using System.Linq;
using System.Collections.Generic;

public static class Q5899274
{
    // TEST DRIVER CODE
    private const int TESTITERATIONS = 100000;
    public static int Main(string[] args)
    {
        var testcases = new [] {
            new [] {0,1,1,2,2,2,3,3,4,4,4,4,5,5,5,6,6,6,7,7,7,7,8,8,8,8,8,9,9,9,9,10},
            new [] {0,1,1,1,2,2,2,3,3,3,4,4,4,5,5,6,6,6,7,7,7,7,8,8,8,8,9,9,9,9,9,10},
            new [] { 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,37,38,39,40,41, 42, 42, 42, },
            new [] {1,1,1,1,2,2,2,2,3,3,3,3,4,4,4,4},
        }.AsEnumerable();

        // // creating some very random testcases
        // testcases = Enumerable.Range(0, 10000).Select(nr => Enumerable.Range(GROUPWIDTH, _seeder.Next(GROUPWIDTH, 400)).Select(el => _seeder.Next(-40, 40)).ToArray());

        foreach (var testcase in testcases)
        {
            // _seeder = new Random(45); for (int i=0; i<TESTITERATIONS; i++) // for benchmarking/regression
            {
                try
                {
                    var output = TestOptimized(testcase);
#if OUTPUT
                    Console.WriteLine("spread\t{0}", string.Join(", ", output));
#endif
#if VERIFY
                    AssertValidOutput(output);
#endif
                } catch(Exception e)
                {
                    Console.Error.WriteLine("Exception for input {0}:", string.Join(", ", testcase));
                    Console.Error.WriteLine("Sequence length {0}: {1} groups and remainder {2}", testcase.Count(), (testcase.Count()+GROUPWIDTH-1)/GROUPWIDTH, testcase.Count() % GROUPWIDTH);
                    Console.Error.WriteLine("Analysis: \n\t{0}", string.Join("\n\t", InternalAnalyzeInputRuns(testcase)));
                    Console.Error.WriteLine(e);
                }
            }
        }

        return 0;
    }

#region Algorithm Core
    const int GROUPWIDTH = 3; /* implying a minimum distance of 2
                                 (GROUPWIDTH-1) values in between duplicates
                                 must be guaranteed*/

    public static T[] TestOptimized<T>(T[] input, bool doShuffle = false)
        where T: IComparable<T>
    {
        if (input.Length==0)
            return input;

        var runs = InternalAnalyzeInputRuns(input);
#if VERIFY
        CanBeSatisfied(input.Length, runs); // throws NoValidOrderingExists if not
#endif
        var transpositions = CreateTranspositionIndex(input.Length, runs);

        int pos = 0;
        for (int run=0; run<runs.Length; run++)
            for (int i=0; i<runs[run].runlength; i++)
                input[transpositions[pos++]] = runs[run].value;

        return input;
    }

    private static ValueRun<T>[] InternalAnalyzeInputRuns<T>(T[] input)
    {
        var listOfRuns = new List<ValueRun<T>>();
        Array.Sort(input);
        ValueRun<T> current = new ValueRun<T> { value = input[0], runlength = 1 };

        for (int i=1; i<=input.Length; i++)
        {
            if (i<input.Length && input[i].Equals(current.value))
                current.runlength++;
            else
            {
                listOfRuns.Add(current);
                if (i<input.Length)
                    current = new ValueRun<T> { value = input[i], runlength = 1 };
            }
        }

#if SIMPLERANDOM
        var rng = new Random(_seeder.Next());
        listOfRuns.ForEach(run => run.tag = rng.Next()); // this shuffles them
#endif
        var runs = listOfRuns.ToArray();
        Array.Sort(runs);

        return runs;
    }

    // NOTE: suboptimal performance 
    //   * some steps can be done inline with CreateTranspositionIndex for
    //   efficiency
    private class NoValidOrderingExists : Exception { public NoValidOrderingExists(string message) : base(message) { } }
    private static bool CanBeSatisfied<T>(int length, ValueRun<T>[] runs)
    {
        int groups = (length+GROUPWIDTH-1)/GROUPWIDTH;
        int remainder = length % GROUPWIDTH;

        // elementary checks
        if (length<GROUPWIDTH)
            throw new NoValidOrderingExists(string.Format("Input sequence shorter ({0}) than single group of {1})", length, GROUPWIDTH));
        if (runs.Length<GROUPWIDTH)
            throw new NoValidOrderingExists(string.Format("Insufficient distinct values ({0}) in input sequence to fill a single group of {1})", runs.Length, GROUPWIDTH));

        int effectivewidth = Math.Min(GROUPWIDTH, length);

        // check for a direct exhaustion by repeating a single value more than the available number of groups (for the relevant groupmember if there is a remainder group)
        for (int groupmember=0; groupmember<effectivewidth; groupmember++)
        {
            int capacity = remainder==0? groups : groups -1;

            if (capacity < runs[groupmember].runlength)
                throw new NoValidOrderingExists(string.Format("Capacity exceeded on groupmember index {0} with capacity of {1} elements, (runlength {2} in run of '{3}'))",
                            groupmember, capacity, runs[groupmember].runlength, runs[groupmember].value));
        }

        // with the above, no single ValueRun should be a problem; however, due
        // to space exhaustion duplicates could end up being squeezed into the
        // 'remainder' group, which could be an incomplete group; 
        // In particular, if the smallest ValueRun (tail) has a runlength>1
        // _and_ there is an imcomplete remainder group, there is a problem
        if (runs.Last().runlength>1 && (0!=remainder))
            throw new NoValidOrderingExists("Smallest ValueRun would spill into trailing incomplete group");

        return true;
    }

    // will also verify solvability of input sequence
    private static int[] CreateTranspositionIndex<T>(int length, ValueRun<T>[] runs)
        where T: IComparable<T>
    {
        int remainder = length % GROUPWIDTH;

        int effectivewidth = Math.Min(GROUPWIDTH, length);

        var transpositions = new int[length];
        {
            int outit = 0;
            for (int groupmember=0; groupmember<effectivewidth; groupmember++)
                for (int pos=groupmember; outit<length && pos<(length-remainder) /* avoid the remainder */; pos+=GROUPWIDTH)
                    transpositions[outit++] = pos;

            while (outit<length)
            {
                transpositions[outit] = outit;
                outit += 1;
            }

#if DEBUG
            int groups = (length+GROUPWIDTH-1)/GROUPWIDTH;
            Console.WriteLine("Natural transpositions ({1} elements in {0} groups, remainder {2}): ", groups, length, remainder);
            Console.WriteLine("\t{0}", string.Join(" ", transpositions));
            var sum1 = string.Join(":", Enumerable.Range(0, length));
            var sum2 = string.Join(":", transpositions.OrderBy(i=>i));
            if (sum1!=sum2)
                throw new ArgumentException("transpositions do not cover range\n\tsum1 = " + sum1 + "\n\tsum2 = " + sum2);
#endif
        }

        return transpositions;
    }

#endregion // Algorithm Core

#region Utilities
    private struct ValueRun<T> : IComparable<ValueRun<T>>
    {
        public T value;
        public int runlength;
        public int tag;         // set to random for shuffling

        public int CompareTo(ValueRun<T> other) { var res = other.runlength.CompareTo(runlength); return 0==res? tag.CompareTo(other.tag) : res; }
        public override string ToString() { return string.Format("[{0}x {1}]", runlength, value); }
    }

    private static /*readonly*/ Random _seeder = new Random(45);
#endregion // Utilities

#region Error detection/verification
    public static void AssertValidOutput<T>(IEnumerable<T> output)
        where T:IComparable<T>
    {
        var repl = output.Concat(output.Take(GROUPWIDTH)).ToArray();

        for (int i=1; i<repl.Length; i++) 
            for (int j=Math.Max(0, i-(GROUPWIDTH-1)); j<i; j++)
                if (repl[i].Equals(repl[j]))
                    throw new ArgumentException(String.Format("Improper duplicate distance found: (#{0};#{1}) out of {2}: value is '{3}'", j, i, output.Count(), repl[j]));
    }
#endregion

}

【讨论】:

  • 添加了 Braindead 实现
  • 我已经尝试过这个例子,但我在处理一些列表时遇到了问题(我删除了子序列中的“OrderBy”,因为它总是失败。
  • @sehe:我编辑了我的帖子,并添加了两个列表,用你的方法改组不满足我需要的条件(第 i 个元素与 i+-1 && i+-2 不同) 在某个时候。
  • @Mg:啊哈,没关系,我不够懒惰:我通过确保将输入序列的末端无序排列来解决问题。只需查看标记为 //MAGIC 的行,了解我的意思
  • @Saha:'.ThenBy(g => _random.Next())' 导致子序列在每次评估时重新排序,这导致了我在上面的评论中提到的问题.我在最后添加了“ToList()”,并修复了它给您的代码带来的一些类型兼容性问题,现在它似乎工作得很好!
【解决方案2】:

这可以通过一个简单的两步过程来完成。首先使用 Fisher-Yates (Knuth) 洗牌。然后遍历列表一次,将其元素复制到新列表中。当您遇到一个元素时,将其插入新列表中的第一个合法位置。 (这将比使用数组作为目标更有效。)如果没有合法的位置可以插入,则问题的实例是无法解决的。如果您设法填写目的地列表,问题就解决了。这将在最好的情况下采用 O(n),在最坏的情况下采用 O(n^2)。

【讨论】:

  • 这是错误的:如果你有“abca”并且你碰巧复制了第一个“b”,那么你会错误地将问题标记为“无法解决”。
  • 一种极具挑战性的插入排序版本 :) 好想法。 +1 来自我
  • @akappa:不! “abca”不是一个合法的解决方案,因为规则应该循环适用于列表的末尾。您编辑了 OP 以删除此约束!!!不酷。
  • 嗯,我没有看到“圆形”的东西。对不起!
  • 无论如何,这仍然是错误的。考虑“abcdabcdabcd”:这是一个可以解决的案例。但是假设您开始只复制 a、b、c 字符:您最终以“abcabcabc”作为部分解决方案。现在您必须填充所有这些“d”,显然没有成功,因此当问题确实可以解决时,您声明问题“无法解决”。
【解决方案3】:

您的要求消除了真正的洗牌替代方案:没有随机性,或者存在受控随机性。 这是一种特殊的方法

  1. 排序原始列表L
  2. 找到模式M,以及它的频率n
  3. 如果n 是偶数,n++。
  4. 创建k (= 5*n - 1) 列表(n 的质数,以及n 的 5 次)L1L k
    (选择 5 是为了避免两个前一个元素和两个后一个元素)
  5. 以循环方式将所有元素分配到 k 列表中
  6. 单独随机播放所有 k 列表。
  7. 按以下顺序整理k 列表的内容: 一种。随机选择 +5 或 -5 作为x
    湾。选择一个随机数j.
    C。重复k 次:
    一世。添加来自Lj的所有内容。
    ii. j j + x) 模组k

[5, 6, 7, 7, 8, 8, 9, 10, 12, 13, 13, 14, 14, 14, 17, 18, 18, 19, 19, 20, 21, 21, 21, 21, 24, 24, 26, 26, 26, 27, 27, 27, 29, 29, 30, 31, 31, 31, 31, 32, 32, 32, 33, 35, 35, 37, 38, 39, 40, 42, 43, 44, 44, 46, 46, 47, 48, 50, 50, 50, 50, 51, 52, 53, 54, 55, 56, 57, 57, 58, 60, 60, 60, 61, 62, 63, 63, 64, 64, 65, 65, 65, 68, 71, 71, 72, 72, 73, 74, 74, 74, 74, 75, 76, 76, 76, 77, 77, 77, 78, 78, 78, 79, 79, 80, 81, 82, 86, 88, 88, 89, 89, 90, 91, 92, 92, 92, 93, 93, 94, 94, 95, 96, 99, 99, 100, 102, 102, 103, 103, 105, 106, 106, 107, 108, 113, 115, 116, 118, 119, 123, 124, 125, 127, 127, 127, 128, 131, 133, 133, 134, 135, 135, 135, 137, 137, 137, 138, 139, 141, 143, 143, 143, 145, 146, 147, 153, 156, 157, 158, 160, 164, 166, 170、173、175、181、181、184、185、187、188、190、200、200、215、217、234、238、240]

模式频率 = 4,因此 19 个插槽 (#0 - #18)

0: [7, 21, 32, 50, 65, 77, 93, 115, 137, 173]
1:[8、21、33、51、65、78、93、116、137、175]
2:[8、24、35、52、65、78、94、118、138、181]
3:[9、24、35、53、68、78、94、119、139、181]
4:[10、26、37、54、71、79、95、123、141、184]
5:[12、26、38、55、71、79、96、124、143、185]
6:[13、26、39、56、72、80、99、125、143、187]
7:[13、27、40、57、72、81、99、127、143、188]
8:[14、27、42、57、73、82、100、127、145、190]
9:[14、27、43、58、74、86、102、127、146、200]
10:[14、29、44、60、74、88、102、128、147、200]
11:[17、29、44、60、74、88、103、131、153、215]
12:[18、30、46、60、74、89、103、133、156、217]
13:[18、31、46、61、75、89、105、133、157、234]
14:[19、31、47、62、76、90、106、134、158、238]
15:[19、31、48、63、76、91、106、135、160、240]
16:[5、20、31、50、63、76、92、107、135、164]
17:[6、21、32、50、64、77、92、108、135、166]
18: [7, 21, 32, 50, 64, 77, 92, 113, 137, 170]

洗牌单个列表,并选择相隔 5 个位置的列表(从 #16 随机开始):

16: [31, 135, 92, 76, 107, 5, 164, 63, 20, 50]
2:[52、24、35、78、181、8、138、94、118、65]
7:[57、143、99、81、40、13、127、72、188、27]
12:[46、30、60、89、133、74、156、18、103、217]
17:[64、50、135、92、21、32、108、77、166、6]
3:[9、94、181、119、24、35、139、68、53、78]
8:[145、27、14、57、42、100、190、82、73、127]
13:[89、18、75、61、157、234、133、105、31、46]
18:[113、21、7、92、64、32、137、50、170、77]
4:[71、10、37、26、123、54、184、79、95、141]
9:[27、74、86、14、102、146、127、43、58、200]
14:[62、106、158、134、19、47、238、31、76、90]
0:[7、77、65、21、50、93、173、115、32、137]
5:[96、79、26、185、12、71、124、143、55、38]
10:[29、14、147、60、128、88、74、44、102、200]
15:[106、240、63、48、91、19、160、31、76、135]
1:[65、33、21、51、137、8、175、93、116、78]
6:[143、26、13、56、99、72、39、80、187、125]
11: [103, 88, 29, 60, 74, 44, 17, 153, 131, 215]

[31, 135, 92, 76, 107, 5, 164, 63, 20, 50, 52, 24, 35, 78, 181, 8, 138, 94, 118, 65, 57, 143, 99, 81, 40, 13, 127, 72, 188, 27, 46, 30, 60, 89, 133, 74, 156, 18, 103, 217, 64, 50, 135, 92, 21, 32, 108, 77, 166, 6, 9, 94, 181, 119, 24, 35, 139, 68, 53, 78, 145, 27, 14, 57, 42, 100, 190, 82, 73, 127, 89, 18, 75, 61, 157, 234, 133, 105, 31, 46, 113, 21, 7, 92, 64, 32, 137, 50, 170, 77, 71, 10, 37, 26, 123, 54, 184, 79, 95, 141, 27, 74, 86, 14, 102, 146, 127, 43, 58, 200, 62, 106, 158, 134, 19, 47, 238, 31, 76, 90, 7, 77, 65, 21, 50, 93, 173, 115, 32, 137, 96, 79, 26, 185, 12, 71, 124, 143, 55, 38, 29, 14, 147, 60, 128, 88, 74, 44, 102、200、106、240、63、48、91、19、160、31、76、135、65、33、21、51、137、8、175、93、116、78、143、26、13、 56、99、72、39、80、187、125、103、88、29、60、74、44、17、153、131、215]

【讨论】:

  • 为什么是七个?你怎么知道输出会满足 OP 后置条件?在约束范围内难道不能做比这更好的洗牌吗?
  • @CMR:模式和频率指的是什么?我根据什么来计算它们??
  • Mode 是组中出现频率最高的元素。频率是它出现的次数。例如:[10, 20, 30, 40, 50, 50, 60] - 众数 = 50,众数的频率 = 2。 [10, 10, 10, 20, 20, 20] - 10 和 20 都是众数。在这个随机化问题中,您可以选择任何一个:频率 (3) 是最重要的。
  • @CMR:我会尝试实现这个,一分钟后让你知道我的结果!
  • @Eric,您可以通过随机做出一些决定来进一步增加随机性。编辑帖子以反映这一点。
【解决方案4】:
  • 置换列表中的有效 5,如果没有无法解决的情况
  • 从列表中删除排列
  • 创建该 5 的循环图
  • 按新图表中有效位置的计数对列表(剩余列表)进行排序(如果不这样做,最终可能会出现错误的不可解决问题,因为放置可以放在更多位置的项目会增加项目的可能位置数少)
  • 继续在列表中挑选项目,将项目添加到循环图中的有效位置
  • 如果图表中没有有效位置或无法创建图表,请继续下一个
  • 如果创建的图恢复到创建图的开始迭代
  • 继续创建其他图表
  • 将所有完整图表保存在列表中
  • 从该列表中随机选择一个
  • 如果列表为空则无法解决

【讨论】:

    猜你喜欢
    • 2012-03-22
    • 2023-01-23
    • 1970-01-01
    • 1970-01-01
    • 2022-10-07
    • 1970-01-01
    • 2014-04-02
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多