排列与组合是信息学奥赛必学内容,同时也是高中数学课程。但是如果要下放到初中甚至小学来教,该如何来教呢?
当我屡次遇到这样的题目,它的基础是排列与组合时,我就思考这个问题。希望学生能更容易的理解它的编程方式。即
如何生成所有的排列或者组合。比如从5个数里面选3个数的所有排列或组合。这个程序该如何编写?我这里用放棋子的
方式来进行讲解。 为了更好理解,先需要具体化一下,也就是举一个例子。在本教程中,我们就以在5个数里面选3个数
的所有排列和组合为例。5个数分别就是[0,1,2,3,4]。从这5个数里取3个数的所有排列是:
[0, 1, 2] [0, 1, 3] [0, 1, 4] [0, 2, 1] [0, 2, 3] [0, 2, 4] [0, 3, 1] [0, 3, 2] [0, 3, 4] [0, 4, 1] [0,
4, 2] [0, 4, 3] [1, 0, 2] [1, 0, 3] [1, 0, 4] [1, 2, 0] [1, 2, 3] [1, 2, 4] [1, 3, 0] [1, 3, 2] [1, 3, 4]
[1, 4, 0] [1, 4, 2] [1, 4, 3] [2, 0, 1] [2, 0, 3] [2, 0, 4] [2, 1, 0] [2, 1, 3] [2, 1, 4] [2, 3, 0] [2,
3, 1] [2, 3, 4] [2, 4, 0] [2, 4, 1] [2, 4, 3] [3, 0, 1] [3, 0, 2] [3, 0, 4] [3, 1, 0] [3, 1, 2] [3, 1, 4]
[3, 2, 0] [3, 2, 1] [3, 2, 4] [3, 4, 0] [3, 4, 1] [3, 4, 2] [4, 0, 1] [4, 0, 2] [4, 0, 3] [4, 1, 0] [4,
1, 2] [4, 1, 3] [4, 2, 0] [4, 2, 1] [4, 2, 3] [4, 3, 0] [4, 3, 1] [4, 3, 2]。共有P(5,3)=60种排列。
我们把求所有排列的过程转换成放棋子的过程。规则为每行每列只放一个,所有棋子的列号不能重复,也就是说在同一列只能有一个棋子。比如对于[0, 1, 2]这个排列,放好了棋子,对应的棋局就是下在这个样子:

通常,我们按行优先来放棋子,也就是说先放第一行的棋子,再放第二行的棋子,依次类推。所以我们就定义一个叫place的函数。它的含义是在当前行放一枚棋子,所以它的参数就是行号,我们用row来作变量名。定义好了这个函数后,那么place(0)就是在索引为0的行放一枚棋子的意思了。由于在一行放棋子的话,在本例中最多都有5种选择,所以place函数中最主要的代码是有一个for循环,用于枚举每一列。如果这一列没有摆放过棋子,那么就能放,则在这里放!然后放下一行的棋子。放一行我们只要递归调用place(row+1)即可。如果下一行的都放完了,那么就要尝试当前行的下一个列。如果这一列没有摆放过棋子,则又像上面这样放棋子,依此继续重复。
在上面,如何记录某个列是否有棋子,而且棋子的行列号到底存储在哪里呢?这里就涉及到数据结构的设计了。我们设一个叫used的列表来记录是否某列是否已被占用。由于共有5列所以它的初始化代码是:used = [False for i in range(5)]。used列表中全初始化为False,表示每一列都没有被占用。我们用一个叫chess的列表来存储棋子的行列号。由于只选3个数,所以它的初始化代码是:chess = [None for i in range(3)]。chess列表中3个值都为None,表示列号未定! 在下面的代码中,我们看到place是一个递归函数。所以必然要有递归结束条件。在本例中,只放三行,也就是放0和1及2行,所以到了row为3的时候必然是已经放完了一种棋局,所以输出chess即可。代码如下所示:
def place(row): # 在第row行放一枚棋子(以0开始)
if row==3:
print(chess,end=' ')
else:
for col in range(5):
# 如果col列已经放了棋子,必然会标记为True
if used[col]==True:continue
used[col] = True # 标记col列已经被占
chess[row] = col # 放棋子在row,col位置
place(row+1) # 放下一行
chess[row]=None # 取消放棋子
used[col]=False # 取消标记
used = [False for i in range(5)]
chess = [None for i in range(3)]
place(0)
以上代码的C++代码如下所示:
#include
#include
using namespace std;
vector used(5,false);
vector chess(3,-1);
void place(int row){
if(row==3)
{
for(int coll:chess)cout << coll << ' ';
cout << endl;
return;
}
for(int col=0;col<5;col++){
if(used[col]==true)continue;
used[col]=true; //记录在col列已放一枚棋子
chess[row]=col; //保存 (row,col)
place(row+1); //在下一行放置棋子
chess[row]=-1; // -1表示在row行没有放棋子
used[col]=false; //取消记录
}
}
int main(){
place(0);
return 0;
}
那么,如何列出从5个里面选3个数的所有组合呢?我们只要稍微把上面的代码修改一下即可。对于排列[0,1,2]和[0,2,1]。它们都同一个组合,所以本质是去除重复。我们只要规定后面的数大于前面,那就去掉了重复。所以只要把上面的代码加一个判定,即如果放当前行棋子是,需要比较是否比上一行的列号大,如果大才会,否则不能放。代码如下所示:
def place(row): # 在第row行放一枚棋子(以0开始)
if row==3:
print(chess,end=' ')
else:
for col in range(5):
# 如果新放的列号比上一行已经放的棋子的列号相等或者小,不行
if row>0 and col<=chess[row-1]:continue
# 如果col列已经放了棋子,必然会标记为True
if used[col]==True:continue
used[col] = True # 标记col列已经被占
chess[row] = col # 放棋子在row,col位置
place(row+1) # 放下一行
chess[row]=None # 取消放棋子
used[col]=False # 取消标记
used = [False for i in range(5)]
chess = [None for i in range(3)]
place(0)
上面代码的C++代码就没必要列出来了,请读者自行“解决”。 本文章亦放在了本人的微信公众号上,文章网址:https://mp.weixin.qq.com/s/pZn_e_uwZLJaWTpupqafIQ


