利用编程解决一道数学题
问题
在朋友QQ空间看到一道题,如下:
当时顺手画了条数轴,在数轴上标出了各个算式的解的特点:和为7的算式的解关于4对称,和为9的解关于5对称等等。草草算了下,发现很难同时满足5个条件。而细算又太麻烦,算了,反正是程序员,写程序求解吧。
利用笛卡尔积求解
因为是4个算式,很自然的就想到穷举法。将每个算式可能的结果放在一起算笛卡尔积,如果有解的话,则必然存在一个笛卡尔积里面存在1到8这8个不同的元素。
计算笛卡尔积的代码如下:
/// <summary> /// 计算元素集列表的笛卡尔积 /// </summary> /// <typeparam name="T">元素具体类型</typeparam> /// <param name="sets">元素集列表</param> /// <returns>笛卡尔积列表</returns> public static T[][] CalculateCartesianProduct<T>(this IList<IList<T>> sets) { // 笛卡尔积总数 int total = 1; // 元素集个数 int setCount = 0; foreach (var set in sets) { total *= set.Count; setCount++; } // 笛卡尔积列表 var products = new T[total][]; // 除数列表,用于计算每个笛卡尔积的情况 var dividers = new int[setCount]; int divider = 1; // 倒序循环,因为后面的多种情况对应前面的一种情况 for (int counter = setCount - 1; counter >= 0; counter--) { dividers[counter] = divider; divider *= sets[counter].Count; } // 计算笛卡尔积,共有permutationNumber个笛卡尔积, // 从0到permutationNumber中,每个数字对应一种笛卡尔积情况 for (int counter = 0; counter < total; counter++) { // 一个笛卡尔积的情况 var permutation = new T[setCount]; int index = 0; foreach (var set in sets) { // 将数字映射到元素集中的位置 var pos = (counter / dividers[index]) % set.Count; permutation[index] = set[pos]; index++; } products[counter] = permutation; } return products; }
原理是将笛卡尔积解的总个数里的每个数字映射到一个解。
比如前面几个笛卡尔积如下:
[2, 1] [3, 1] [1, 6] [1, 8]
[2, 1] [3, 1] [1, 6] [2, 7]
[2, 1] [3, 1] [1, 6] [3, 6]
[2, 1] [3, 1] [1, 6] [4, 5]
[2, 1] [3, 1] [2, 5] [1, 8]
[2, 1] [3, 1] [2, 5] [2, 7]
[2, 1] [3, 1] [2, 5] [3, 6]
[2, 1] [3, 1] [2, 5] [4, 5]
全排列
后面我又想到,实际上计算全排列也可以解决此题。将1到8取全排列,如果此题有解的话,则必然存在一个全排列,使得每两个元素为一对,依次满足4个算式。
简单粗暴的解法
代码如下:
/// <summary> /// 计算指定元素集的全排列 /// </summary> /// <typeparam name="T">元素的具体类型</typeparam> /// <param name="collection">元素集</param> /// <returns>全排列</returns> public static IEnumerable<T[]> CalculatePermutationCrude<T>(ICollection<T> collection) { // 元素个数 int elementCount = collection.Count(); // 全排列列表 var permutations = new List<T[]>(); // 一个全排列 var permutation = new List<T>(); CalculatePermutationCrude(permutations, permutation, collection, elementCount); return permutations; } /// <summary> /// 计算指定元素集的全排列 /// </summary> /// <typeparam name="T">元素的具体类型</typeparam> /// <param name="permutations">全排列列表</param> /// <param name="permutation">一个全排列</param> /// <param name="collection">元素集</param> /// <param name="setLength">集合长度</param> private static void CalculatePermutationCrude<T>( List<T[]> permutations, List<T> permutation, ICollection<T> collection, int setLength) { if (permutation.Count < setLength) { foreach (var item in collection) { // 不是可重排列,不能包含 if (!permutation.Contains(item)) { var temp = permutation.ToList(); temp.Add(item); if (temp.Count == setLength) { // 达到最大计算深度,表示完成一个全排列,添加 permutations.Add(temp.ToArray()); } else { CalculatePermutationCrude(permutations, temp, collection, setLength); } } } } }
原理是有多少个元素,就循环多少次,将包含重复元素的排列剔除掉,自然就是集合的全排列了。
深度优先遍历解法
代码如下:
/// <summary> /// 计算指定元素集的全排列 /// </summary> /// <typeparam name="T">元素的具体类型</typeparam> /// <param name="set">元素集</param> /// <returns>全排列</returns> public static IEnumerable<T[]> CalculatePermutationDFS<T>(IEnumerable<T> set) { var permutations = new List<T[]>(); var path = new List<T>(); bool[] visited = new bool[set.Count()]; CalculatePermutationDFS(set, permutations, path, visited); return permutations; } /// <summary> /// 计算指定元素集的全排列 /// </summary> /// <typeparam name="T">元素的具体类型</typeparam> /// <param name="set">元素集</param> /// <param name="permutations">全排列</param> /// <param name="path">排列的元素列表</param> /// <param name="visited">元素访问与否的数组</param> private static void CalculatePermutationDFS<T>( IEnumerable<T> set, List<T[]> permutations, List<T> path, bool[] visited) { if (path.Count == set.Count()) { permutations.Add(path.ToArray()); } for (int i = 0; i < set.Count(); i++) { if (!visited[i]) { path.Add(set.ElementAt(i)); visited[i] = true; CalculatePermutationDFS(set, permutations, path, visited); path.RemoveAt(path.Count - 1); visited[i] = false; } } }
在代码中使用了一个辅助列表保存遍历过的元素,一个辅助数组保存元素的访问情况,在深度遍历前设置相关信息,遍历完成后重置相关信息。
非递归解法
上面的两种解法使用了递归,众所周知,全排列的个数为待排列个数的阶乘。而在数据量较大时,阶乘比指数爆炸更可怕。如果不使用自建堆栈,将递归化为迭代,有没有其他办法计算全排列,就像前面计算笛卡尔积一样,将每个数字映射到一个解。经过多次试错,总算被我想到了一个办法。
将每一个全排列对应一个数,以1234为例,如下图:
将每个序号(从0开始)对应到一个数。对应数的有如下规律:
- 位数总是比全排列的元素个数少1。
- 设从右到左,每个数位对应的权值依次递增,最右边个数权植为1。则对应数的每位上的数值为序号除以位数权值的阶乘然后对其位数的权值加1取余(真是太绕了)。注:在程序运算中转型会直接向下取整。以23为例:第1位(23/1!)%2=23%2=1;第2位(23/2!)%3=11%3=2,第3位(23/3!)%4=3再以13为例,第1位(13/1!)%(2)=13%1=1;第2位(13/2!)%3=6%3=0,第三位(13/3!)%4=2。
- 从对应数到全排列,只需要从左到右,依次在元素集中取出剩余数中从小到大的第对应数数位位上的数(从0开始计数,同样还是很绕),最后还将剩下的数取出即可。示例如下:
终于可以上代码了:
/// <summary> /// 计算指定元素集的全排列 /// </summary> /// <typeparam name="T">元素的具体类型</typeparam> /// <param name="collection">元素集</param> /// <returns>全排列</returns> public static IEnumerable<T[]> CalculatePermutation<T>(IList<T> collection) { // 全排列总数 int total = 1; // 元素个数 int elementCount = collection.Count; // 除数列表,用于计算每个全排列的情况 var dividers = new int[elementCount - 1]; for (int i = 2; i <= elementCount; i++) { dividers[i - 2] = total; total *= i; } // 全排列列表 var permutations = new T[total][]; // 计算全排列,共有permutationNumber个全排列, // 从0到permutationNumber中,每个数字对应一种全排列情况 for (int counter = 0; counter < total; counter++) { // 一个全排列的情况 var permutation = new T[elementCount]; // 指示数组,指示全排列对应元素集中的位置 var indicates = new int[elementCount - 1]; // 使用数组,指示元素集中对应的元素是否已使用 var usedFlags = new bool[elementCount]; for (int j = 0; j < elementCount - 1; j++) { // 从右向左计算 indicates[elementCount - 2 - j] = (counter / dividers[j]) % (j + 2); } // 全排列的索引 int index = 0; foreach (var indicate in indicates) { int pos = 0; // 统计未使用的元素 int unusedCount = -1; for (; pos < elementCount; pos++) { if (!usedFlags[pos]) { unusedCount++; } // 找到了要放入的元素位置 if (unusedCount >= indicate) { break; } } permutation[index] = collection[pos]; usedFlags[pos] = true; index++; } // 将最后剩余的元素放入全排列 for (int j = 0; j < elementCount; j++) { if (!usedFlags[j]) { permutation[elementCount - 1] = collection[j]; break; } } permutations[counter] = permutation; } return permutations; }
答案
经过一番劳累,结局是悲伤的,答案就是没有答案。可能有的朋友会说特殊运算,由于题目的限制,幂跟对数是不可能了,唯一能够使用的特殊运算就只有平方,在这种情况下,依然没有答案。当然,如果你硬要说立方也是特殊运算,那么还是有解的。
此刻我的内心是崩溃的