我最近在“Programming Pearls”一书中读到了解决方案时,我最近了解了 Juggling算法如何在线性时间内旋转数组。
解决问题的代码如下:
/*Function to left rotate arr[] of siz n by d*/
void leftRotate(int arr[], int d, int n)
{
int i, j, k, temp;
for (i = 0; i < gcd(d, n); i++)
{
/* move i-th values of blocks */
temp = arr[i];
j = i;
while(1)
{
k = j + d;
if (k >= n)
k = k - n;
if (k == i)
break;
arr[j] = arr[k];
j = k;
}
arr[j] = temp;
}
}
我对此算法有两个问题 -
我觉得,我遗漏了关于 GCD ,模数和周期的工作的基本信息。
以下问题回答了我的第一个问题,但我仍然无法理解它。
Juggling algorithm of string rotation
因此,如果有人能够以外行的方式解释它以及它们如何凝聚在一起以使该算法有效,那将会很有帮助。
答案 0 :(得分:33)
GCD如何决定旋转阵列所需的周期数?
因为内循环以d
的步长递增,并且当它回到起始点时停止,即总跨度是n
的某个倍数。该倍数为LCM(n, d)
。因此,该周期中的元素数量为LCM(n, d) / d
。此类周期的总数为n / (LCM(n, d) / d)
,等于GCD(n, d)
。
为什么一旦我们完成一个循环,我们从下一个元素开始新的循环,即下一个元素不能已经是处理循环的一部分?
没有。内循环以d
的步长递增,GCD(n, d)
是i
的倍数。因此,当我们开始(k*GCD + z) % n == i
- 周期时,对于点击,我们需要0 <= z < i
(对于(k*GCD) % n == (i - z)
)。这会导致{{1}}。这显然没有解决方案。
答案 1 :(得分:2)
GCD确实是数学之美的一个例子。有时候,当你在脑子里得到这个东西时,你的思想就会回答它自己所做的事情。
现在开始质疑,旋转任务,可以简单地使用for循环。杂耍算法可能比它有一些优点(我没有找到什么)。
现在为什么要GCD。 GCD给出了要执行的精确旋转图。它实际上最小化了没有旋转。
例如,
如果你想进行30个数字的旋转
,d = 1
外环将旋转一次,内部旋转30次1*30=30
,d = 2
外环将旋转两次,内部旋转15次2*15=30
,d = 3
外环将旋转三次,内部旋转10次3*10=30
因此,GCD在这里将确保旋转不超过30.当你得到一个数字是总元素的除数时,它不会跳过任何元素
答案 2 :(得分:1)
根据我的理解,以下对杂耍算法的实现已经实现,但是当长度为13且旋转为3时会发生什么,这样GCD将变得无效,因此拆分为步骤也会引起问题。
遵循杂耍算法
public void jugglingAlgorithm(int arr[],int limit)
{
for(int i =0;i<findGCD(arr.length, limit);i++)//iterate the sets
{
int j;
int temp = arr[i];
for(j=i;j<arr.length;j = j+limit)
{
if((j+limit)>=arr.length) arr[j] = temp;
else arr[j]=arr[j+limit];
}
}
printArray(arr);
}
public int findGCD(int num1,int num2)
{
int gcd = 1;
for(int i=1;(i<=num1 && i<=num2);i++)
{
if(num1%i==0 && num2%i==0) gcd = i;
}
return gcd;
}
public void printArray(int[] arr){
for(int i=0;i<arr.length;i++)
{
System.out.print(arr[i]+"\t");
}
System.out.println("");
}
答案 3 :(得分:1)
聚会有点晚。
虽然 Oliver 的回答很好地解释了它,但我还想对他的解释细节进行更多说明。(可能对某人有用!)
<块引用>GCD 如何决定旋转所需的周期数 数组?
让我们找出为什么外循环的长度应该是 GCD(n,k)
其中 n 是数组的长度,k 是移位。让我们假设外环的长度应该是 x
在数组旋转的杂耍方法中,在内部循环中,我们基本上将一个数组元素j
的值与另一个数组元素(j+k) mod n
的值交换
arr[j] = arr[ (j+k) % n ]
所以假设对于外循环索引 i=0
,我们将 j 的可能值为 0
、(0+d) mod n
、(0+2d) mod n
、(0+3d) mod n
.... . (0+m*d) mod n
其中 m 是最小的整数,其中 (0+m*d) mod n
变为等于 i(因为它是循环的)。
现在当 j
的值等于 i
时,内循环终止
因此,
(0+m*d) mod n = i
m*d mod n = 0
因此,m*d
是可以被 n 和 d 整除的最小数,并且被 definition 可以被两个数整除的最小数称为这两个数的 LCM。
所以m*d = LCM(n, d)
。这只是一个内部循环。所以一个内循环运行大约 LCM(n, d)
长度(注意这不是内循环的运行时间,而只是它覆盖的长度)。
因此,循环旋转的元素将是 LCM(n,d)/d
,因为我们使用 d
跳转到下一个索引。
所以,
一个循环覆盖LCM(n,d)/d
外循环将运行 x 次,因此将覆盖数组的所有 n 个元素,
x * LCM(n,d) / d = n
我们可以简化上面的等式并重写如下
x = (n*d) / LCM(n,d)
这是 GCD(n,d)。即 GCD 可以通过将两个数字的乘积与其 LCM 相除来计算。
x = GCD(n,d).
<块引用>
为什么一旦我们完成了一个循环,我们就从新的循环开始 下一个元素,即。下一个元素不能已经是 a 的一部分 处理周期?
您可能已经观察到,一旦内循环完成第一个运行长度 (k
),我们在内循环中一次跳跃 LCM(n,d)
个单位,因此它只会在相同的索引上重复而不是在不同的索引上。因此,除非您更改 i
,否则不会触及由于某个起始位置 i
而在第一次运行中未覆盖的索引。 (这就是外循环改变 i
的原因)。
例如让我们取一个数组 A = [1,2,3,4,5,6]
其中 n=6
并取 d=2
。
因此,如果我们跟踪 j
的内循环的索引 i=0
,它将是
j => 0, 2, 4, 0
而对于 i=1
,它将是 j = 1, 3, 5, 1
在这个例子中,GCD(6,2)
是 2。
如果我们选择了一个长度为 5 和 d=2
的数组,那么跟踪将是 j => 0, 2, 4, 1, 3, 0
并且只有一个外循环也满足 GCD(5,2)
= 1 .
答案 4 :(得分:0)
如果我们了解GCD和LCM的含义,我们可以说明为什么我们将GCD用作杂耍算法。
Gcd->可以将两组或更多组项目平均分配到最大的分组中。
Lcm->确定何时会再次发生某些事情
答案 5 :(得分:0)
好吧,我更喜欢这样处理数组旋转,其中 A 是数组 d 是旋转,n 是数组的大小:
对于逆时针旋转:
def rotateArr(A,d,n):
A[:]=A[d:]+A[:d]
return A
答案 6 :(得分:0)
关于 Juggling 算法为何起作用的推理并不简单,而是基于模块化算术。下面是一个非常简短的非正式大纲:
对于任何正整数 a
和 n
,请说 g=gcd(a,n)
和 t=n/g
。那么,形式(ai)mod n
、i = 0,1,2,..,t-1
的数都是不同的,形成集合G = {0,g,2g,...,(n/g-1)g}
。 (这本身需要单独证明,你可以参考this)。
此外,对于任何整数 b
和 0 <= b < g
,形式 (ai+b)mod n
、i = 0,1,2,...,t-1
的数都是不同的并且形成集合 {{1} }.
很容易看出集合 G(b) = {b,g+b,2g+b,...,(n/g-1)g+b}
在任何两个集合之间没有共同的元素。此外,它们共同构成了一组 G(0),G(1),...,G(g-1)
元素:n
。
问题中描述的杂耍算法(也称为海豚算法)在上述结果中具有整数 {0,1,2,...,n-1}
作为 a
。通过迭代 d
,该程序有效地逐个挑选这些 i = 0,1,2,...,g-1
集合(对于 g
)。对于每个集合,它将每个 b=0,1,2,...,g-1
元素移动到它们的最终索引。
更多详情,请参考this。
答案 7 :(得分:-2)
您可以参考 codewhoop 上的视频教程,它将帮助您更好地理解杂耍算法:)