Table of Contents
- 1 前言
- 2 递归简介
- 3 利用数学知识深入对递归的认知
- 3.1 “求解最大素数伴侣数量”的实现
- 3.2 进一步改进
- 3.2.1 减少测试的重复性
- 3.2.2 当找到最优解时提前结束
- 4 完整的程序代码
前言
今天做了一个题目《素数伴侣》,具体如下:
- 输入:偶数个正整数,如,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
利用数学知识深入对递归的认知
阶乘可以用一些的公式进行表示:
???????????????????????????????f(n)=n×f(n?1) if n>1f(n)=1 if n=1
从公式中很容易就能够看出递归实现的四条基本(引用自《数据结构与算法分析 - c语言描述)法则中的两条:
- 基准情形。必须总有某些基准情形,它无需递归就能解出。如,n = 1时的情况
- 不断推进。对于那些需要递归求解的情形,每一次递归的调用都必须要使求解状况朝接近基准情形的方向推进。也就是公式中,n > 1的情况
另外两条是:
- 设计法则。假定所有的递归调用都能够运行。
- 合成效益法则。在求解一个问题的同一实例时,切勿在不同的递归中做重复性工作。
注意,本文的介绍主要是基于前两条以及最后一条法则的。
那么如何利用数学公式的形式来帮助理解前言中提到的问题呢?
???????????????????????input={1,2,3,4}countMaxPrimerPairs(input)=testPairs(input.get(i,j))+countMaxPrimerPairs(input.delete(i,j)) where 0≤i≤input.size(),i+1≤j≤input.size()countMaxPrimerPairs(input)=0 if input.size()=0
注意,其实这个数学公式模型是存在问题的,在下文中将会给出问题的所在。
“求解最大素数伴侣数量”的实现
这里通过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("\t");
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("\t");
}
System.out.println("return " + nPairs);
return nPairs;
}
此时输出的结果为:
(2,3)
(4,5)
return 1
(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("\t");
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("\t");
}
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 1
(2,4)
(3,5)
return 0
(2,5)
(3,4)
return 1
return 1
return 0
return 1
return 2
2
发现我们避免了反复测试,但并没有避免反复递归,于是认为最初建立相应的数学模型的时候出现了问题,让我们再来看一下相应的数学模型。
???????????????????????input={1,2,3,4}countMaxPrimerPairs(input)=testPairs(input.get(i,j))+countMaxPrimerPairs(input.delete(i,j)) where 0≤i≤input.size(),i+1≤j≤input.size()countMaxPrimerPairs(input)=0 if input.size()=0
在第二个公式中,使用了i,j两个变量,这就是导致最终结果中被反复计算了两遍的罪魁祸首,为什么这样说呢?因为不管最终的配对情况如何,第一个数,必然会和其中的一个数形成配对,于是第一个变量i应该是一个定值,于是对公式模型进行了更改:
???????????????????????input={1,2,3,4}countMaxPrimerPairs(input)=testPairs(input.get(i,j))+countMaxPrimerPairs(input.delete(i,j)) where i=0,i+1≤j≤input.size()countMaxPrimerPairs(input)=0 if input.size()=0
此时得到的结果如下:
(2,3)
(4,5)
return 1
(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)
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("\t");
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("\t");
}
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 + ")";
}
}