前言
昨天去面试,遇到一个笔试题。题目大致意思是N男的,N个女的,他们是N对夫妻。他们述说自己或别人是否为夫妻关系,而这些叙述都是假的。求这N对男女的正确夫妻关系。
看到题目后马上想到了解题思路,生成1到N的全排列,每一个排列为一个长度为N的数组A。如果A[i]==k,则表示第i个男人与第k个女人为夫妻关系。如果这些男女的叙说与数组A中的相应值都不匹配,这数组A是一个可行解。
解题思路有了,结果在生成1到N的全排列时掉链子了,想了一下没想出来,感觉一下子也想不出来。然后也想了一个稍笨的办法,但感觉代码亮会更大些,试卷空间可能也不够,最终题目没做了。感觉这类题属于基础题,没做出来非常不舒服,非常耻辱。回来后重新做了一下。
解决思路
问题化简思路
问题的一个重要化简思路为:1到N的全排列 == 1到N-1的全排列中每个排列Pi进行以下操作的结果集合的并集:
在Pi的最后一位(N-1位)之后插入数字N;在Pi的N-2位之后插入数字N;... 在Pi的1位之后插入数字N, 在Pi的1位之前插入数字N
遍历全排列
想到了问题化简思路继续想:笔试时主要是想生成全排列,当时想错了,受男女两类人影响把全排列数目想成了A(N, 2),已经感生成全排列太占空间。现在再一想,全排列数目有 N! 个,很大的数目.而解决问题不需要生成全排列,只需要遍历全排列就好了。并且,若能遍历全排列,在此基础上生成全排列也是轻而易举的事情。
问题转变为遍历全排列。
具体解决过程
(1)要遍历全排列,要节省空间考虑只有一个数组A。
(2)得到1到n-1的第一个排列项之后,把A[n]设n,然后依次与A[1]、A[2]...A[n-1]交换得到1到n的全排列中的一项。每一个1到n-1的排列项可以得到n个1到n的排列项。(3)为防止混乱,每次交换A[n]与A[i]后,都要交换回去。然后在进行下一次的A[n]与A[i+1]交换。(4)设计遍历函数nextN,每调用一次讲数组A设置为之前没有设置过的排列项,并返回true,表示获取一个排列项成功。遍历完成后(所有可能都设置过之后),函数返回false,表示没有其他排列项了,遍历完成。(5.1)nextN函数在执行(2)时,需要一个变量i,记录当前交换到第几个了。(5.2)nextN函数在执行(2)生成1到n的一个排列项时,需要获得一个1到n-1的排列项。采用递归调用nextN实现传入参数n表示得到1到n排列项的下一个,传入参数n-1表示得到1到n-1排列项的下一个。(5.3)nextN函数的递归有n+1层,最外层传入参数n, 最底层传入参数0直接返回false;(5.4)在(5.1)中不但最外层的nextN需要变量i记录交换到第几个,除了最底层的每一层递归函数都要有一个变量i进行记录,各层的变量i相互独立。由于调用的是同一个函数,不能采用内部的static变量,考虑在函数外定义一个iN[n]数字专门记录,iN[n]为最外层i。最后的函数实现
写完之后感觉代码好短?。
bool nextN(int n, int iN[], int a[]){ if(n == 0){ return false; } int &i = iN[n]; if(i > n){ if(nextN(n-1, iN, a)){ i = 1; }else{ return false; } } if(i != 1){ a[i-1] = a[n]; } a[n] = a[i]; a[i] = n; i++; return true;}
iN[0]和a[0]没有使用。
调用前为iN数组和a数组分别分配长度 > n+1的空间,并将iN的第iN[1]到iN[n]初始化为 > n的值。int main(int argc, const char * argv[]) { int a[6 + 1] ; int iN[6 + 1]= {0, 100, 100, 100, 100, 100, 100}; while(nextN(3, iN, a)){ std::cout << a[1] << ", " << a[2] << ", " << a[3] << ", " << a[4] << ", " << a[5] << ", " << a[6] << std::endl; } return 0;}
两个问题
(1)思考问题化简时,由于更直观,想到的是在1到n-1的一个排列中各个位置插入n,获取n个1到n的排列。程序实现时,由于交换笔插上需要移动的数据更少,速度更快,想到和使用的是A[n],与前面各A[i]的交换。
两种处理方式是不同,得到的n个排列也不同。
但交换方式最终得到的结果是正确的,交换方式和插入方式得到的排列数的数目是相同的。两个不同的1到n-1的排列数,各自利用交换方式得到任意两个1到n的排列数,这两个排列数一定不相同。因此用交换方式得到的所有排列数一定没有重复。因此将所有可能的排列数都会遍历一遍,得到正确结果。
(2)将插入方式改成交换方式能够得到正确结果,那具体解决过程中第(3)步的恢复现场可以省略吗?实验显示,不可以。