这个游戏背后的math/计算原理是什么?

我的孩子们有这个有趣的游戏叫Spot It! 游戏的限制(我可以描述的最好的)是:

  • 这是一副55张牌
  • 在每张卡上有8张独特的照片(即一张卡不能有2张相同的照片)
  • 鉴于从甲板上select的任何2张卡,有1个,只有1个匹配的图片
  • 匹配的图片可能在不同的卡片上有不同的缩放比例,但这只是为了让游戏变得更加困难(即小树仍然匹配更大的树)

游戏的原理是:翻转2张牌,谁先挑选匹配的图片得到一个点。

这是澄清的图片:

发现它

(例如:从上面两张卡片可以看出,匹配的图片是绿色的恐龙,在右下angular和中间的图片之间,这是一个小丑的头)。

我想了解以下内容:

  1. 满足这些标准所需的不同图片的最小数量是多less?您如何确定?

  2. 使用伪代码(或Ruby),你将如何从N张图片(其中N是问题1中的最小数字)中生成55张游戏卡?

更新:

每张卡片的确出现两次以上(与一些猜测相反)。 看到这张3张卡片,每个都有一个闪电: 3张牌

有限投影几何

投影(平面)几何的公理与欧几里德几何稍有不同:

  • 每两点有一条直线穿过它们(这是相同的)。
  • 每两条线恰好相交一点(这与欧几里得有点不同)。

现在,添加“有限”的汤,你有问题:

我们可以只有2点的几何? 有3分? 用4? 用7?

关于这个问题仍然存在悬而未决的问题,但我们知道这一点:

  • 如果存在具有Q点的几何,则Q = n^2 + n + 1n称为几何的order数。
  • 每行有n+1个点。
  • 从每一点开始,精确地传递n+1行。
  • 总行数也是Q

  • 最后,如果n是素数,那么确实存在n阶几何。


有人可能会问,这个谜题有什么关系呢?

card而不是pointpicture而不是line和公理成为:

  • 每张卡片都有一张共同的照片。
  • 对于每两张图片,都有一张同时拥有两张图片的卡片。

现在让我们取n=7 ,我们有order-7有限几何, Q = 7^2 + 7 + 1 。 这使Q=57行(图片)和Q=57点(卡)。 我猜拼图制造者决定55比57更圆,剩下2张牌。

我们也得到n+1 = 8 ,所以从每一个点(卡),8行通过(8张图片出现),每一行(图片)有8个点(出现在8卡)。


这里是最有名的有限投影(2阶)平面(几何)的表示,有7个点,被称为Fano平面 ,从Noelle Evans复制- 有限几何问题页面

在这里输入图像说明

我正在考虑创build一张图片来解释上面的2号飞机如何可以做成7张卡片和7张图片的类似拼图,但是从math交换孪生问题的链接就有这样一个图表: Dobble-et- LA-几何学,finie

法诺飞机

因此,总共有n张图片的池中有k = 55张包含m = 8张图片的卡片。 我们可以重申一个问题:“我们需要多less张图片,这样我们就可以在任何一张卡片之间build立一张只有一张共享图片的k张卡片? 相当于问:

给定一个n维向量空间和所有向量的集合,其中包含正好m个元素等于一个,所有其他零,有多大n ,以便我们可以find一组k向量,其成对的点积是全部等于1

有( nselectm )可能的向量来构build对。 所以我们至less需要一个足够大的n使得( nselectm )> = k 。 这只是一个下界,所以为了实现成对的兼容性约束,我们可能需要更高的n

只是为了试验一下,我写了一个小型的Haskell程序来计算有效卡组:

编辑:我刚刚看到尼尔和Gajet的解决scheme后,我使用的algorithm并不总是find最好的解决scheme,所以下面的一切都不一定有效。 我会尽快更新我的代码。

 module Main where cardCandidates nm = cardCandidates' [] (nm) m cardCandidates' buildup 0 0 = [buildup] cardCandidates' buildup zc oc | zc>0 && oc>0 = zerorec ++ onerec | zc>0 = zerorec | otherwise = onerec where zerorec = cardCandidates' (0:buildup) (zc-1) oc onerec = cardCandidates' (1:buildup) zc (oc-1) dot xy = sum $ zipWith (*) xy compatible xy = dot xy == 1 compatibleCards = compatibleCards' [] compatibleCards' valid [] = valid compatibleCards' valid (c:cs) | all (compatible c) valid = compatibleCards' (c:valid) cs | otherwise = compatibleCards' valid cs legalCardSet nm = compatibleCards $ cardCandidates nm main = mapM_ print [(n, length $ legalCardSet nm) | n<-[m..]] where m = 8 

由此产生的最大数量的兼容卡,每张卡m = 8张图片,对于前几个n,n中select的不同图片数目看起来像这样:

尽pipe由于组合爆炸,这个蛮力方法并没有得到很大的进展。 但我认为这可能还是有趣的。

有趣的是,似乎对于给定的mk随着n的增加而增加,直到某个n之后,它保持不变。

这意味着,对于每个卡片的每个图片数量,有一定数量的图片可供select,这导致了最大数量的合法卡片。 添加更多的图片来select过去最佳的数字不会增加合法卡的数量。

前几个最优k是:

最佳的k表

我刚刚find了一个57或58张照片的方法,但是现在我头疼得厉害,我睡了8-10个小时后就会发布ruby代码! 只是提示我的解决scheme,每7张卡片有相同的标记,总共可以使用我的解决scheme构build56张卡片。

这里是生成ypercube所讨论的全部57个卡片的代码。 它使用正好57张图片,抱歉的人我已经写了实际的C ++代码,但知道vector <something>是一个包含types值的数组,很容易理解这段代码的作用。 并且该代码使用每个包含P+1图片的P^2+P+1图片并且仅共享1个图片来针对每个素数P值生成P^2+P+1卡片。 这意味着我们可以使用7张图片,每张图片有3张图片(对于p = 2),13张图片使用13张图片(对于p = 3),31张图片使用31张图片(对于p = 5) (对于p = 7)等等…

 #include <iostream> #include <vector> using namespace std; vector <vector<int> > cards; void createcards(int p) { cards.resize(0); for (int i=0;i<p;i++) { cards.resize(cards.size()+1); for(int j=0;j<p;j++) { cards.back().push_back(i*p+j); } cards.back().push_back(p*p+1); } for (int i=0;i<p;i++) { for(int j=0;j<p;j++) { cards.resize(cards.size()+1); for(int k=0;k<p;k++) { cards.back().push_back(k*p+(j+i*k)%p); } cards.back().push_back(p*p+2+i); } } cards.resize(cards.size()+1); for (int i=0;i<p+1;i++) cards.back().push_back(p*p+1+i); } void checkCards() { cout << "---------------------\n"; for(unsigned i=0;i<cards.size();i++) { for(unsigned j=0;j<cards[i].size();j++) { printf("%3d",cards[i][j]); } cout << "\n"; } cout << "---------------------\n"; for(unsigned i=0;i<cards.size();i++) { for(unsigned j=i+1;j<cards.size();j++) { int sim = 0; for(unsigned k=0;k<cards[i].size();k++) for(unsigned l=0;l<cards[j].size();l++) if (cards[i][k] == cards[j][l]) sim ++; if (sim != 1) cout << "there is a problem between cards : " << i << " " << j << "\n"; } } } int main() { int p; for(cin >> p; p!=0;cin>> p) { createcards(p); checkCards(); } } 

再次抱歉延迟的代码。

这里是Gajet的Python解决scheme,因为我发现Python更具可读性。 我已经修改它,以便它与非素数一起工作。 我已经使用Thies洞察力来生成一些更容易理解的显示代码。

 from __future__ import print_function from itertools import * def create_cards(p): for min_factor in range(2, 1 + int(p ** 0.5)): if p % min_factor == 0: break else: min_factor = p cards = [] for i in range(p): cards.append(set([i * p + j for j in range(p)] + [p * p])) for i in range(min_factor): for j in range(p): cards.append(set([k * p + (j + i * k) % p for k in range(p)] + [p * p + 1 + i])) cards.append(set([p * p + i for i in range(min_factor + 1)])) return cards, p * p + p + 1 def display_using_stars(cards, num_pictures): for pictures_for_card in cards: print("".join('*' if picture in pictures_for_card else ' ' for picture in range(num_pictures))) def check_cards(cards): for card, other_card in combinations(cards, 2): if len(card & other_card) != 1: print("Cards", sorted(card), "and", sorted(other_card), "have intersection", sorted(card & other_card)) cards, num_pictures = create_cards(7) display_using_stars(cards, num_pictures) check_cards(cards) 

输出:

 *** * *** * **** * * * * * * * * * * * * * * * * * ** * ** * * * * * * * * * * * * * * **** 

其他人已经描述了devise的一般框架(有限投影平面),并且展示了如何生成有序的素数阶投影平面。 我只想填补一些空白。

有限投影平面可以产生许多不同的顺序,但是在素数阶p的情况下它们是最直接的。 然后整数模p形成一个有限域,可以用来描述平面上的点和线的坐标。 有三种不同的坐标: (1,x,y)(0,1,x)(0,0,1) ,其中xy可以取0p-1 。 3种不同的点解释了系统中点数的公式p^2+p+1 。 我们也可以用相同的3种不同的坐标来描述直线: [1,x,y][0,1,x][0,0,1]

我们通过坐标的点积是否等于0 mod p来计算点和线是否入射。 因此,例如,当p=7时,点(1,2,5)和线[0,1,1]是入射的,因为1*0+2*1+5*1 = 7 == 0 mod 7 ,但是由于1*1+3*2+3*6 = 25 != 0 mod 7(1,3,3)和线[1,2,6]不会发生。

翻译成卡片和图片的语言,这意味着坐标为(1,2,5)的卡片包含坐标为[0,1,1]的图片,但坐标为(1,3,3)的卡片不包含坐标[1,2,6]的图片。 我们可以使用这个程序来开发完整的卡片列表和它们包含的图片。

顺便说一下,我认为把图像看成是点和线是比较容易的,但是点和线之间的投影几何有一个双重性,所以它并不重要。 但是,在下面的内容中,我将使用点数来表示图片和线条。

任何有限的领域的相同的build设工程。 我们知道当且仅当q=p^k是一个素数幂时,有一个有序的q阶域。 该字段被称为GF(p^k) ,代表“伽罗瓦域”。 在主要案例中,这些领域并不像在首要案例中那样容易构build。

幸运的是,辛勤工作已经在自由软件,即Sage中完成和实施。 例如,要获得4阶投影平面devise,只需键入

 print designs.ProjectiveGeometryDesign(2,1,GF(4,'z')) 

你会得到看起来像输出

 ProjectiveGeometryDesign<points=[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20], blocks=[[0, 1, 2, 3, 20], [0, 4, 8, 12, 16], [0, 5, 10, 15, 19], [0, 6, 11, 13, 17], [0, 7, 9, 14, 18], [1, 4, 11, 14, 19], [1, 5, 9, 13, 16], [1, 6, 8, 15, 18], [1, 7, 10, 12, 17], [2, 4, 9, 15, 17], [2, 5, 11, 12, 18], [2, 6, 10, 14, 16], [2, 7, 8, 13, 19], [3, 4, 10, 13, 18], [3, 5, 8, 14, 17], [3, 6, 9, 12, 19], [3, 7, 11, 15, 16], [4, 5, 6, 7, 20], [8, 9, 10, 11, 20], [12, 13, 14, 15, 20], [16, 17, 18, 19, 20]]> 

我把上面的解释如下:从0到20有21张图片。每个块(投影几何中的线)告诉我哪张图片出现在卡片上。 例如,第一张卡片将有图片0,1,2,3和20; 第二张卡片将有图片0,4,8,12和16; 等等。

订单7的系统可以通过

 print designs.ProjectiveGeometryDesign(2,1,GF(7)) 

生成输出

 ProjectiveGeometryDesign<points=[0, 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, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56], blocks=[[0, 1, 2, 3, 4, 5, 6, 56], [0, 7, 14, 21, 28, 35, 42, 49], [0, 8, 16, 24, 32, 40, 48, 50], [0, 9, 18, 27, 29, 38, 47, 51], [0, 10, 20, 23, 33, 36, 46, 52], [0, 11, 15, 26, 30, 41, 45, 53], [0, 12, 17, 22, 34, 39, 44, 54], [0, 13, 19, 25, 31, 37, 43, 55], [1, 7, 20, 26, 32, 38, 44, 55], [1, 8, 15, 22, 29, 36, 43, 49], [1, 9, 17, 25, 33, 41, 42, 50], [1, 10, 19, 21, 30, 39, 48, 51], [1, 11, 14, 24, 34, 37, 47, 52], [1, 12, 16, 27, 31, 35, 46, 53], [1, 13, 18, 23, 28, 40, 45, 54], [2, 7, 19, 24, 29, 41, 46, 54], [2, 8, 14, 27, 33, 39, 45, 55], [2, 9, 16, 23, 30, 37, 44, 49], [2, 10, 18, 26, 34, 35, 43, 50], [2, 11, 20, 22, 31, 40, 42, 51], [2, 12, 15, 25, 28, 38, 48, 52], [2, 13, 17, 21, 32, 36, 47, 53], [3, 7, 18, 22, 33, 37, 48, 53], [3, 8, 20, 25, 30, 35, 47, 54], [3, 9, 15, 21, 34, 40, 46, 55], [3, 10, 17, 24, 31, 38, 45, 49], [3, 11, 19, 27, 28, 36, 44, 50], [3, 12, 14, 23, 32, 41, 43, 51], [3, 13, 16, 26, 29, 39, 42, 52], [4, 7, 17, 27, 30, 40, 43, 52], [4, 8, 19, 23, 34, 38, 42, 53], [4, 9, 14, 26, 31, 36, 48, 54], [4, 10, 16, 22, 28, 41, 47, 55], [4, 11, 18, 25, 32, 39, 46, 49], [4, 12, 20, 21, 29, 37, 45, 50], [4, 13, 15, 24, 33, 35, 44, 51], [5, 7, 16, 25, 34, 36, 45, 51], [5, 8, 18, 21, 31, 41, 44, 52], [5, 9, 20, 24, 28, 39, 43, 53], [5, 10, 15, 27, 32, 37, 42, 54], [5, 11, 17, 23, 29, 35, 48, 55], [5, 12, 19, 26, 33, 40, 47, 49], [5, 13, 14, 22, 30, 38, 46, 50], [6, 7, 15, 23, 31, 39, 47, 50], [6, 8, 17, 26, 28, 37, 46, 51], [6, 9, 19, 22, 32, 35, 45, 52], [6, 10, 14, 25, 29, 40, 44, 53], [6, 11, 16, 21, 33, 38, 43, 54], [6, 12, 18, 24, 30, 36, 42, 55], [6, 13, 20, 27, 34, 41, 48, 49], [7, 8, 9, 10, 11, 12, 13, 56], [14, 15, 16, 17, 18, 19, 20, 56], [21, 22, 23, 24, 25, 26, 27, 56], [28, 29, 30, 31, 32, 33, 34, 56], [35, 36, 37, 38, 39, 40, 41, 56], [42, 43, 44, 45, 46, 47, 48, 56], [49, 50, 51, 52, 53, 54, 55, 56]]> 

使用z3定理certificate

P是每个卡片的符号数量。 根据这篇文章和ypercubeᵀᴹ的回答,分别有N = P**2 - P + 1卡片和符号。 一副扑克牌可以用其关联matrix来表示,每张牌有一行,每个可能的符号有一列。 它的(i,j)元素是1如果卡片i有符号j的话。 我们只需要考虑这些约束来填充这个matrix:

  • 每个元素是零或一个
  • 每一行的总和恰好是P
  • 每列的总和恰好是P
  • 任何两行必须只有一个共同的符号

这意味着N**2variables和N**2 + 2*N + (N choose 2)约束。 对于小型投入来说, z3在不太长的时间似乎是可以pipe理的。

编辑 :不幸的是,P = 8对于这种方法似乎太大了。 计算时间14小时后,我杀死了这个进程。

 from z3 import * from itertools import combinations def is_prime_exponent(K): return K > 1 and K not in 6 # next non-prime exponent is 10, # but that is too big anyway def transposed(rows): return zip(*rows) def spotit_z3(symbols_per_card): K = symbols_per_card - 1 N = symbols_per_card ** 2 - symbols_per_card + 1 if not is_prime_exponent(K): raise TypeError("Symbols per card must be a prime exponent plus one.") constraints = [] # the rows of the incidence matrix s = N.bit_length() rows = [[BitVec("r%dc%d" % (r, c), s) for c in range(N)] for r in range(N)] # every element must be either 1 or 0 constraints += [Or([elem == 1, elem == 0]) for row in rows for elem in row] # sum of rows and cols must be exactly symbols_per_card constraints += [Sum(row) == symbols_per_card for row in rows] constraints += [Sum(col) == symbols_per_card for col in transposed(rows)] # Any two rows must have exactly one symbol in common, in other words they # differ in (symbols_per_card - 1) symbols, so their element-wise XOR will # have 2 * (symbols_per_card - 1) ones. D = 2 * (symbols_per_card - 1) for row_a, row_b in combinations(rows, 2): constraints += [Sum([a ^ b for a, b in zip(row_a, row_b)]) == D] solver = Solver() solver.add(constraints) if solver.check() == unsat: raise RuntimeError("Could not solve it :(") # create the incidence matrix model = solver.model() return [[model[elem].as_long() for elem in row] for row in rows] if __name__ == "__main__": import sys symbols_per_card = int(sys.argv[1]) incidence_matrix = spotit_z3(symbols_per_card) for row in incidence_matrix: print(row) 

结果

 $python spotit_z3.py 3 [0, 0, 1, 1, 0, 1, 0] [0, 0, 0, 0, 1, 1, 1] [0, 1, 0, 1, 0, 0, 1] [1, 1, 0, 0, 0, 1, 0] [0, 1, 1, 0, 1, 0, 0] [1, 0, 0, 1, 1, 0, 0] [1, 0, 1, 0, 0, 0, 1] python spotit_z3.py 3 1.12s user 0.06s system 96% cpu 1.225 total $ time python3 spotit_z3.py 4 [0, 0, 0, 0, 0, 0, 1, 0, 1, 1, 0, 1, 0] [0, 0, 0, 0, 0, 1, 0, 1, 1, 0, 1, 0, 0] [0, 0, 0, 1, 0, 1, 1, 0, 0, 0, 0, 0, 1] [0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 1, 1, 0] [0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 1, 1] [0, 0, 1, 0, 1, 1, 0, 0, 0, 1, 0, 0, 0] [0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 1] [0, 1, 0, 0, 1, 0, 1, 1, 0, 0, 0, 0, 0] [0, 1, 1, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0] [1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1] [1, 0, 0, 1, 0, 0, 0, 1, 0, 1, 0, 0, 0] [1, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0] [1, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 0] python spotit_z3.py 4 664.62s user 0.15s system 99% cpu 11:04.88 total $ time python3 spotit_z3.py 5 [1, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0] [0, 0, 1, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0] [0, 0, 0, 1, 1, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0] [0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0] [0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 1, 0, 0, 0, 1, 1, 0, 1, 0, 0, 0] [0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 0, 1, 0, 0, 0, 1, 0, 0, 1, 0] [0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 1, 0, 1] [0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 1, 1, 0, 1, 0, 0, 0, 0, 0, 0] [0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] [0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0] [0, 1, 1, 0, 1, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0] [0, 0, 0, 1, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 1] [1, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0] [0, 1, 0, 1, 0, 0, 0, 0, 1, 0, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0] [0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 1, 1, 0] [0, 0, 1, 0, 0, 0, 0, 0, 0, 1, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 1] [1, 1, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1] [1, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0] [0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 1, 0, 1, 0, 0, 0, 0] [0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0, 0, 1] [1, 0, 0, 0, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0] python spotit_z3.py 5 1162.72s user 20.34s system 99% cpu 19:43.39 total $ time python3 spotit_z3.py 8 <I killed it after 14 hours of run time.> 

对于那些无法用57分来描绘投影平面几何的人来说,有一个非常好的,直观的方式来构build57卡和57个符号的游戏(根据Yuval Filmus对这个问题的回答 ):

  1. 对于有8个符号的卡片,创build一个7×7独特符号网格。
  2. 为0到6的“斜率”额外添加8个符号,再加上一个用于无限斜率的符号。
  3. 每张牌是网格上的一条线(7个符号),加上斜线设置的斜线上的一个符号。 线条有一个偏移量(即左边的起点)和一个斜率(即每个步骤右边有多less个符号)。 当线路离开顶部的网格时,重新进入底部。 看到这个例子图(从boardgamegeek图片)两个这样的卡:

两个示例卡(红色和绿色)从网格线中取出

在这个例子中,我用一条斜率为零的线(红色),另一条用斜率为1的(绿色)。 它们恰好相交于一个共同点(猫头鹰)。

这种方法可以确保任何两张卡都有一个共同的符号,因为

  1. 如果斜坡不同,那么这些线将总是正好相交于一点。
  2. 如果斜率相同,那么这些线不会相交,并且网格中将没有共同的符号。 在这种情况下,斜率符号将是相同的。

这样我们可以构build7x7的卡片(7个偏移和7个斜坡)。

我们还可以通过网格从垂直线上构build七张额外的卡(即每列)。 对于那些,使用无穷斜率图标。

由于每张卡片由网格中的七个符号和正好一个“斜率”符号组成,因此我们可以创build一个额外的卡片,它只包含所有8个斜率符号。

这给我们留下了7×8 + 1 = 57个可能的卡片,以及7×7 + 8 = 57个所需的符号。

(当然,这只适用于素数大小的网格(例如n = 7),否则,如果斜率是网格大小的除数,则不同斜率的线可以具有零个或多个交点。