扩展OEIS:计算钻石平铺


46

我保证,这将是我关于diamong拼贴的最后一个挑战(不管怎么说,还是一段时间)。从好的方面来说,这一挑战与ASCII艺术没有任何关系,而且也不是编码竞赛,因此实际上是完全不同的。

提醒一下,每个六角形都可以用三个不同的菱形来命名:

一个有趣的问题是,对于给定的六边形尺寸,这些平铺中存在多少个。似乎已经对这些数字进行了相当彻底的研究,可以在OEIS A008793中找到它们

但是,如果我们问到旋转和反射之前存在多少个平铺,问题就会变得棘手。例如,对于边长N = 2,存在以下20个拼接:

   ____     ____     ____     ____     ____     ____     ____     ____     ____     ____  
  /\_\_\   /\_\_\   /\_\_\   /\_\_\   /_/\_\   /_/\_\   /\_\_\   /_/\_\   /_/\_\   /_/\_\ 
 /\/\_\_\ /\/_/\_\ /\/_/_/\ /\/_/\_\ /\_\/\_\ /\_\/_/\ /\/_/_/\ /\_\/\_\ /\_\/_/\ /_/\/\_\
 \/\/_/_/ \/\_\/_/ \/\_\_\/ \/_/\/_/ \/\_\/_/ \/\_\_\/ \/_/\_\/ \/_/\/_/ \/_/\_\/ \_\/\/_/
  \/_/_/   \/_/_/   \/_/_/   \_\/_/   \/_/_/   \/_/_/   \_\/_/   \_\/_/   \_\/_/   \_\/_/ 
   ____     ____     ____     ____     ____     ____     ____     ____     ____     ____  
  /_/_/\   /\_\_\   /_/\_\   /_/_/\   /_/\_\   /_/\_\   /_/_/\   /_/_/\   /_/_/\   /_/_/\ 
 /\_\_\/\ /\/_/_/\ /_/\/_/\ /\_\_\/\ /\_\/_/\ /_/\/_/\ /_/\_\/\ /\_\_\/\ /_/\_\/\ /_/_/\/\
 \/\_\_\/ \/_/_/\/ \_\/\_\/ \/_/\_\/ \/_/_/\/ \_\/_/\/ \_\/\_\/ \/_/_/\/ \_\/_/\/ \_\_\/\/
  \/_/_/   \_\_\/   \_\/_/   \_\/_/   \_\_\/   \_\_\/   \_\/_/   \_\_\/   \_\_\/   \_\_\/ 

但是其中许多在旋转和反射下都是相同的。如果我们考虑这些对称性,则仅剩下6个不同的平铺:

   ____     ____     ____     ____     ____     ____  
  /\_\_\   /\_\_\   /\_\_\   /_/\_\   /_/\_\   /_/\_\ 
 /\/\_\_\ /\/_/\_\ /\/_/_/\ /\_\/_/\ /\_\/_/\ /_/\/\_\
 \/\/_/_/ \/\_\/_/ \/\_\_\/ \/\_\_\/ \/_/\_\/ \_\/\/_/
  \/_/_/   \/_/_/   \/_/_/   \/_/_/   \_\/_/   \_\/_/ 

   2        2        6        6        1        3

其中的数字表示每个拼贴的多重性。请注意,对于较大的六边形,也存在具有多重性4和12的平铺。

似乎尚未完全研究到对称性的平铺数目。OEIS条目A066931仅列出了五个术语:

1, 1, 6, 113, 20174

其中第一个项是边长N = 0,最后一个是边长N = 4

我敢肯定,我们可以做得更好!

您的任务是计算给定边长的平铺数。

这是。您的分数将是N您的代码在30分钟内在我的计算机上产生正确结果的最高边长。在平局的情况下,我会接受它产生的结果的提交 N最快的。

像往常一样,您不得硬编码已经知道的赢得决胜局的结果。解决的算法N = 3应与解决的算法相同N = 5

您提交的内容不得使用超过4GB的内存。如果您的操作接近该限制,我会为此留出一些余地,但是如果您始终高于该限制,或者如果您大幅超出该限制,我将不计入N您的提交中。

我将在Windows 8计算机上测试所有提交的内容,因此请确保您选择的语言在Windows上免费可用。唯一的例外是Mathematica(因为我碰巧拥有它的许可证)。请提供有关如何编译/运行代码的说明。

当然,您可以在自己的时间随意计算更多的术语(科学和其他人可以根据它们来计算数字),但是答案的分数将在这30分钟内确定。


4
请注意,由于N = 6输出的结果大于10 ^ 12,因此几乎肯定需要采用非建设性的解决方案。
彼得·泰勒

1
@PeterTaylor我希望可以留出更多的改进空间。也许首先是几个简单的构造性答案,可以做N = 5以获得对问题的更多了解,然后可能是不需要构建所有切片的混合方法,但是可以从几个构造的方法中推断总数。然后如果我们真的很幸运的话,可以进行分析。:)
马丁·恩德

2
冒着明显的危险,在我看来,每个这样的平铺都对应于从远方的角度(例如,从(100,-100,100))观察到的单位多维数据集的组合的投影。我发现这减轻了构建图块的负担。
DavidC 2015年

1
@DavidCarraher确实。更具体地,单位立方体的这种布置是3D 杨氏图。(也许对某人
有所

@DavidCarraher如果您足够看重大六角形,您会发现有两种不同的方法将其解释为Young图表。最明显的方法(至少对我来说)是在顶部和左侧看到平坦区域,左上角缺少2x2x1长方体。但是还有另一种查看方式:该区域中有一个空区域,其中有一个2x2x1的长方体。倾斜60度可能会有所帮助。它伤害了我的眼睛,但我认为这两个年轻的图表可能通过其中一个的反射而组合在一起。OEIS A008793的措辞非常谨慎:“ 其年轻图表的平面分区数量...”
Level River St

Answers:


80

代数,图论,莫比乌斯反演,研究和Java

六边形的对称组是12阶的二面体组,由60度旋转和跨直径的反射镜翻转生成。它有16个子组,但其中一些属于非平凡的共轭组(仅具有反射的子组具有3种轴选择),因此六边形的平铺可以具有10种根本不同的对称性:

10个对称图片

可以计算一个三角形格子子集的钻石拼贴数作为行列式,因此我最初的方法是为六边形的每个对称设置一个行列式,以计算至少具有那些对称性的拼贴数; 然后在其位姿的入射代数中使用Möbius求逆(基本上是对包含-排除原理的推广),计算出对称组恰好是10种情况中每一种的平铺数目。但是,某些对称性具有令人讨厌的边缘条件,因此我被迫对许多行列式求和。幸运的是,获得的值n < 10给了我足够的数据,以便能够识别OEIS中的相关序列,并将其拼凑成闭包形式(“闭包”的某个值允许有限乘积)。在我准备证明OEIS序列更新合理的正式文章中,对序列进行了一些讨论,并参考了证明。

一旦完成了重复计算,结果便会发现十个值中的四个完全抵消了,因此我们只需要计算其余六个值,然后进行加权和即可。

此代码N=1000在我的计算机上花费不到30秒。

import java.math.BigInteger;

public class OptimisedCounter {
    private static int[] minp = new int[2];

    public static void main(String[] args) {
        if (args.length > 0) {
            for (String arg : args) System.out.println(count(Integer.parseInt(arg)));
        }
        else {
            for (int n = 0; n < 16; n++) {
                System.out.format("%d\t%s\n", n, count(n));
            }
        }
    }

    private static BigInteger count(int n) {
        if (n == 0) return BigInteger.ONE;

        if (minp.length < 3*n) {
            int[] wider = new int[3*n];
            System.arraycopy(minp, 0, wider, 0, minp.length);
            for (int x = minp.length; x < wider.length; x++) {
                // Find the smallest prime which divides x
                for (wider[x] = 2; x % wider[x] != 0; wider[x]++) { /* Do nothing */ }
            }
            minp = wider;
        }

        BigInteger E = countE(n), R2 = countR2(n), F = countF(n), R3 = countR3(n), R = countR(n), FR = countFR(n);
        BigInteger sum = E.add(R3);
        sum = sum.add(R2.add(R).multiply(BigInteger.valueOf(2)));
        sum = sum.add(F.add(FR).multiply(BigInteger.valueOf(3)));
        return sum.divide(BigInteger.valueOf(12));
    }

    private static BigInteger countE(int n) {
        int[] w = new int[3*n];
        for (int i = 0; i < n; i++) {
            for (int j = i + 1; j <= i + n; j++) w[j]--;
            for (int j = i + n + 1; j <= i + 2*n; j++) w[j]++;
        }
        return powerProd(w);
    }

    private static BigInteger countR2(int n) {
        int[] w = new int[3*n];
        for (int i = 0; i < n; i++) {
            w[3*i+2]++;
            for (int j = 3*i + 1; j <= 2*i + n + 1; j++) w[j]--;
            for (int j = 2*i + n + 1; j <= i + n + n; j++) w[j]++;
        }
        return powerProd(w);
    }

    private static BigInteger countF(int n) {
        int[] w = new int[3*n];
        for (int i = 0; i < n; i++) {
            for (int j = 2*i + 1; j <= 2*i + n; j++) w[j]--;
            for (int j = i + n + 1; j <= i + 2*n; j++) w[j]++;
        }
        return powerProd(w);
    }

    private static BigInteger countR3(int n) {
        if ((n & 1) == 1) return BigInteger.ZERO;
        return countE(n / 2).pow(2);
    }

    private static BigInteger countR(int n) {
        if ((n & 1) == 1) return BigInteger.ZERO;
        int m = n / 2;
        int[] w = new int[3*m-1];
        for (int i = 0; i < m; i++) {
            for (int j = 1; j <= 3*i+1; j++) w[j] += 2;
            for (int j = 1; j <= i + m; j++) w[j] -= 2;
        }
        return powerProd(w);
    }

    private static BigInteger countFR(int n) {
        if ((n & 1) == 1) return BigInteger.ZERO;
        int m = n / 2;
        int[] w = new int[3*n-2];
        for (int j = 1; j <= m; j++) w[j]--;
        for (int j = 2*m; j <= 3*m-1; j++) w[j]++;
        for (int i = 0; i <= 2*m-3; i++) {
            for (int j = i + 2*m + 1; j <= i + 4*m; j++) w[j]++;
            for (int j = 2*i + 3; j <= 2*i + 2*m + 2; j++) w[j]--;
        }
        return powerProd(w);
    }

    private static BigInteger powerProd(int[] w) {
        BigInteger result = BigInteger.ONE;
        for (int x = w.length - 1; x > 1; x--) {
            if (w[x] == 0) continue;

            int p = minp[x];
            if (p == x) result = result.multiply(BigInteger.valueOf(p).pow(w[p]));
            else {
                // Redistribute it. This should ensure we avoid negatives.
                w[p] += w[x];
                w[x / p] += w[x];
            }
        }

        return result;
    }
}

24
您确实是凡人中的神。我希望看到您的解决方案在著名的期刊上发表。
Alex A.

这太棒了。顺便说一句,我的代码(当前未发布)给出了22306956(N = 5):22231176(12)+275(4)+75328(6)+352(2),差异为1,这很奇怪。我不知道您在这里做什么,是否适合对称性分解?对于N = 4,我比您和oeis.org/A066931/a066931.txt16。从该引用看来,我的多重性12太多了16,我需要将其转换为多重性6的32。我不是太惊讶了,即使是N对我来说也更加困难。但是我对奇数N没问题,对于0 <N <4,我得到正确的答案。将查找明显的问题并在明天发布我的代码。
级圣河

@steveverrill,如果我理解这种表示法,则对于N = 5,我将其设置为22231176(12)+ 75328(6)+ 275(4)+ 176(2)。我认为您无法将索引2除以2。(FWIW为奇数,它们都具有穿过两个顶点的对称轴和3阶的旋转对称性)。
彼得·泰勒

@steveverrill,对于N = 4,您的差异似乎非常适合具有对称轴穿过两个边的中点的数字。
彼得·泰勒

3
您解决了这个问题,印象深刻。我希望您最终能发布非数学家可以遵循的答案。
DavidC 2015年

15

C

介绍

正如David Carraher所评论的那样,分析六边形拼贴的最简单方法似乎是利用3维杨氏图的同构性,本质上是一个x,y正方形,其中填充了整数高度条,其z高度必须保持相同或增加接近z轴时。

我以一种算法为基础,该算法用于查找总数,该算法比已发布的算法更适合于对称计数,该算法基于对三个笛卡尔轴之一的偏向。

算法

我首先用1填充x,y和z平面的像元,而其余区域包含零。完成此操作后,我将逐层构建图案,其中每一层都包含与原点具有共同3D曼哈顿距离的像元。如果单元格下面的三个单元格也包含1,则该单元格只能包含1。如果其中任何一个包含0,则该单元格必须为0。

以这种方式建立图案的优势在于,每一层都围绕x = y = z线对称。这意味着可以独立检查每一层的对称性。

对称检查

实体的对称性如下:围绕x = y = z线旋转3倍->围绕六边形中心旋转3倍;关于包含x = y = z线的3个平面和每个轴x,y,z的3个x反射->关于穿过六边形角的线的反射。

这总共只增加了6倍的对称性。为了获得六边形的完全对称性,必须考虑另一种对称性。每个实体(从1构造)具有互补的实体(从0构造)。当N为奇数时,互补实体必须与原始实体不同(因为它们不可能具有相同数量的立方体)。然而,当互补实体旋转时,会发现它的2D表示为钻石拼贴,与原始实体相同(除了2倍对称操作)。在N为偶数的情况下,固体可能是自逆的。

这可以在问题的N = 2的示例中看到。如果从左侧看,第一个六边形看起来像是带有8个小立方体的实心立方体,而最后一个六边形看起来像是带有0个小立方体的空壳。如果从右边看,则相反。第3,第4和第5六角形以及第16、17和18六角形看起来包含2个或6个立方体,因此它们在3个维度上互补。它们通过2倍对称操作(2倍旋转或围绕穿过六边形边缘的轴的反射)在二维上相互关联。另一方面,第9、10、11和12个六边形显示3D图案,是它们自己的补码,因此具有较高的对称性(因此,这是唯一具有奇重数的模式)。

请注意,具有(N ^ 3)/ 2个立方体是进行自我补全的必要条件,但通常,如果N> 2,则不是充分条件。所有这些的结果是,对于奇数N,平铺总是成对出现(N ^ 3)/ 2个立方体,必须仔细检查。

当前代码(为N = 1,2,3,5生成正确的总数。针对N = 4讨论的错误。)

int n;                     //side length

char t[11][11][11];        //grid sized for N up to 10

int q[29][192], r[29];     //tables of coordinates for up to 10*3-2=28 layers 

int c[9];                  //counts arrangements found by symmetry class. c[8] contains total.


//recursive layer counting function. m= manhattan distance, e= number of cells in previous layers, s=symmetry class.
void f(int m,int e,int s){

  int u[64], v[64], w[64]; //shortlists for x,y,z coordinates of cells in this layer
  int j=0;                 
  int x,y,z;

  for (int i=r[m]*3; i; i-=3){
    // get a set of coordinates for a cell in the current layer.
    x=q[m][i-3]; y= q[m][i-2]; z= q[m][i-1];
    // if the three cells in the previous layer are filled, add it to the shortlist u[],v[],w[]. j indicates the length of the shortlist.
    if (t[x][y][z-1] && t[x][y-1][z] && t[x-1][y][z]) u[j]=x, v[j]=y, w[j++]=z ;
  }


  // there are 1<<j possible arrangements for this layer.   
  for (int i = 1 << j; i--;) {

    int d = 0;

    // for each value of i, set the 1's bits of t[] to the 1's bits of i. Count the number of 1's into d as we go.
    for (int k = j; k--;) d+=(t[u[k]][v[k]][w[k]]=(i>>k)&1);

    // we have no interest in i=0 as it is the empty layer and therefore the same as the previous recursion step. 
    // Still we loop through it to ensure t[] is properly cleared.      

    if(i>0){
      int s1=s;    //local copy of symmetry class. 1's bit for 3 fold rotation, 2's bit for reflection in y axis.
      int sc=0;    //symmetry of self-complement.

      //if previous layers were symmetrical, test if the symmetry has been reduced by the current layer 
      if (s1) for (int k = j; k--;) s1 &= (t[u[k]][v[k]][w[k]]==t[w[k]][u[k]][v[k]]) | (t[u[k]][v[k]][w[k]]==t[w[k]][v[k]][u[k]])<<1;

      //if exactly half the cells are filled, test for self complement
      if ((e+d)*2==n*n*n){
        sc=1;
        for(int A=1; A<=(n>>1); A++)for(int B=1; B<=n; B++)for(int C=1; C<=n; C++) sc&=t[A][B][C]^t[n+1-A][n+1-B][n+1-C];
      }

      //increment counters for total and for symmetry class.
      c[8]++; c[s1+(sc<<2)]++;

      //uncomment for graphic display of each block stacking with metadata. not recommended for n>3.
      //printf("m=%d  j=%d  i=%d c1=%d-2*%d=%d c3=%d cy=%d(cs=%d) c3v=%d ctot=%d\n",m,j,i,c[0],c[2],c[0]-2*c[2],c[1],c[2],c[2]*3,c[3],c[8]);
      //printf("m=%d  j=%d  i=%d C1=%d-2*%d=%d C3=%d CY=%d(CS=%d) C3V=%d ctot=%d\n",m,j,i,c[4],c[6],c[4]-2*c[6],c[5],c[6],c[6]*3,c[7],c[8]);
      //for (int A = 0; A<4; A++, puts(""))for (int B = 0; B<4; B++, printf(" "))for (int C = 0; C<4; C++) printf("%c",34+t[A][B][C]);

      //recurse to next level.
      if(m<n*3-2)f(m + 1,e+d,s1);

    }
  } 
}

main()
{
  scanf("%d",&n);

  int x,y,z;

  // Fill x,y and z planes of t[] with 1's
  for (int a=0; a<9; a++) for (int b=0; b<9; b++) t[a][b][0]= t[0][a][b]= t[b][0][a]= 1;

  // Build table of coordinates for each manhattan layer
  for (int m=1; m < n*3-1; m++){
    printf("m=%d : ",m);
    int j=0;
    for (x = 1; x <= n; x++) for (y = 1; y <= n; y++) {
      z=m+2-x-y;
      if (z>0 && z <= n) q[m][j++] = x, q[m][j++] = y, q[m][j++]=z, printf(" %d%d%d ",x,y,z);
      r[m]=j/3;
    }
    printf(" : r=%d\n",r[m]);
  }

  // Set count to 1 representing the empty box (symmetry c3v)
  c[8]=1; c[3]=1; 

  // Start searching at f=1, with 0 cells occupied and symmetry 3=c3v
  f(1,0,3); 

  // c[2 and 6] only contain reflections in y axis, therefore must be multiplied by 3.
  // Similarly the reflections in x and z axis must be subtracted from c[0] and c[4].
  c[0]-=c[2]*2; c[2]*=3; 
  c[4]-=c[6]*2; c[6]*=3;



  int cr[9];cr[8]=0;
  printf("non self-complement                   self-complement\n");
  printf("c1  %9d/12=%9d           C1  %9d/6=%9d\n",   c[0], cr[0]=c[0]/12,     c[4], cr[4]=c[4]/6);
  if(cr[0]*12!=c[0])puts("c1 division error");if(cr[4]*6!=c[4])puts("C1 division error");

  printf("c3  %9d/4 =%9d           C3  %9d/2=%9d\n",   c[1], cr[1]=c[1]/4,      c[5], cr[5]=c[5]/2);
  if(cr[1]*4!=c[1])puts("c3 division error");if(cr[5]*2!=c[5])puts("C3 division error");

  printf("cs  %9d/6 =%9d           CS  %9d/3=%9d\n",   c[2], cr[2]=c[2]/6,      c[6], cr[6]=c[6]/3);
  if(cr[2]*6!=c[2])puts("cs division error");if(cr[6]*3!=c[6])puts("CS division error");

  printf("c3v %9d/2 =%9d           C3V %9d/1=%9d\n",   c[3], cr[3]=c[3]/2,      c[7], cr[7]=c[7]);
  if(cr[3]*2!=c[3])puts("c3v division error");  

  for(int i=8;i--;)cr[8]+=cr[i]; 
  printf("total =%d unique =%d",c[8],cr[8]);    
}

输出量

程序根据实体的8种对称性生成8个条目的输出表。实体可以具有以下4种对称形式中的任何一种(Schoenflies表示法)

c1: no symmetry
c3: 3-fold axis of rotation (produces 3-fold axis of rotation in hexagon tiling)
cs: plane of reflection (produces line of reflection in hexagon tiling)
c3v both of the above (produces 3-fold axis of rotation and three lines of reflection through the hexagon corners)

另外,当实体正好具有1的单元格的一半和0的单元格的一半时,存在翻转所有1和0的单元格,然后通过立方体空间的中心反转坐标的可能性。这就是我所说的自补数,但是更多的数学术语是“关于反转中心的反对称”。

这种对称操作在六角形拼贴中提供了2倍的旋转轴。

具有这种对称性的图案在单独的栏中列出。它们仅在N为偶数的位置出现。

对于N = 4,我的计数似乎略有下降。在与Peter Taylor的讨论中,似乎我没有检测到仅具有穿过六边形边缘的直线对称性的平铺。大概是因为我没有针对除(反转)x(同一性)以外的操作测试自补(反对称)。针对操作数(反转)x(反射)和(反转)x(3倍旋转)的自补测试)可能会发现缺失的对称性。然后,我希望N = 4的数据的第一行看起来像这样(c1中少16个,C1中多32个):

c1   224064/12=18672          C1  534/6=89

这将使总数与Peter的答案和https://oeis.org/A066931/a066931.txt保持一致

电流输出如下。

N=1
non self-complement     self-complement
c1      0/12= 0           C1  0/6= 0
c3      0/4 = 0           C3  0/2= 0
cs      0/6 = 0           CS  0/3= 0
c3v     2/2 = 1           C3V 0/1= 0
total =2 unique =1

non self-complement     self-complement
N=2
c1      0/12= 0           C1  0/6= 0
c3      0/4 = 0           C3  0/2= 0
cs     12/6 = 2           CS  3/3= 1
c3v     4/2 = 2           C3V 1/1= 1
total =20 unique =6

N=3
non self-complement     self-complement
c1    672/12=56           C1  0/6= 0
c3      4/4 = 1           C3  0/2= 0
cs    288/6 =48           CS  0/3= 0
c3v    16/2 = 8           C3V 0/1= 0
total =980 unique =113

N=4 (errors as discussed)
non self-complement     self-complement
c1   224256/12=18688          C1  342/6=57
c3       64/4 =16             C3  2/2= 1
cs     8064/6 =1344           CS  54/3=18
c3v      64/2 =32             C3V 2/1= 2
total =232848 unique =20158

N=5
non self-complement     self-complement
c1  266774112/12=22231176        C1  0/6= 0
c3       1100/4 =275             C3  0/2= 0
cs     451968/6 =75328           CS  0/3= 0
c3v       352/2 =176             C3V 0/1= 0
total =267227532 unique =22306955

待办事项清单(已更新)

整理当前代码。

完成或多或少

实现当前层的对称性检查,并传递上一层对称性的参数(检查最后一层是否不对称毫无意义。)

完成,奇数N的结果与发布的数据一致

添加一个选项以抑制对不对称数字的计数(应该运行得更快)

这可以通过在递归调用中添加另一个条件来完成:if(s1 && m<n*3-2)f(m + 1,e+d,s1)将N = 5的运行时间从5分钟减少到大约一秒钟。结果,输出的第一行变成了总垃圾(总总数也是如此),但是如果已经从OEIS中得知了总垃圾,那么可以重新构造非对称平铺的数量,至少对于奇数N是可以的。

但是,即使是N,也会丢失自补体的不对称(根据c3v对称性)实体的数量。对于这种情况,可能需要一个单独的程序,该程序专门用于具有(N ** 3)/ 2个单元格正好为1的实体。使用此功能(并正确计数),可以尝试N = 6,但是运行将花费很长时间。

实现对单元格的计数,以减少对多达(N ^ 3)/ 2个立方体的搜索。

未完成,预计节省的成本很小

对包含正好(N ^ 3)/ 2个立方体的图案实施对称性(互补固体)检查。

完成,但似乎有遗漏,请参阅N = 4。

找到一种从不对称的数字中选择词法最低的数字的方法。

节省不会那么大。抑制不对称图形可以消除大多数情况。唯一检查的反射是通过y轴的平面(x和z随后乘以3计算得出。)仅具有旋转对称性的图形都以其两种对映体形式计数。如果只计算一个,则运行速度可能快将近两倍。

为了简化此过程,可能会改进列出每一层中的坐标的方式(它们由简并的6或3组组成,在该层的正中央可能有1组)。

有趣,但网站上可能还有其他问题需要探讨。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.