內容目录

SudokuStudyLib

_images/logo.jpg

**这是一个让学生学习逻辑及Python OOP基础程式设计的一个Python3 程式库。 ** 数独是一种益智类游戏,同时也是学习逻辑的最好工具之一,而Python 则是世界上最好的初学者学习程式设计的电脑语言。所以,如果能够结合这两种来教导儿童、学生、青少年来学习逻辑的话,那将会是最好的组合!这也是这个专案的缘起,也是这个专案追求的目标。

欢迎来到人类最单纯的世界:数独与逻辑

内容:

安装与使用

这个专案是一个Python 程式库以便让使用者学习逻辑与Python 程式设计。它包含了两个套件,一个是Sudoku,另一个是Matrix。 Sudoku 是一个以物件导向程式设计的模组,而Matrix 则是一个传统程式设计的模组。

这个专案与文件只要是提供给Sudoku 套件来使用。 Matrix 只是提供给传统程式设计者做一参考。

安装方法

你可以使用 pip 来安装这个专案:

pip install SudokuStudyLib

你也可以到下列网址来复制整个专案:

https://github.com/RobertOfTaiwan/SudokuStudyLib

当你安装完毕后,你安装的目录应包含了两个套件,sudoku 及matrix。下面就是整个目录的结构表:

_images/p1.png

使用方法

  1. 物件导向:sudoku,在 test.py:

    from sudoku import *
    
    # to solve a sudoku defined in data directory
    solve("m18.data")
    
    pass
    
    # to solve a sudoku and just using the methods which level <= 15 and if can't solve, don't use guess method
    solve("m3.data", level_limit=15, use_try=False)
    
    pass
    
    # to solve a sudoku with emulator methods and print the steps
    solve("m12.data", use_emu=True, print_step=True)
    
    pass
    
    # to solve the world's best difficult sudoku
    # by default method
    solve("m10.data")
    
    # by computer's try error
    try_error(None, file="m10.data")
    
    # by all methods but not using human guessing, it can't solve the sudoku
    solve("m10.data", use_emu=True, use_try=False)
    
    # by basic human methods and guess
    solve("m10.data", level_limit=10, use_try=True)
    solve("m10.data", level_limit=3, use_try=True)
    
  2. 传统方法 :matrix,在 test.py:

    from matrix import *
    
    # solve it directly
    m, n, p = main("m6.data")
    
    # solve it by limit methods, it can't solve the sudoku
    m, n, p = main("m3.data", methods=8)
    
    # set the limit methods to the 10, and it can solve the sudoku
    m, n, p = main("m3.data", methods=10)
    
    # using the try error's method to solve the best difficult sudoku in the world
    m, n, p = TryError("m10.data")
    

数独(Sudoku)基础知识

数独(Sudoku)是一种智力游戏, 也是一种学习逻辑的最好工具. 而Python 则是世界上最好的电脑程式语言之一. 所以如果能够结合这两种工具, 来教导小孩或青少年来学习逻辑的话, 那就是最好的组合. 这就是这个专案的缘起, 也是这个专案的目标.

一个典型的数独题目

你可以从下列网站取得与学习数独(sudoku)的相关知识:http://en.wikipedia.org/wiki/Sudoku

下面是一个典型的数独题目:

 

下面是一个典型解出来的数独:

sudoku_init   sudoku_result

基本规则

数独(Sudoku)的基本规则非常简单:

  1. 将数字1-9 填到每一行(包含x-way 及y-way)及每一个3x3 区块。

  2. 每行及每个区块的数字不能够重复。

在一个9x9 数独中, 可以组成多少种可能的游戏?

假如我们开始在位置(1, 1) 中置放一个数, 那必然是有9个可能的数字让我们选择. 然后当我要置放第二个数到位置(1, 2) 时, 那我们会有8个数字可以选择. 所以, 以此​​类推, 从上往下, 从左到右, 我们可以写下每一个位置可以被选择的数字:

9! 6! 3! 6! 3! 1! 3! 1! 1!
9 6 3 6 3 1 3 1 1
8 5 2 5 2 1 2 1 1
7 4 1 4 1 1 1 1 1
6 3 1 3 1 1 1 1 1
5 2 1 2 1 1 1 1 1
4 1 1 1 1 1 1 1 1
3 1 1 1 1 1 1 1 1
2 1 1 1 1 1 1 1 1
1 1 1 1 1 1 1 1 1

所以, 所有的组合就是9!*6!*3!*6!*3!*1!*3!*1!*1* = 4,514,807,808,000

假如我们使用 python 来计算时:

>>> def n(x):
    if x==1:
        return 1
    else:
        return x*n(x-1)

>>> n(9)*n(6)*n(3)*n(6)*n(3)*n(1)*n(3)*n(1)*n(1)

关于数独的数学知识, 你可以到下列网站来取得, http://en.wikipedia.org/wiki/Mathematics_of_Sudoku

「逻辑」是什么?

对一个开始学习去探索这个世界的学子而言,逻辑是最重要的工具之一。

最基础的逻辑就是二分法,也就是「是」与「非」,或者在电脑科学里面的「0」与「1」。这是人类在认知上、与他人沟通上的最小分类方法。所以学习逻辑对中小学生而言是非常重要的,因为这是所有知识的基础,也是所有信仰的基础。

**如果依某一特定的角度、在某一特定时候,你无法判断某个事物是「是」或「非」、或者「存在」或「不存在」,那你就是不了解这个世界,同时你也不了解你自己。 **

从解数独来学习逻辑

学习逻辑可以变成一件有趣的事情,如果我们能够从游戏中去学习的话,而数独就是一种很好玩的游戏。我们可以列举很多以数独来学习逻辑的优点:

  • 它的规则相当简单, 任何一个人能够在5分钟内了解它。

  • 它又够复杂,它可以有多达数十亿种的组合。

  • 它可以分各种不同的困难程度,来让不同程度的人都能使用。

以学习 Python 程式设计来学习逻辑

学习一种电脑语言是一种很直觉的方法来学习逻辑。 Python 是一种直译式电脑语言,提供一种简便的方式来实作程式的设计。你可以在下面官方网站来取得该电脑语言的所有资源: https://www.python.org/。我们从其 FAQ中剪輯一段來說明為何Python 適合程式設計的初學者:

问题:

对一个程式设计初学者而言,Python是否是一个好选择?

解答:

是的。

现在学生开始学习程式时,还是在使用传统的电脑语言,如Pascal, C, C++。但这是有点古板了,学生应该得到更好与更简单的方法来开始他们的程式探索,而Python 就提供了这些特点。它拥有非常简单及一致的文法结构,以及大量的标准程式库,最重要的是,它让学生能够将心思放在解决问题的程式设计技巧上,而不是在电脑科学的细节上。使用Python,学生可以很快地学习程式设的基本知识,如回圈(loops)、程序(procedures)。甚至在第一堂课,就可以让他们尝试使用自订的物件。

一个从来没有程式设计经验者,一开始就使用静态式电脑语言(如C等)似乎是不自然的。它让学生必须先去了解电脑复杂的内部结构,使整个课程缓慢下来。学生必须学习如何使用电脑的角度来思考、解决问题及设计界面。虽然就长期目标而言,学习静态式电脑有其必要性与利益性,但对初学者而言,大可不必将程式设计的门槛弄得那么高。

Python拥有很多优点来当作学习程式设计的首选电脑语言。就如同Java,它拥有大量的标准程式库,可以让学生在课堂上去实作一些专案,而这些专案都可以是一些实用的应用程式,而不是简单的加减乘除四则运算。同时,使用这些标准程式库可以让学生学习程式的再利用。其他丰富的第三方模组也非常好用,如PyGame,都可以直接拿来让学生使用。

Python是一个互动式的直译器,可以让学生直接边撰写程式边测试。它可以让你在一个视窗边写程式,而在另外一个视窗执行与测试这些程式码。

这个程式库的特点

这世上已经有太多的数独游戏或者学习程式,有些是为了娱乐,有些是为了数学的研究,而此专案则是专注在逻辑的学习上。而且,这里的逻辑是专门以人的角度为出发点,而不是电脑科学的角度。所以这个专案有以下特点:

  1. 让人们学习「逻辑」 的基本精神。

  2. 它「不是」要成为一般性的程式设计课程。

  3. 它「不是」要成为严谨的数学研究。

  4. 探索解数独的方法是以人的角度,而「非」电脑科学角度。

  5. 让人们自己去发现解数独的方法,并且以自己的文字来命名它。 ( 这虽然不在此Python Package 里面,但必须将此安排在课程里面。)

  6. 让人们能够学习Python 程式设计以时做学生们自己探索出来的方法。

  7. 学习以物件导向程式设计(OOP)来求解数独。 OOP方法可以比拟为人的思考与行为角度。

课程计画

我们可以安排一个暑期数独营来上这些逻辑探索课程. 我们可以预先安排六种难易度不同的数独题目. 目标不是让学习者学会所有的数独解法, 而是让他们找到适合自己程度的数独. 我们的目标不是教他们所有的解决数独的技巧, 而是教他们逻辑. 所以, 我们可以让不同的学习者, 使用不同的教材, 并且以逻辑的精神来解决它.

谁适合上这些课程呢?

每一个上过小学3-4年级课程的人都适合.

第一步: 以手写来解数独

时数:

14小时, 2小时/每天, 共 7 天

目标:
  1. 让每个学习者找到适合他程度的数独

  2. 学习电脑基础知识

第二步: 学习去发现解决数独的方法

时数:

14小时, 2小时/每天, 共 7 天

目标:
  1. 当他们发现一个模式来解决时, 用自己的语言来命名, 并且写下字句来描述此模式.

  2. 让他们将自己发现的方法阐述给他人听.

  3. 从解决他们以前家庭作业的方式来学习Python 基本程式的撰写, 如从1 加到100 等等...

第三步: 开始撰写Python 程式来实践自己发现的方法

时数:

14小时, 2小时/每天, 共 7 天

目标:
  1. 学习物件导向程式设计(OOP)的基础观念.

  2. 学习使用OOP 来模拟他们所发现的方法.

如何在Python 中制作一个解数独的模拟环境?

我们认为, 类别定义, 是在OOP程式设计中最困难的部分. 所以在这些课程中, 我们不准备详细地解释如何设计类别, 属性与方法. 我们只会解说在这个程式库中既有的类别, 属性与方法. 所以我们可以类比这些类别, 属性与方法所构成的资料结构是一个解决数独的模拟环境, 以便让学习者以人的思考模式来学习如何解开一个数独.

首先, 我们可以想像在一个美丽的山谷中, 有9x9 间方方正正的房子, 它们排列如下:

_images/p4.png

一个假想的联合王国

这里有9 个国家, 每一个国家有9 个人, 他们决定一起定居在这个美丽的山谷. 而这个山谷共有9x9 间房子. 他们决定每一排(x-way line), 每一列(y-way line ), 及每一个3x3 区块都包含了每一个国家的人. 如此, 他们才会认为他们这个团体才是一个真正的联合王国, 可以永久和平地生活在一起. 你可以帮助他们达到这个目的吗?

接着, 我们就可以开始协助这些人来促成这一个美好的世界...

什么是类别? 什么是物件?

在OOP中,类别(class)的定义是主角。它就如同人类j为了探索、沟通或者纪录等目的,会将有同样行为、特性与外表的事物归类一样。如同我们会将动物当作一类,而大象就是动物类别的一个子类别,一只大象就是同属于动物及大象这两类的一个物件。

虽然在自然上,一只大象这个物件同时隶属于大象类及动物类,但看目标的需要,也可以直接将一只大象当做动物类别的一个物件即可。所以我们可以说在OOP 所说的物件(Object)就是我们现实世界的一个实体,就如同人类若是一个类别,那你就是隶属此类别的一个唯一独立的物件。

依据我们设定的范围与需要,我们可能设计出不同的类别,而让同一个物件同时隶属于它们。如我们想要研究城市生态时,我们可能会设计一个animal 类别,而这个类别中的物件包括一些人,一些宠物等等...但当我们想要制作一个电话簿程式时,那我们就会设计一个person 类别,一些人会成为此类别的物件,但不会包括宠物,除非这些宠物也都拥有手机。

在这个专案中,我们设计的主要类别有:

  • Number Class:

    我们可以视数独游戏中每个数字是一个人。而整个数独世界中有9 个国家,每个国家有9 个人。如此,这个Number Class 就可以视为是一个国家类别。每一个国家都有一个识别代码(ID),在这里是1-9,而每个国家都会纪录它的人民住在这山谷中的位置。

  • Point Class:

    Point 物件就是一间房子。每间房子都会标注它是否已住人,如果已住人,那是哪个国家的人民;如果是空的,那可以让哪些国家的人民来申请入住?

  • GroupBase Class:

    GroupBase是一种群组类别,也就是它的物件不是一个实体,而是一群实体的组合。在这里的GroupBase 包含了三种群组,X​​ 与Y 轴方向的房子群组及区块房子群组。每个物件都将指出它包含了哪些房子,已经住了多少人,还有哪些国家的人民还没住进来?

  • Box Class:

    它是GroupBase的子类别,为区块群组。在数独世界中总共有9 个区块物件,从左到右,从上到下,被标注其区块代码如下:

    _images/p5.png
  • lineX Class:

    这是GroupBase 子类别,为X 轴的房子群组。在数独世界中共有9 个物件,从左到右被标注1-9,如下图:

    _images/p6.png
  • lineY Class:

    这是GroupBase 子类别,为Y 轴的房子群组。在数独世界中共有9 个物件,从上到下被标注1-9,如下图:

    _images/p7.png
  • Matrix Class:

    Matrix class定义了数独游戏的整个世界。它是一个美丽的山谷,包含了9 个国家的人民,每一个国家有9 个人,山谷中建造了9x9 间房子以提供给这些人民来居住,以组成一个永远和平的联合王国。

什么是属性(Property)?

属性(Property)是在类别(Class)的定义里面,类别用它来定义成员,特征及纪录状况。如在一个人的类别里面可能会包含以下一些属性: 这人拥有多少钱、他有几个小孩子、第一个小孩是男或是女、每个小孩各是几岁?

以下是这个专案中几个主要类别的主要属性定义:

  • Number Class:

    • v: 这个国家的代码, 1-9

    • p: 这个国家每一个人民所居住的房子列表

    • filled: 有多少人已经住进房子了

  • Point Class:

    • x: 这个房子的 x 轴座标

    • y: 这个房子的 y 轴座标

    • v: 这个房子居住了哪个国家的人民,如果是空的,那它的值就是 0

    • b: 这个房子隶属哪个区块

  • GroupBase Class:

    • idx: 这个群组的代码

    • p: 隶属这个群组的房子列表

    • filled: 在这个群组里面已经居住了多少人

    • possilbe: 在这个群组里面还有哪些国家还没住进来,值为这些国家的代码列表

  • Box Class:

    • 包含所有 GroupBase 的属性

    • effects: 这个区块的所有邻居区块

    • effectsX: 这个区块的 x 轴方相邻居

    • effectsY: 这个区块的 y 轴方相邻居

  • lineX Class:

    • 与 GroupBase 的属性相同

  • lineY Class:

    • 与 GroupBase 的属性相同

  • Matrix Class:

    • p: 一个二阶阵列的房子列表,从p[0][0] 到p[8][8],代表这个山谷的所有房子。

    • lineX: x 轴方向房子群组的列表

    • lineY: y 轴方向房子群组的列表

    • b: 区块房子群组的列表

    • n: 所有国家的列表

    • filled: 纪录已经有多少人入住在这个山谷了

什么是方法(Method)?

方法(methods)是一个类别及其物件的一些特定行为。举个例子,如果我们定义了一个收音机类别,它将包含一些按钮的属性,而当我们按下这些按钮时,我们就必须定义一些方法来执行这个动作。这些动作有可能是开始接收某个电台的节目、或者是录制节目到CD等等...

以下是这个专案中的类别中使用道的主要方法:

  • Number Class:

    • setit(p1): 当有人找到属于自己的房子时,就会启动这个方法

  • Point Class:

    • can_see(p1): 测试一个房子是否能够**看到**另外一个房子(p1)?

    • can_see_those(posList): 测试一个房子能否**看见** posList 所列的房子,并将所有能看见的房子列表传回。

    注解

    什么是「看见」?

    对一个房子而言,与它同一排、同一列、或者同在一个区块的其他房子,都是它能够**看见**的房子。

  • GroupBase Class:

    • allow(v): 测试一个房子群组能否让标记为v 的国家人民来居住?

    • get_num_pos(v): 在这个房子群组中,取得v 国人民居住的房子物件,如果该国尚未有人入住,则以None来回应。

    • count_num_possible(count): 在一个房子群组中,取得国家的id及可供该国人民居住的房子列表,如果有参数count,则表示要取得的可供居住的房子数要等于count 才取回。

    • get_all_pos(method): 如果method = “a”, 取得一个房子群组的所有房屋物件列表;如果method=”u”,则取得所有空房列表;如果method=”s”,则取得所有已住人的房​​子列表。

  • Box Class:

    • 所有 GroupBase 类别的方法

    • get_group_number(num): 测试这个国家代码num 在一个房子区块中能否形成一个**虚拟国民**?

    注解

    什么是「虚拟国民」?

    虚拟国民存在于一个房子区块群组中。在一个区块中尚未有人入住的房子中,如果所有可能让某个国家居住的房子在同一个方向时(无论是x 轴或y 轴),那我们就可以称这些房子形成了一个**虚拟国民**。虽然我还不晓得在这个区块中,这个国家的人民最后将居住在哪里,但我们从虚拟国民中知道,在与它同个方向的其他区域的房子,都将不允许居住这个国家的人民了。

  • lineX Class:

    • 与 GroupBase 有相同的方法

  • lineY Class:

    • 与 GroupBase 有相同的方法

  • Matrix Class:

    • get_all_pos(method): 如果method = “a”, 取得所有房屋物件列表;如果method=”u”,则取得所有空房列表;如果method=”s”,则取得所有已住人的房子列表。

    • sort_unassigned_pos_by_possibles(possibles): 取得所有的空房列表,而这些空房数必须仅能够让possibles 个国家的人入住,如果possibles == 0, 将取得全部空房。回应回来时将以可居住国家数,从小排到大。

    • can_see(p0, method=”u”, num=0): 取得能够看见某一房子(p0)的房子列表,如国num!=0,表示仅取得可让国家代码为num 者居住的房子。

    • setit(x, y, v): 让国家代码为v 的人民安住在座标为(x, y)的房子。

    • reduce(x, y, v): 当一个房子(x, y)被入住时,任何能够看见此房子的其他空房,都可用此方法来减掉已入住这个国家人民(v)的可能性。

    • allow(x, y, v): 测试这个国家(v)的人民是否能够居住于座标(x, y)的房子?

    • read(file): 从一个定义档(file)中读入最初到此山谷的国家、人民与居住位置。

数独游戏定义档案

你能够定义数独游戏的最初状况。在文字档中一行定义一个房子的座标及居住者,格式为x, y, v,如下图,此专案附有一些已定义好的数独,置于[安装的目录]/sudoku/data/ 目录里。

一个定义范例、最初的外观以及解出来的外观
m3.data

起始的数独外观

已解的数独外观

_images/m3.png _images/origin.png _images/result.png

撰写 Python 程式来解数独了

当我们已经在电脑建构好一个数独模拟世界时,对那些我们以前使用手写的方式所发现的解数独方法,我们就可以着手来将其以电脑语言(这里是Python)来重新阐述。就术语来说,称为程式设计(Programming)。所以程式设计可以视为我们在教导电脑如何去执行我们解决问题的方法。

一开始,我们先介绍这个专案整个求解数独的环境,然后我们再说明一些基本方法。

求解环境

我们设计了solve() 这个函数来做整个求解数独的入口,另外也定义了两个事件类别,SudokuDon 及SudokuError,以处理求解过程中发现已经解开数独或者产生违反数独规则的状况。

注解

什么是「例外处理(Exception)」?

「例外处理(Exceptions)」是一种事件定义,当构成这个事件的条件满足时,系统就会停止处理目前的事务,而跳到该事件的处理程序。在这个专案中有两个主要的例外处理:

SudokuDone:

当这个数独已经被解开时,这个例外处理会被叫醒

SudokuError:

当求解过程中发现违反数独规则时,这个例外处理会被叫醒

为了让整个求解环境能够知道有多少种求解方法它能够使用,我们设计了一个类别,SolveMethod。我们以这个类别来将所有的求解方法置入成一个**虚拟大脑**。我们可以将这个虚拟大脑视为是这个美丽山谷的守护神。每当有新进来此山谷者,找不到居住所时,都可以透过祂来选择出适合的房子,但祂也可能回答:「对不起,我也不知道该如何选择!」

对**虚拟大脑**而言,每一个解数独的方法都化为一个SovleMethod 物件储存在里面,这些物件有以下主要属性:

  1. fun: 解数独方法的 Python 函数名称

  2. idx: 方法的排序,从简单的方法开始到困难的方法依序排列,虚拟大脑将依序一个一个地使用这些方法来解开数独。

  3. name: 此方法的名称,使用者可以自订其名称

  4. level: 困难度,对人的直觉而言。系统用此来计数解开整个数独的困难积分。

求解过程

下面是主要求解函数,solve(),的流程图:

_images/flowchart.png

注解

**「有用(work)」或「没用(not work)」? **

一个求解方法如果是「有用」,表示它能够:

  1. 让一个或多个人找到他们的居所

  2. 或者是可让一间或多间房子知道一个或多个国家的人不能居住在他们的房子

在这张流程图中,我们知道:

  1. 当一个求解方法有用时,设定了一个人的住所或降低了一间房子可居住者的可能性时,整个虚拟大脑会重新回到第一个求解方法来继续求解。

  2. 假如一个求解方法没用时,它会交给下一个方法来解决。

  3. 当到最后一个方法都没用时,他会离开整个求解环境并说:「我无法解开这个游戏,抱歉!」

  4. 在整个求解过程中,如果发现已解开数独(Done)或者违反了数独规则(Error)时,它就会跳离整个求解环境。

一些基本求解方法

  1. fill_only_one_possible:

    在每一个房子群组中,检查每一间房子,是否只有一间房子能够让某国的人民居住,如果是,则该间房子必然要让那国的人民迁入。

  2. fill_last_position_of_group:

    当一个房子群组中只剩一间空房时,那必然是只有一个国家的人民尚未入住。

  3. check_obvious_number:

    检查每一个国家中已经定居的国民,看他们所在的位置所交互影响、而尚未有该国人民入住的区块中,是否可以找到仅剩一间房子容许该国国民来定居,如果是,则可以很明显地配给该房子给与该国的人民。

  4. check_inobvious_number:

    这个求解方法与check_obvious_number 相同,只是先找到一些区块的虚拟国民,让他们以及已定居的国民一起来探测其他尚未有本国国民居住的区块。

  1. reduce_by_group_number:

    如果在一个房子区块中形成了一个虚拟国民时, 那与这个虚拟国民同方向的房子群组中的其他空房子,就不可能居住这个国民。

  2. update_chain:

    当一些房子开始住人以后,会影响一些房子不再能够允许一些国家的居民,而这些空房子可能因此在一个房子群组中(x轴、y轴、区块)中产生了一个**键链(Chain)**。这个方法就是去寻找可能的键链,并将键链所影响的空房子减少它们被这些键链的国民居住的可能。

如何去实作一个求解方法呢?

我们来实作一个只为一个国家的人民来看是否能有一个直觉的方法来为一个尚未有该国住民的区块找到住所,我们命为check_obvious_for_a_country(m, num):

 1  def check_obvious_for_a country(m, num):
 2  checked = list()
 3  for p1 in m.n[num].p:
 4      for b in m.b[p1.b].effects:
 5          possible = []
 6          if b in checked:
 7              continue
 8          else:
 9              checked.add(b)
10          if num not in m.b[b].possible:
11              continue
12          for p2 in m.b[b].p:
13              if p2.v != 0 or p2.can_see(p1) > 0:
14                  continue;
15              if not m.lineX[p2.x].allow(num):
16                  continue
17              if not m.lineY[p2.y].allow(num):
18                  continue
19              possible.append(p2)
20          if len(possible) == 1:
21              m.setit(possible[0].x, possible[0].y, num, d="Obvious For a Country People")
  1. line#1, 定义一个求解方法, m 是这个游戏的数独世界,num 则是一个国家代码, 可能值为1-9。

  2. line#3, 将每一个已经入住此山谷的此国国民都找出他们的住所。

  3. line#4-9, 检查每一个尚未被检查的房子区块。

  4. line#10-11, 如果此区块已有该国人民居住,那就不用检查了。

  5. line#12-19, 检查此区块每一个空房是否可以让该​​国的人民来居住。如果是,则将该房子放进去可能的名单里面。

  6. line#20-21, 最后检查可能的名单中,如果只有一个时,那就表示该房子必然由该国人民来居住。

程式详解: sudoku套件

这是使用物件导向程式设计来求解数独的程式库

索引与参考资料