D a t e : 2022 − 10 − 04 \color{FFCC99}{Date:2022-10-04} Date:2022−10−04
E v e r y o n e \color{FFCC99}{Everyone} Everyone h a s \color{FFCC99}{has} has t o \color{FFCC99}{to} to g r o w \color{FFCC99}{grow} grow u p , \color{FFCC99}{up,} up, b u t \color{FFCC99}{but} but n o t \color{FFCC99}{not} not e v e r y o n e \color{FFCC99}{everyone} everyone u n d e r s t a n d \color{FFCC99}{understand} understand g r e w \color{FFCC99}{grew} grew u p ! \color{FFCC99}{up!} up!
🍕1. 纸牌三角形🍕
🍔题目🍔
A,2,3,4,5,6,7,8,9 共9张纸牌排成一个正三角形(A按1计算)。要求每个边的和相等。如果考虑旋转、镜像后相同的算同一种,一共有多少种不同的排法呢?三角形如下
A
2 3
4 5
6 7 8 9
🍔思路🍔
本题是典型的全排列算法
题目,通过对9个数字进行全排列,就可以计算出所有的三角形种类
什么是全排列?
全排列算法是一种经典的递归算法,如{1,2,3}的全排列为{1, 2, 3}, {1, 3, 2}, {2, 1, 3}, {2, 3, 1}, {3, 1, 2}, {3, 2, 1},一共6钟排列结果。全排列就是将全部可能出现的排序都列出来。而每一个数字能出现在任一个位置,如果不固定的话,那排序起来就会十分混乱,排序结果也不好记录,所以递归求解的思路是每一次全排列都固定一个元素,然后求剩下元素的全排列,直到就剩一个元素的全排列时结束本次全排列,而且每一次全排列完成后,都应先将数字排序恢复到本次全排列开始前的状态,以保证下一次的全排列都是基于初始状态进行的。
举个例子
对{1, 2, 3}进行全排列,下面按照上述全排列递归算法的思路实际模拟一遍
(每一次递归的第一步都是先判断当前遍历的是不是最后一个元素,如果是则说明全排列完成一次)
- 第
1
次递归调用,从头开始遍历,此时选择当前第一个元素1
为固定元素,并将固定元素1
与当前遍历的元素1
进行换位,剩余元素为{2, 3},此时排列为{1, 2, 3}; - 第
2
次递归调用(基于第1
次):从剩余元素开始遍历,此时选择当前第一个元素2
为固定元素,并将固定元素2
与当前遍历的元素2
进行换位,剩余元素为{3},此时排列为{1, 2, 3}; - 第
3
次递归调用(基于第2
次):从剩余元素开始遍历,此时选择当前第一个元素3
为固定元素,并将固定元素3
与当前遍历的元素3
进行换位,剩余元素为空,此时排列为{1, 2, 3}; - 第
4
次递归调用(基于第3
次):因为第4
次递归调用时已经选择到了最后一个元素,遍历结束,取得第一个全排列结果{1, 2, 3}
,第4
次递归结束; - 因为第
3
次递归调用已经遍历结束,将本次递归调用开始前进行的换位元素返回原位置(3 <-> 3),得到开始前的排列{1, 2, 3}; - 第
5
次递归调用(基于第2
次):第2
次递归往下遍历,将固定元素2
与遍历元素3
进行换位,剩余元素为空,此时排列为{1, 3, 2}; - 第
6
次递归调用(基于第5
次):因为第6
次递归调用时已经选择到了最后一个元素,遍历结束,取得第二个全排列结果{1, 3, 2}
, 第6
次递归结束; - 因为第
5
次递归调用已经遍历结束,将本次递归调用开始前进行的换位元素返回原位置(3 <-> 2),得到开始前的排列{1, 2, 3}; - 因为第
2
次递归调用已经遍历结束,所以将本次递归调用开始前进行的换位元素返回原位置(2 <-> 2),得到开始前的排列{1, 2, 3}; - 第
7
次递归调用(基于第1
次):第1
次递归往下遍历,并将固定元素1
与遍历元素2
进行换位,此时排列为{2, 1, 3}; - 第
8
次递归调用(基于第7
次):此时选择剩余元素的第一个元素3
为固定元素,并将固定元素3
与当前遍历的元素3
进行换位,剩余元素为空,此时排列为{2, 1, 3}; - 第
9
次递归调用(基于第8
次):因为第9
次递归调用时已经选择到了最后一个元素,遍历结束,取得第三个全排列结果{2, 1, 3}
,第9
次递归结束; - 因为第
8
次递归调用已经遍历结束,将本次递归调用开始前进行的换位元素返回原位置(3 <-> 3),得到开始前的排列{2, 1, 3}; - 第
10
次递归调用(基于第7
次):第7
次递归往下遍历,将固定元素1
与遍历元素3
进行换位,剩余元素为空,此时排列为{2, 3, 1}; - 第
11
次递归调用(基于第10
次):因为第11
次递归调用时已经选择到了最后一个元素,遍历结束,取得遍历结束,取得第四个全排列结果{2, 3, 1}
, 第11
次递归结束; - 因为第
10
次递归调用已经遍历结束,将本次递归调用开始前进行的换位元素返回原位置(1 <-> 3),得到开始前的排列{2, 1, 3}; - 因为第
7
次递归调用已经遍历结束,将本次递归调用开始前进行的换位元素返回原位置(2 <-> 1),得到开始前的排列{1, 2, 3}; - 第
12
次递归调用(基于第1
次):第1
次递归往下遍历,并将固定元素1
与遍历元素3
进行换位,剩余元素为{1},此时排列为{3, 2, 1}; - 第
13
次递归调用(基于第12
次):此时选择剩余元素第一个元素1
为固定元素,与遍历元素1
进行换位,剩余元素为空,此时排列为{3, 2, 1}; - 第
14
次递归调用(基于第13
次):因为第14
次递归调用时已经选择到了最后一个元素,遍历结束,取得遍历结束,取得第五个全排列结果{3, 2, 1}
, 第14
次递归结束; - 因为第
13
次递归调用已经遍历结束,将本次递归调用开始前进行的换位元素返回原位置(1 <-> 1),得到开始前的排列{3, 2, 1}; - 第
15
次递归调用(基于第12
次):第12
次递归往下遍历,将固定元素1
与遍历元素2
进行换位,此时排列为{3, 1, 2}; - 第
16
次递归调用(基于第15
次):因为第16
次递归调用时已经选择到了最后一个元素,遍历结束,取得遍历结束,取得第六个全排列结果{3, 1, 2}
, 第16
次递归结束; - 因为第
12
次递归调用已经遍历结束,将本次递归调用开始前进行的换位元素返回原位置(1 <-> 3),得到开始前的排列{1, 2, 3}; - 因为第
1
次递归调用已经遍历结束,将本次递归调用开始前进行的换位元素返回原位置(1 <-> 1),得到开始前的排列{1, 2, 3};
递归算法处理
- 确定递归函数的参数和返回值
参数:这里要求的是全排列的排列个数,所以参数一定有元素数组;要对整个数组进行遍历,那么就少不了下标和范围,所以这里的参数就是{数组、起始下标、数组长度
}
返回值:这里不需要返回值,全排列个数通过全局变量进行累加计算,若放在函数中,则每次都会被重置
void func(vector<int>& arr, int start, int len);
- 确定终止条件
上面一开始就说到了终止条件:当遍历到最后一个元素时,即说明完成一个全排列
所以终止条件就是当下标值 >= 数组长度
if (start >= len)
- 确定单层递归的逻辑
由上面的模拟可以看到,每一次递归都是新的遍历,所以有一个最外围的循环遍历逻辑,而每次递归前后都有换位操作,所以最终确定如下单层逻辑
for ()
{
swap();
func();
swap();
}
🍔代码🍔
void func(vector<int>& arr, int start, int len)
{
if (start >= len)
{
count++;
return;
}
for (i = start; i < len; i++)
{
swap();
func(arr, i + 1, len);
swap();
}
}
以上是我对全排列算法的粗糙理解,但是本题还有一个关键点:考虑旋转、镜像后相同的算同一种
因为三角形有三个角,也就是说每个数字都能在三个角上出现一次,所以旋转后相同算同一种的话,就需要将全排列总数 / 3;
镜像对CV战士来说好理解,就是2分一模一样的数据,所以镜像后算同一种的话,就需要将全排列总数 / 2;
最终计算结果就是 全排列总数 / 3 / 2,即总数 / 6;
🍔总结🍔
递归算法一般都很难理顺,自己一开始自己按原理推理的时候也一直弄乱,记不清当前递归的条件。本题的关键就在于全排列算法和确定旋转/镜像的关系