【问题标题】:Card Shuffling (SPOJ / Interviewstreet)洗牌(SPOJ / Interviewstreet)
【发布时间】:2012-08-20 22:31:48
【问题描述】:

以前有人问过这个问题,但是没有一个得到明确的回答,我尝试编译在这里找到的所有信息。如有必要,请随意合并/移动到另一个 stackexchange 站点。

以下是我发现的与此相关的问题:

该问题最初是作为 Interviewstreet Code Sprint 发布的,但现在它被列为 a practice problem。也是ported to SPOJ

这是问题陈述:

这是洗 N 张牌的算法:

1) 将卡片分成 K 个相等的堆。

2) 底部的 N / K 张牌 属于第1堆的顺序相同(所以最初的底牌 桩是桩 1) 的底牌。

3) 接下来的 N / K 张卡片来自 底部属于第 2 堆,以此类推。

4) 现在洗好的牌堆的顶牌就是牌堆 1 的顶牌。 下一张牌是第 2 堆的最上面的牌,...,洗好的牌堆的第 K 张牌是第 K 堆的最上面的牌。那么第(K + 1)张牌就是现在在牌堆顶部的牌 堆 1,(K + 2)nd 是现在在堆 2 顶部的卡 等等。

例如,如果 N = 6 且 K = 3,则一副纸牌的顺序为“ABCDEF” (从上到下)洗牌一次时将变为“ECAFDB”。

给定 N 和 K,之后最少需要多少次随机播放 堆恢复到原来的顺序了吗?

输入:第一行包含测试用例的数量T。下一个T 行包含两个整数,每个整数 N 和 K。

输出:输出 T 行,每个测试用例一个包含最小值 需要的洗牌次数。如果甲板永远不会回到它的 原始顺序,输出-1。

约束:

  • K 将是 N 的一个因子。
  • T
  • 2

剧透警告 - 如果您想自己解决,请不要阅读下文。

问题可以翻译为:

求K-way(完美)in-shuffle需要执行的次数 将一副 N 牌恢复到初始顺序。

我采取了两种方法来解决这个问题。想到的第一个方法是:

  • 找到一个公式,给定初始顺序中的位置将生成卡片的下一个位置
  • 使用公式确定从第一堆中的每张牌(n / k 大小)返回其初始位置所需的洗牌次数
  • 返回之前确定的洗牌次数的最小公倍数

这个解决方案的复杂度是 O(n / k + max_number_of_suhffles)。 Here's the actual implementation。这样做的问题是它超过了最长时间,所以我开始寻找一个可以让我在接近恒定的时间内得到数字的公式。

我在这里可以优化的最多(例如,使用一些地图来缓存相同排列循环中的计算值等)是让它通过 interviewstreet 上的 3/10 测试。


我找到this implementation,它假设返回初始状态所需的洗牌次数是K相对于N + 1的multiplicative order。来自wiki:

As a consequence of Lagrange's theorem, ordn(a) always divides φ(n). 

φ(n) 是Euler totient function,ordn 是group order——我们正在寻找的。我发现this paper 使用 φ 来计算 shuffle 的数量,但它仅适用于 2-way in-shuffle,而不是 k-way。

以下是此实施的步骤:

  • 预计算素数列表
  • 根据其主要因数计算 φ(N+1)
  • 通过以所有可能的方式组合其素因子来确定φ(N + 1) 的所有因子。
  • 依次尝试每个因素,得到最小的一个,x,验证k ^ x % N + 1 = 1

这个实现也是posted on GitHub

这运行得非常快,但自动评分器在 SPOJ 和 Interviewstreet 上的 10 次测试中有 9 次给了我“错误答案”分类。

我尝试比较两个实现的输出,但是对于我输入的测试用例(已知结果和随机),两个实现总是输出相同的东西。这很奇怪,因为我很确定第一个算法是正确的,我假设第二个算法也应该是正确的。

“错误答案”分类可能来自代码中的运行时错误,但没有任何可能的原因跳出。

我没有考虑到没有数字洗牌可以使牌组恢复到初始状态的情况——我的理解是这是不可能的。有限数量的完美洗牌最终将恢复初始排序,即使洗牌的数量可能非常高。

如果您花时间阅读本文,谢谢。 :) 我很好奇这个问题,我想解决它。

【问题讨论】:

  • Same Here.. 我用数学公式尝试过,7/10 案例显示超出时间限制。虽然我不确定测试用例。
  • 请注意,在spoj.pl/problems/CODESPTC 中,我们有 2
  • 在看了上面的内容,并摆弄了 3*2 和 3*3 的情况后,我猜想如果你将卡片标记为 1、2、3...N,那么洗牌会留下包x, 2x, 3x, ..Nx mod (N+1),您可以通过查看位置 1 中的内容来计算 x 是什么。如果是这样,您需要 x 在数字组中的顺序 mod N+1 与 N+1 互质——我认为这是您的第二种方法,乘数略有不同。您能否通过简单地检查 x、x^2、x^3... 来检查这个想法并简化您的代码,直到找到一个为 1 的结果?
  • 感谢提示,明天试试。今天的工作妨碍了:)。

标签: java algorithm shuffle


【解决方案1】:

这是我在纸上进行了一些观察后得出的结论。

class CardShuffle {
    private long k;
    private long n;
    private long kn;
    private long kn2;

    public CardShuffle(long k, long n) {
            //I omitted some checks done here
        this.k = k;
        this.n = n;
        this.kn = k / n;
        this.kn2 = k - kn;
    }

    public long shuffle() {
        long count = 0L;
        long next = 0L;
        do {
               //this can be further optimized
           next = kn2 - kn * (next % n) + (next / n); 
           ++count;
        } while((next != 0L) && (count < k));
        if(count > k)
           return -1;
        return count;
    }
}

结果是……

Testing 1000000 : 2
#ms: 3.121905
#ms: 1424.487191
#1: 9900 #2: 9900
Testing 1000000 : 5
#ms: 1.409955
#ms: 556.329366
#1: 2475 #2: 2475
Testing 1000000 : 10
#ms: 0.007823
#ms: 186.797204
#1: 12 #2: 12
Testing 1000000 : 20
#ms: 0.590298
#ms: 275.751527
#1: 4950 #2: 4950
Testing 1000000 : 25
#ms: 0.298642
#ms: 260.559372
#1: 2475 #2: 2475
Testing 1000000 : 40
#ms: 1.187581
#ms: 241.956729
#1: 9900 #2: 9900
Testing 1000000 : 50
#ms: 1.187581
#ms: 241.015548
#1: 9900 #2: 9900
Testing 9999999 : 41841
#ms: 14.499887
#ms: 1829.868042
#1: 125000 #2: 125000
Testing 9999999 : 3333333
#ms: 58.119398
#ms: 311.005728
#1: 500000 #2: 500000
Testing 9999999 : 13947
#ms: 52.704185
#ms: 2095.336418
#1: 500000 #2: 500000

已针对此输入进行测试...

10
1000000 2
1000000 5
1000000 10
1000000 20
1000000 25
1000000 40
1000000 50
9999999 41841
9999999 3333333
9999999 13947

第一个#ms 是我的方法所花费的毫秒数,第二个是你的。 #1#2 分别是结果。

至于这个输入...

15
1000000000 2
1000000000 5
1000000000 10
1000000000 20
1000000000 25
1000000000 40
1000000000 50
1000000000 1000
1000000000 200000000
1000000000 250000000
1000000000 500000000
1000000000 50000000
999999999 1001001
999999999 37037037
999999999 333333333

我的方法在

中找到了解决方案
Testing 1000000000 : 2
#ms: 71.360466
#1: 525780
Testing 1000000000 : 5
#ms: 68.987259
#1: 525780
Testing 1000000000 : 10
#ms: 0.008381
#1: 18
Testing 1000000000 : 20
#ms: 75.608492
#1: 525780
Testing 1000000000 : 25
#ms: 31.843154
#1: 262890
Testing 1000000000 : 40
#ms: 33.014531
#1: 262890
Testing 1000000000 : 50
#ms: 84.27384
#1: 525780
Testing 1000000000 : 1000
#ms: 0.006705
#1: 6
Testing 1000000000 : 200000000
#ms: 53.991778
#1: 525780
Testing 1000000000 : 250000000
#ms: 43.765898
#1: 262890
Testing 1000000000 : 500000000
#ms: 54.457201
#1: 525780
Testing 1000000000 : 50000000
#ms: 68.080999
#1: 525780
Testing 999999999 : 1001001
#ms: 115.060154
#1: 1000000
Testing 999999999 : 37037037
#ms: 5783.539528
#1: 50000000
Testing 999999999 : 333333333
#ms: 5391.880532
#1: 50000000

虽然您的第一个内存不足,但在我又旧又慢的笔记本电脑上。

我还没有验证这种方法,但在我看来它是可行的。 您可以尝试查看它是否在某些输入上失败。我会很感激的。

如果您对我是如何开发公式的更感兴趣,请发表评论。

我也向interviewstreet提交了解决方案,但由于时间限制,它在第 4 次测试用例中失败了。

我很快就会尝试使用 c 程序,并会在这里报告。

【讨论】:

  • 感谢您的回答!我今天没有时间检查这个,但我会尽快回复您。
  • 我试过你的解决方案,这让我很困惑。据我了解,您返回的计数永远不会大于K - 这就是我从 while 循环中的条件和最后的 if 语句中得出的。如果是这样,对于 N=1000000000 和 K=2,您是如何得到 525780 的?我尝试运行您的代码,对于相同的测试用例,输出为 2。
  • @AlexCiminian 我把变量搞混了一点。 K 在源代码中是 N :) 所以,让它成为 N=2K=1000000000
【解决方案2】:

给定 N 和 K,计算出产生了什么排列,并计算出它在 http://en.wikipedia.org/wiki/Cycle_notation 中的样子。在循环符号中,排列是不相交循环的产物,例如ABCDEF => ECAFDB 是 (AEDFBC) 因为 A->E->D->F->B->C->A。如果你的排列是像这样的一个循环,那么循环的长度就是你需要重复它才能回到同一个地方的次数。如果排列是多个不相交循环的产物,例如 (ABC)(DE)(F),则您需要重复的次数是各个循环长度的http://en.wikipedia.org/wiki/Least_common_multiple

【讨论】:

  • -1,这与 Alex 的“第一个”解决方案相同,并且不回答问题。
  • 是的,@cmh 是对的,这就是我在第一次运行时所做的。如果您能找到错误或优化我的代码的方法,我将不胜感激,但我认为您无法通过这种方法获得所需的复杂性。我将尝试使用您建议的术语来更新问题,排列周期听起来比“方法 1”更好。 :)
  • @AlexCiminian。如果您阅读了有关 permutation groups 的信息,那么您可能会看到这两种解决方案之间的联系。
  • 我已经对此进行了编码,并获得了与发布的第一个版本大致相同的速度。按照编码,它需要 O(N),因为写出一个排列需要很长时间,只要你确保每个循环只循环一次,计算出循环结构也是 O(N)。想不出什么
  • 第二个变体是 O( φ(N + 1) 的因子数),这比 O(n) 好很多,但是我得到了一个“错误的答案”。我不知道我在那里做错了什么,在我的测试中,两种实现都得到了相同的结果。
【解决方案3】:
#include <iostream>
using namespace std;
int main() 
{
    int t, m , n, c, p, k, pos, cases;

    cin>>cases;

    while(cases--)
    {
        cin>>t;
        cin>>k;

        p = t/k;
        pos = 1;
        c = 0;

        do
        {
            c++;
            m = (pos - 1)%p + 1;
            n = k - (pos - m)/p;
            pos = k*(m-1) + n;

        }while(pos!=1);

        cout<<c<<endl;


    }
    return 0;
}

【讨论】:

  • 这并不能解决问题,第三次测试后超时。
猜你喜欢
  • 2021-06-14
  • 1970-01-01
  • 1970-01-01
  • 2013-11-26
  • 1970-01-01
  • 1970-01-01
  • 2011-07-09
  • 2012-09-01
  • 2015-06-30
相关资源
最近更新 更多