Table of Contents
前言
今天做了一个题目《素数伴侣》,具体如下:
- 输入:偶数个正整数,如,1,2,3,4
- 处理过程:将偶数个正整数进行配对处理,如a: (1,2)(3,4); b: (1,3)(2,4); c: (1,4)(2,3),配对的整数进行求和,a: 3,7; b: 4,6; c: 5,5; 和为素数的配对数称为素数伴侣,如 a组合中的(1,2), (3,4); c组合中的(1,4), (2,3).
- 输出: 可能的组合中出现的最大的素数伴侣的个数,上例为:2
看到这个题目时候,最初想到的就是用递归的方法进行求解,但是本人对于递归的认知,基本是停留在,求阶乘,或是一些很简单就能够实现的问题。
于是经过反复的思考,终于将问题进行解决了,在此就对解决过程中的收获进行详细的记录,希望对看者能有所帮助。
递归简介
递归,从字面上理解就是层层递进,最终回归到远点的意思。用梦中梦来进行类比,层层递进,就是:“你梦到梦中的你做梦,梦中的你也梦中的自己做梦”;回归到原点就是:“梦中的你的梦中的你,醒来了;梦中的你醒来了;最后你醒来了”, 当然这只是一个类比,假如有谁能够清晰的感受到逐一醒来的过程,我将深表佩服。 用一个函数来进行表示,就类似于:
Function dream(you): If (you don't dream you dreaming): Return dream(you in you dream) break()
用另一种方式进行理解:
dream(你) dream(梦中的你) dream(梦中的你的梦中的你) ....; 这个过程直到下一个梦中的你没有梦到自己做梦为止 break break break
我最初接触递归的时候,看到的第一个例子是求阶乘(想必大家也看到的第一个例子也差不多一样):
Function factorial(n): If (n == 1): Return 1 Return n*factiorial(n-1)
当n为3时,
factorial(3) factorial(2) factorial(1) return 1 return 2*1 return 3*2
利用数学知识深入对递归的认知
阶乘可以用一些的公式进行表示:
egin{equation} egin{cases} & f(n) = n imes f(n-1) ext{ if } n > 1 \ & f(n) = 1 ext{ if } n = 1 end{cases} end{equation}
从公式中很容易就能够看出递归实现的四条基本(引用自《数据结构与算法分析 - c语言描述)法则中的两条:
- 基准情形。必须总有某些基准情形,它无需递归就能解出。如,n = 1时的情况
- 不断推进。对于那些需要递归求解的情形,每一次递归的调用都必须要使求解状况朝接近基准情形的方向推进。也就是公式中,n > 1的情况
另外两条是:
- 设计法则。假定所有的递归调用都能够运行。
- 合成效益法则。在求解一个问题的同一实例时,切勿在不同的递归中做重复性工作。
注意,本文的介绍主要是基于前两条以及最后一条法则的。
那么如何利用数学公式的形式来帮助理解前言中提到的问题呢?
egin{equation} egin{cases} & input = left{ 1, 2, 3, 4 ight} \ & countMaxPrimerPairs(input) = testPairs(input.get(i,j)) + countMaxPrimerPairs(input.delete(i, j)) ext{ where } 0 le i le input.size(), i+1 le j le input.size() \ & countMaxPrimerPairs(input) = 0 ext{ if } input.size() = 0 end{cases} end{equation}
注意,其实这个数学公式模型是存在问题的,在下文中将会给出问题的所在。
“求解最大素数伴侣数量”的实现
这里通过java实现。
从上面的公式中,我们可以发现,要实现整个求解过程,递归的书写已经不再是重点中的重点了,很容易可以通过直接翻译得到如下的代码:
static public int countMaxPrimerPairs(Input input) { if (input.size() == 0) { return 0; } int nPairs = 0; int record = 0; for (int i = 0; i < input.size(); i++) { for (int j = i+1; j < input.size(); j++) { record = testPairs(input.get(i,j)) + countMaxPrimerPairs(input.delete(i,j)); if (nPairs < record) { nPairs = record; } } } return nPairs; }
因此这里的重点是,如何实现Input结构,它应该具有的成员变量和方法有:
class Input { private ArrayList<Integer> arrayList; public Input(); public void add(int a); public Pair get(int i, int j); public int size(); public Input delete(int i, int j); }
这里选用了ArrayList对数据进行存储,具体的实现如下:
/** * 记录输入的参数信息 * @author Zz * */ class Input { private ArrayList<Integer> arrayList; public Input() { arrayList = new ArrayList<>(); } public Input(ArrayList<Integer> list) { arrayList = new ArrayList<>(list); } /** * 添加元素 * @param a */ public void add(int a) { arrayList.add(new Integer(a)); } /** * 获得相应位置的元素 * @param i 第i个位置 * @param j 第j个位置 * @return Pair */ public Pair get(int i, int j) { return new Pair(arrayList.get(i).intValue(), arrayList.get(j).intValue()); } public int size() { return arrayList.size(); } public Input delete(int i, int j) { @SuppressWarnings("unchecked") ArrayList<Integer> list = (ArrayList<Integer>) arrayList.clone(); list.remove(i); list.remove(j-1); // 第一个移除,使得所有的元素都向前移动了一个位置 Input input = new Input(list); return input; } }
完整的代码如下:
import java.util.ArrayList; public class Main { // 计算最大素数伴侣对数 static public int countMaxPrimerPairs(Input input) { if (input.size() == 0) { return 0; } int nPairs = 0; int record = 0; for (int i = 0; i < input.size(); i++) { for (int j = i+1; j < input.size(); j++) { record = testPairs(input.get(i,j)) + countMaxPrimerPairs(input.delete(i,j)); if (nPairs < record) { nPairs = record; } } } return nPairs; } // 测试一个数据对是否是素数伴侣 static public int testPairs(Pair p) { int n = p.x + p.y; System.out.println(p); if (isPrimer(n)) { return 1; } return 0; } // 判断一个数是否是质数 static private boolean isPrimer(int n) { if (n==2 || n==3 || n==5) return true; if (n%2 == 0) return false; int sqrtV = (int) Math.sqrt(n); for (int i=3; i≤sqrtV; i=i+2) { if (n%i == 0) { return false; } } return true; } public static void main(String[] args) { // TODO Auto-generated method stub Input input = new Input(); input.add(2); input.add(3); input.add(4); input.add(5); int n = countMaxPrimerPairs(input); System.out.println(n); } } /** * 记录输入的参数信息 * @author Zz * */ class Input { private ArrayList<Integer> arrayList; public Input() { arrayList = new ArrayList<>(); } public Input(ArrayList<Integer> list) { arrayList = new ArrayList<>(list); } /** * 添加元素 * @param a */ public void add(int a) { arrayList.add(new Integer(a)); } /** * 获得相应位置的元素 * @param i 第i个位置 * @param j 第j个位置 * @return Pair */ public Pair get(int i, int j) { return new Pair(arrayList.get(i).intValue(), arrayList.get(j).intValue()); } public int size() { return arrayList.size(); } public Input delete(int i, int j) { // 使用浅复制,具体数值在内存中只存在一份 @SuppressWarnings("unchecked") ArrayList<Integer> list = (ArrayList<Integer>) arrayList.clone(); list.remove(i); list.remove(j-1); // 第一个移除,使得所有的元素都向前移动了一个位置 Input input = new Input(list); return input; } } /** * 用来记录配对点的信息 * @author Zz * */ class Pair { public final int x; public final int y; public Pair(int a, int b) { x = a; y = b; } public String toString() { return "(" + x + "," + y + ")"; } }
此时输出为:
(2,3) (4,5) (2,4) (3,5) (2,5) (3,4) (3,4) (2,5) (3,5) (2,4) (4,5) (2,3) 2
进一步改进
输出的内容中,显示了测试过的数据对,以及最大的素数伴侣对。为了层次性的显示测试过的数据对,将countMaxPrimerPairs方法添加了层次参数,如下:
/** * 计算最大素数伴侣对数 * @param input * @param level 标记递归中具体的层次 * @return */ static public int countMaxPrimerPairs(Input input, int level) { if (input.size() == 0) { return 0; } int nPairs = 0; int record = 0; for (int i = 0; i < input.size(); i++) { for (int j = i+1; j < input.size(); j++) { for (int k=0; k<level; ++k) System.out.print(" "); record = testPairs(input.get(i,j)) + countMaxPrimerPairs(input.delete(i,j), level+1); if (nPairs < record) { nPairs = record; } } } for (int k=0; k<level; ++k) { System.out.print(" "); } System.out.println("return " + nPairs); return nPairs; }
此时输出的结果为:
(2,3) (4,5) return 0 (2,4) (3,5) return 0 (2,5) (3,4) return 1 (3,4) (2,5) return 1 (3,5) (2,4) return 0 (4,5) (2,3) return 1 return 2 2
我们从输出的结果中发现如下问题:
- 相同的数据对被重复测试了两次
- 当找到最大的测试对为input.size/2的时候仍然继续递归过程
减少测试的重复性
对第一个问题的解决可以采用Hashmap对进行过的测试进行记录,当调用到已经测试过的,直接利用记录中的结果,增加与更改后的代码如下:
public class Main { private static Hashtable<String, Integer> table; static public void initTable() { table = new Hashtable<String, Integer>(); } /** * 计算最大素数伴侣对数 * @param input * @param level 标记递归中具体的层次 * @return */ static public int countMaxPrimerPairs(Input input, int level) { if (input.size() == 0) { return 0; } int nPairs = 0; int record = 0; Pair pair; int testResult; for (int i = 0; i < input.size(); i++) { for (int j = i+1; j < input.size(); j++) { pair = input.get(i, j); if (table.containsKey(pair.toString())) { testResult = table.get(pair.toString()); } else { for (int k=0; k<level; ++k) System.out.print(" "); testResult = testPairs(pair); table.put(pair.toString(), new Integer(testResult)); } record = testResult + countMaxPrimerPairs(input.delete(i,j), level+1); if (nPairs < record) { nPairs = record; } } } for (int k=0; k<level; ++k) { System.out.print(" "); } System.out.println("return " + nPairs); return nPairs; } // 测试一个数据对是否是素数伴侣 static public int testPairs(Pair p) { ... } // 判断一个数是否是质数 static private boolean isPrimer(int n) { ... } public static void main(String[] args) { // TODO Auto-generated method stub initTable(); ... } }
此时输出的结果为:
(2,3) (4,5) return 0 (2,4) (3,5) return 0 (2,5) (3,4) return 1 return 1 return 0 return 1 return 2 2
发现我们避免了反复测试,但并没有避免反复递归,于是认为最初建立相应的数学模型的时候出现了问题,让我们再来看一下相应的数学模型。
egin{equation} egin{cases} & input = left{ 1, 2, 3, 4 ight} \ & countMaxPrimerPairs(input) = testPairs(input.get(i,j)) + countMaxPrimerPairs(input.delete(i, j)) ext{ where } 0 le i le input.size(), i+1 le j le input.size() \ & countMaxPrimerPairs(input) = 0 ext{ if } input.size() = 0 end{cases} end{equation}
在第二个公式中,使用了i,j两个变量,这就是导致最终结果中被反复计算了两遍的罪魁祸首,为什么这样说呢?因为不管最终的配对情况如何,第一个数,必然会和其中的一个数形成配对,于是第一个变量i应该是一个定值,于是对公式模型进行了更改:
egin{equation} egin{cases} & input = left{ 1, 2, 3, 4 ight} \ & countMaxPrimerPairs(input) = testPairs(input.get(i,j)) + countMaxPrimerPairs(input.delete(i, j)) ext{ where } i=0, i+1 le j le input.size() \ & countMaxPrimerPairs(input) = 0 ext{ if } input.size() = 0 end{cases} end{equation}
此时得到的结果如下:
(2,3) (4,5) return 0 (2,4) (3,5) return 0 (2,5) (3,4) return 1 return 2 2
当输入参数为6个整数时,结果如下:
(2,3) (4,5) (6,7) return 1 (4,6) (5,7) return 0 (4,7) (5,6) return 1 return 2 (2,4) (3,5) return 1 (3,6) return 0 (3,7) return 1 return 1 (2,5) (3,4) return 1 return 1 return 0 return 2 (2,6) return 0 return 1 return 1 return 1 (2,7) return 1 return 0 return 1 return 2 return 3 3
此时可以发现,我们不仅将整个遍历过程实现了一遍,并且消除了重复检测的步骤。
当找到最优解时提前结束
从结果中我们可以发现,在对数据进行递归的时候,有可能很早就得到的最优解,既“素数伴侣数”为“输入数据长度的一半”,但是上述的程序在找到最优解之后依然在进行运算,这大大增加了程序运行的时间,因此,在函数中我们增加了一个判断,用于提前退出递归程序。
if (nPairs < record) { nPairs = record; // 增加了提前退出循环的判断 if (nPairs == input.size()/2) return nPairs; }
此时的结果为:
(2,3)
(4,5)
return 0
(2,4)
(3,5)
return 0
(2,5)
(3,4)
2
可见,程序很好的按照最初的预期进行运行了。
完整的程序代码
import java.util.ArrayList; import java.util.Hashtable; public class Main { private static Hashtable<String, Integer> table; static public void initTable() { table = new Hashtable<String, Integer>(); } /** * 计算最大素数伴侣对数 * @param input * @param level 标记递归中具体的层次 * @return */ static public int countMaxPrimerPairs(Input input, int level) { if (input.size() == 0) { return 0; } int nPairs = 0; int record = 0; Pair pair; int testResult; for (int j = 1; j < input.size(); j++) { pair = input.get(0, j); if (table.containsKey(pair.toString())) { testResult = table.get(pair.toString()); } else { for (int k=0; k<level; ++k) System.out.print(" "); testResult = testPairs(pair); table.put(pair.toString(), new Integer(testResult)); } record = testResult + countMaxPrimerPairs(input.delete(0,j), level+1); if (nPairs < record) { nPairs = record; if (nPairs == input.size()/2) return nPairs; } } for (int k=0; k<level; ++k) { System.out.print(" "); } System.out.println("return " + nPairs); return nPairs; } // 测试一个数据对是否是素数伴侣 static public int testPairs(Pair p) { int n = p.x + p.y; System.out.println(p); if (isPrimer(n)) { return 1; } return 0; } // 判断一个数是否是质数 static private boolean isPrimer(int n) { if (n==2 || n==3 || n==5) return true; if (n%2 == 0) return false; int sqrtV = (int) Math.sqrt(n); for (int i=3; i≤sqrtV; i=i+2) { if (n%i == 0) { return false; } } return true; } public static void main(String[] args) { // TODO Auto-generated method stub initTable(); Input input = new Input(); input.add(2); input.add(3); input.add(4); input.add(5); int n = countMaxPrimerPairs(input, 0); System.out.println(n); } } /** * 记录输入的参数信息 * @author Zz * */ class Input { private ArrayList<Integer> arrayList; public Input() { arrayList = new ArrayList<>(); } public Input(ArrayList<Integer> list) { arrayList = new ArrayList<>(list); } /** * 添加元素 * @param a */ public void add(int a) { arrayList.add(new Integer(a)); } /** * 获得相应位置的元素 * @param i 第i个位置 * @param j 第j个位置 * @return Pair */ public Pair get(int i, int j) { return new Pair(arrayList.get(i).intValue(), arrayList.get(j).intValue()); } public int size() { return arrayList.size(); } public Input delete(int i, int j) { // 使用浅复制,具体数值在内存中只存在一份 @SuppressWarnings("unchecked") ArrayList<Integer> list = (ArrayList<Integer>) arrayList.clone(); list.remove(i); list.remove(j-1); // 第一个移除,使得所有的元素都向前移动了一个位置 Input input = new Input(list); return input; } } /** * 用来记录配对点的信息 * @author Zz * */ class Pair { public final int x; public final int y; public Pair(int a, int b) { x = a; y = b; } public String toString() { return "(" + x + "," + y + ")"; } }