完美洗牌问题

完美洗牌问题:给定一个数组a1, a2, a3, ..., an, b1, b2, b3, ..., bn,把它最终变换为b1, a1,b2, a2, ..., bn, an这样的。

为方便起见,我们考虑一个下标从1开始的数组,下标范围是[1, 2n]。我们看一下每个元素最终去了什么地方。

前n个元素 a1 -> a2, a2->a4, ... 第i个元素去了第(2 * i)个的位置。


后n个元素a(n + 1)->a1, a(n + 2)->a3, ... 第i个元素去了第 ((2 * (i - n)) - 1) = 2 * i - (2 * n + 1) = (2* i) % (2 * n + 1) 个的位置。


综合到任意情况,任意的第i个元素,最终换到了 (2 * i) % (2 * n + 1)的位置。为何呢?因为:

当1 <= i <= n时,原式= (2 * i) % (2 * n + 1) = 2 * i;

当n < i <= 2*n时,原式= (2 * i) % (2 * n + 1)保持不变。


(1)完美洗牌算法1

如果允许我们使用一个额外数组的话,我们直接把每个元素放到对应的位置就好了。

于是产生了最简单的方法pefect_shuffle1,它的时间复杂度是O(n),空间复杂度也是O(n)。

//时间O(n),空间O(n),数组下标从1开始
void pefect_shuffle1(int *a, int n)
{
	int n2 = n * 2, i, b[N];
    for (i = 1; i <= n2; i++) {
        b[(i * 2) % (n2 + 1)] = a[i];
    }
    for (i = 1; i <= n2; i++) {
        a[i] = b[i];
    }
}

(2)完美洗牌算法2——分治

考虑分治法,假设n是偶数。我们考虑把数组拆成两半(我们只写数组的下标):

原始数组为(1, 2, ..., n, n+1, n+2, ..., 2n)

拆分之后为(1, ...,n/2, ..., n)和(n+1, ..., n+n/2, ..., 2n)

前半段长度为n,后半段长度也为n。

我们把前半段的后n / 2个元素(n/2 + 1, ..., n)与后半段的前n / 2 个元素 (n+1, ..., n + n/2 )交换,得到:

新的前n个元素A :(1, ..., n/2, n+1, ..., n + n/2)

新的后n个元素B :(n/2 + 1, ..., n, n + n/2 + 1, ..., n)

因为n是偶数,我们得到了A,B两个子问题。问题转化为了求解n' = n / 2的两个问题。

那么当n是奇数怎么办?我们可以把前半段多出来的那个元素先拿出来,后面所有元素前移,再把当时多出的那个元素放到末尾,这样数组最后两个元素已经满足要求了。于是只考虑前2 * (n - 1)个元素就可以了,于是转换成了(n - 1)的问题。

针对上述 n 分别为偶数和奇数的情况,下面举 n=4 和 n=5 两个例子来说明下。

当n=4 时,原始数组即为

a1 a2 a3 a4 b1 b2 b3 b4

按照之前n为偶数时的思路,把前半段的后2个元素a3 a4同后半段的前2个元素b1 b2交换,可得:

a1 a2 b1 b2 a3 a4 b3 b4

然后再分别求解 A(a1 a2 b1 b2)和 B(a3 a4 b3 b4)两个子问题就可以了。

当 n=5 时,原始数组则为

a1 a2 a3 a4 a5 b1 b2 b3 b4 b5

按照之前 n 为奇数时的思路,先把 a5 先单独拎出来放在最后,然后所有剩下的元素全部前移,变为:

a1 a2 a3 a4 b1 b2 b3 b4 b5 a5

此时,最后的两个元素b5 a5已经是我们想要的结果,只要跟之前n=4的情况一样考虑即可。

//时间O(nlogn),空间O(1),数组下标从1开始
void perfect_shuffle2(int *a, int n)
{
	int tmp, i;
    if (n == 1) {
        tmp = a[1];
        a[1] = a[2];
        a[2] = tmp;
        return;
    }
    int n2 = n * 2, n3 = n / 2;
    if (n % 2 == 1) {  //奇数的处理
        tmp = a[n];
        for (i = n + 1; i <= n2; i++) {
            a[i - 1] = a[i];
        }
        a[n2] = tmp;
        --n;
    }
    //到此n是偶数
    
    for (i = n3 + 1; i <= n; ++i) {
        tmp = a[i];
        a[i] = a[i + n3];
        a[i + n3] = tmp;
    }
    
    // [1.. n /2]
    perfect_shuffle2(a, n3);
    perfect_shuffle2(a + n, n3);    
}

分析下此算法的复杂度:每次,我们交换中间的n个元素,需要O(n)的时间,n是奇数的话,我们还需要O(n)的时间先把后两个元素调整好,但这不影响总体时间复杂度。

故事实上,当我们采用分治算法的时候,其时间复杂度的计算公式为: T(n) = 2*T(n/2)+ O(n),这个就是跟归并排序一样的复杂度式子,由《算法导论》中文第二版 44 页的主

定理,可最终解得 T(n) = O(nlogn)。至于空间,此算法在数组内部折腾的,所以是 O(1)(在不考虑递归的栈的空间的前提下)。


(3)完美洗牌算法

首先,对于每一个元素,它最终都会到达一个位置,我们如果记录每个元素应到的位置会形成圈。为什么会形成圈?

比如原来位置为a的元素最终到达b,而b又要达到c……,因为每个新位置和原位置都有一个元素,所以一条链a->b->c->d……这样下去的话,必然有一个元素会指向a,(因为中间那些位置b, c, d……都已经被其它元素指向了)。这就是圈的成因。

当n=4的情况:

起始序列:a1 a2 a3 a4 b1 b2 b3 b4

 数组下标:1 2 3 4 5 6 7 8 

最终序列:b1 a1 b2 a2 b3 a3 b4 a4

我们可以看出有两个圈:

一个是1 -> 2 -> 4 -> 8 -> 7 -> 5 -> 1

一个是3 -> 6 -> 3

这两个圈可以表示为(1, 2, 4, 8, 7, 5)和(3, 6),且perfect_shuffle1算法也已经告诉了我们,不管你n是奇数还是偶数,每个位置的元素都将变为第(2*i) % (2*n+1)个元素:

因此我们只要知道圈里最小位置编号的元素即圈的头部,顺着圈走一遍就可以达到目的,且因为圈与圈是不想交的,所以这样下来,我们刚好走了O(N)步。

上面沿着圈走的算法我们给它取名为cycle_leader,这部分代码如下:

//数组下标从1开始,from是圈的头部,mod是要取模的数 
//mod应该为 2*n + 1,时间复杂度O(圈长)
void cycle_leader(int *a,int from, int mod)
{
	int last = a[from], tmp, i;
    
    for (i = 2*from % mod; i != from; i = 2*i % mod) {
        tmp = a[i];
        a[i] = last;
        last = tmp;        
    }
    a[from] = last;
}

那么如何找到每个圈的头部呢?引用一篇论文A Simple In-Place Algorithm for In-Shuffle.里面的结论:

对于2n = (3^k - 1)这种长度的数组,恰好只有k个圈,且每个圈头部的起始位置分别是1, 3, 9, ..., 3^(k-1)。

利用上述这个结论,我们可以解决这种特殊长度2n = (3^k - 1)的数组问题,那么若给定的长度n是任意的咋办呢?此时,我们可以采取分而治之算法的思想,把整个数组一分为二,即拆分成两个部分:让前一部分满足上述结论,后一部分单独计算。

假设2m = (3^k - 1),其中m<=n,我们把n分解成m和n-m两部分。

当把n分解成m和n-m两部分后,原始数组对应的下标如下(为了方便描述,我们依然只需要看数组下标就够了):

原始数组下标:1, ..., m, m+1, ..., n, n+1, ..., n+m, n+m+1, ..., 2n

且为了能让前部分的序列满足2m = (3^k - 1),我们可以把中间那两段长度为n-m和m的段交换位置,即相当于把m+1, ..., n, n+1, ..., n+m的段循环右移m次(为什么要这么做?因为如此操作后,数组的前部分的长度为2m,而根据结论:当2m = (3^k - 1),可知这长度2m的部分恰好有k个圈)。

循环移位的代码:
//翻转字符串时间复杂度O(to - from)
void reverse(int *a, int from, int to)
{
	int tmp;
    for (; from < to; ++from, --to) {
        tmp = a[from];
        a[from] = a[to];
        a[to] = tmp;
    }
    
}

//循环右移num位 时间复杂度O(n)
void right_rotate(int *a, int num, int n)
{
    reverse(a, 1, n - num);
    reverse(a, n - num + 1, n);
    reverse(a, 1, n);
}

当给定n=7时,若要满足结论2m = (3^k - 1),k只能取2,继而m=4。

原始数组:a1 a2 a3 a4 a5 a6 a7b1 b2 b3 b4 b5 b6 b7

既然m=4,即让上述数组中有下划线的两个部分交换,得到:

目标数组:a1 a2 a3 a4 b1 b2 b3 b4a5 a6 a7 b5 b6 b7

继而目标数组中的前半部分a1 a2 a3 a4 b1 b2 b3 b4部分可以用走圈算法cycle_leader搞定,于此我们最终求解的n长度变成了n’=3,单独再解决后半部分a5 a6 a7 b5 b6 b7即可。

所以这也是一种分治算法。算法流程:

输入数组a[1, ..., 2n]

step 1 找到 2m = 3^k - 1 使得 3^k <=2n < 3^(k +1)

step 2 把a[m + 1, ..., n + m]那部分循环右移m位

step 3 对每个i = 0, 1, 2, ..., k- 1,3^i是每个圈的头部,做cycle_leader算法,数组长度为m,所以对2 * m + 1取模。

step 4 对数组的后面部分a[2m + 1, ...,2n]继续使用本算法,这相当于n减小了m。

以上各个步骤对应的时间复杂度分析如下:

1. 因为循环不断乘3的,所以时间复杂度O(logn)

2. 循环移位O(n)

3. 每个圈,每个元素只走了一次,一共2m个元素,所以复杂度omega(m),而m < n,所以也在O(n)内。

4. T(n - m)

因此,复杂度为 T(n) = T(n - m) + O(n),m = omega(n)  解得:总复杂度T(n) = O(n)。

算法代码:

//时间O(n),空间O(1)
void perfect_shuffle3(int *a, int n)
{
	int n2, m, i, k, tmp;
	
    for (; n > 1; ) {
        // step 1
        n2 = 2 * n;
        for (k = 0, m = 1; n2 / m >= 3; ++k, m *= 3)
        ;
        m /= 2;
        // 2m = 3^k - 1 , 3^k <= 2n < 3^(k + 1)
        
        // step 2
        right_rotate(a + m, m, n);
        
        // step 3        
        for (i = 0, t = 1; i < k; ++i, t *= 3) {
            cycle_leader(a ,t, 2 * m + 1);            
        }
        
        //step 4
        a += 2 * m;
        n -= m;    
    }

    // n = 1
    tmp = a[1];
    a[1] = a[2];
    a[2] = tmp;    
}

问题扩展:

1.      给定的输入为a1, a2, a3, ..., an, b1, b2, b3, ..., bn, 要求输出为a1, b1,a2, b2, ..., an, bn. 如何实现?

        我们可以利用完美洗牌算法得到b1, a1, b2, a2, ..., bn, an,在此基础上再两两交换相邻元素即可。

        或者保持a1和bn不变,对中间的a2, a2, ..., an, b1, b2, ..., bn-1这2*(n-1)个元素使用完美洗牌算法即可。


2.      完美洗牌问题的逆问题:给定b1, a1, b2, a2, ..., bn, an,输出a1, a2, a3, ..., an,b1, b2, b3, ..., bn.

        这相当于把偶数位上的数放到一起,奇数位上的数放到一起。

        关键问题:我们需要把cycle_leader算法改一下,沿着圈换回去。改造后的叫reverse_cycle_leader,代码如下:

//逆变换,数组下标从1开始,from是圈的头部
//mod是要取模的数mod应该为2n + 1,时间复杂度O(圈长)
void reverse_cycle_leader(int *a, int from, int mod)
{
    int last = a[from], next, i;
    
    for (i = from; ; i = next) {
        next = 2 * i % mod;
        if (next == from) {
            a[i] = last;
            break;
        }
        a[i] = a[next];        
    }
}

按照完美洗牌算法,我们同样把数分为m和(n - m)两部分。

假设我们把前面若干项已经置换成先a后b的形式了,现在把这m项也置换成先a后b的形式,我们需要把这m项中的a部分换到前面去,这里需要一个循环右移,还要知道以前处理了多长。总之,这个逆shuffle算法需要小心实现一下,代码如下:

//逆shuffle 时间O(n),空间O(1)
void reverse_perfect_shuffle3(int *a, int n)
{
	int n2, m, i, k, t, done = 0;
    for (; n > 1; ) {
        // step 1
        n2 = n * 2;
        for (k = 0, m = 1; n2 / m >= 3; ++k, m *= 3)
            ;
        m /= 2;
        // 2m = 3^k - 1 , 3^k <= 2n < 3^(k + 1)
        
        for (i = 0, t = 1; i < k; ++i, t *= 3) {
            reverse_cycle_leader(a, t, m * 2 + 1);
        }
        
        if (done) {
            right_rotate(a - done, m, done + m); //移位
        }
		
        a += m * 2;
        n -= m;
        done += m;       
    }
    // n = 1
    right_rotate(a - done, 1, done + 2);
}


3.      如果输入是a1, a2, ...,  an, b1, b2, ...,bn,  c1, c2, ..., cn,要求输出是c1, b1 ,a1,c2, b2, a2, ..., bn, an。怎么办?

对于任意位置i = 1, 2, ..., 3n 我们发现:

原始1 <= i <= n 时,即a部分,转移到的位置是3i

原始n < i <= 2n 时即b部分,转移到的位置是 3i - (3n + 1)

原始2n < i <= 3n时,即c部分,转移到的位置是 3i - 2 * (3n + 1)

于是我们得到映射位置 i'= 3i % (3n + 1)

剩下的问题和完美洗牌算法差不多,我们试图对一个特定的长度解决掉。

仿照完美洗牌算法的思路,验证了3是7的原根,是49的原根,于是3是7^k的原根。于是,我们可以把原来的圈截取出一个m,满足3m = 7^k - 1,截取出一个m长度后,我们同样需要循环移位,使得(a1...am)(b1...bm)(c1...cm)在一起,这里要循移位两次:

a1 ... am am+1 ... an b1 ... bm bm+1 ... bn c1 ... cm cm+1 ... cn

首先对上面加粗的部分,即下标[m+1, n+m]循环右移m位,变成

a1 ... am b1 ... bm am+1 ... an bm+1 ... bn c1 ... cm cm+1 ... cn

然后对上面加粗的部分,即下标[2m+1, 2n+m]循环右移m位,变成

a1 ... am b1 ... bm c1 ... cm am+1... an bm+1 ... bm cm+1 ... cn

算法的步骤如下:

输入数组a[1, ..., 3n]

step 1 找到 3m = 7^k -1 使得 7^k <= 3n < 7^(k +1)

step 2 把a[m+1, ..., n+m]那部分循环右移m位,再把a[2m+1, ...,2n+m]那部分循环右移m位,这样把数组分成了m和(n - m)两部分。

step 3 对每个i = 0, 1 ,2,..., k-1,7^i是每个圈的头部,做cycle_leader算法,数组长度为m,所以对3m + 1取模。

step4 对数组的后面部分a[3m+1, ..., 3n]继续使用本算法,这相当于n减小了m。


//数组下标从1开始,from是圈的头部
//mod是要取模的数,mod应该为3n+1,时间复杂度O(圈长)  
void cycle_leader(int *a, int from, int mod)
{  
    int last = a[from], t, i;  
      
    for (i = from*3 % mod; i != from; i = i*3 % mod) {  
        t = a[i];  
        a[i] = last;  
        last = t;          
    }  
    a[from] = last;  
}  

//时间O(n),空间O(1)
void perfect_shuffle3n(int *a, int n)
{
    int n3, m, i, k, t;
    for (; n > 2; ) {
        // step 1
        n3 = n * 3;
        for (k = 0, m = 1; n3 / m >= 7; ++k, m *= 7)
            ;
        m /= 3;
        // 3m = 7^k - 1 , 7^k <= 3n < 7^(k + 1)
        
        // step 2
        right_rotate(a + m, m, n);
        right_rotate(a + m * 2, m , n * 2 - m);
        
        // step 3        
        for (i = 0, t = 1; i < k; ++i, t *= 7) {
            cycle_leader(a , t, m * 3 + 1);
            
        }
        
        //step 4
        a += m * 3;
        n -= m;        
    }
    if (n == 2) {
        cycle_leader(a, 1, 7);
    }
    else if (n == 1) {
        t = a[1];
        a[1] = a[3];
        a[3] = t;
    }    
}

参考链接

1. http://blog.csdn.net/caopengcs/article/details/10176093

2. http://blog.csdn.net/caopengcs/article/details/10521603


以下是完整程序:

#include <stdio.h>

//翻转字符串
void reverse(int *a, int from, int to)
{
    int tmp;
    while (from < to) {
        tmp = a[from];
        a[from++] = a[to];
        a[to--] = tmp;
    }
}

//循环右移num位,数组下标从1开始
void right_rotate(int *a, int num, int n)
{
    reverse(a, 1, n - num);
    reverse(a, n - num + 1, n);
    reverse(a, 1, n);
}

void cycle_leader(int *a, int from, int mod)
{
    int last = a[from], tmp, i;
    for (i = 2 * from % mod; i != from; i = 2 * i % mod) {
        tmp = a[i];
        a[i] = last;
        last = tmp;
    }
    a[from] = last;
}

void perfect_shuffle(int *a, int n)
{
    int m, k, tmp, i, t, n2;

    while (n > 1) {
        //step1
        n2 = n * 2;
        k = 0;
        m = 2;
        tmp = 3;
        while (m <= n2) {
            k++;
            tmp *= 3;
            m = tmp - 1;
        }
        m = (tmp/3 - 1) / 2;
        // 2m = 3^k - 1 <= 2n

        //step2
        if (m < n) {
            right_rotate(a + m, m, n);
        }

        //step3
        for (i = 0, t = 1; i < k; i++, t *= 3) {
            cycle_leader(a, t, 2 * m + 1);
        }
        
        //step4
        a += 2 * m;
        n -= m;
    }
    // n = 1
    tmp = a[1];
    a[1] = a[2];
    a[2] = tmp;    
}

void reverse_cycle_leader(int *a, int from, int mod)
{
    int last = a[from], next, i;

    i = from;
    next = 2 * from % mod;
    while (next != from) {
        a[i] = a[next];
        i = next;
        next = 2 * i % mod;
    }
    a[i] = last;
}

void reverse_perfect_shuffle(int *a, int n)
{
    int m, k, n2, i, tmp, t, done;

    while (n > 1) {
        n2 = n * 2;
        k = 0;
        m = 2;
        tmp = 3;
        while (m <= n2) {
            k++;
            tmp *= 3;
            m = tmp - 1;
        }
        m = (tmp/3 - 1) / 2;

        for (i = 0, t = 1; i < k; i++, t *= 3) {
            reverse_cycle_leader(a, t, 2 * m + 1);
        }

        if (done) {
            right_rotate(a - done, m, done + m);
        }

        a += 2 * m;
        n -= m;
        done += m;
    }
    // n = 1
    right_rotate(a - done, 1, done + 2);
}

int main(void)
{
    int a[11] = {-1, 2, 4, 6, 8, 10, 1, 3, 5, 7, 9};
    int i;
    perfect_shuffle(a, 5);
    for (i = 1; i <= 10; i++)
        printf("%d ", a[i]);
    printf("\n");
    reverse_perfect_shuffle(a, 5);
    for (i = 1; i <= 10; i++)
        printf("%d ", a[i]);
    printf("\n");  
    return 0;
}



  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值