素数伴侣
题目描述
若两个正整数的和为素数,则这两个正整数称之为“素数伴侣”,如2和5、6和13,它们能应用于通信加密。现在密码学会请你设计一个程序,从已有的 N ( N 为偶数)个正整数中挑选出若干对组成“素数伴侣”,挑选方案多种多样,例如有4个正整数:2,5,6,13,如果将5和6分为一组中只能得到一组“素数伴侣”,而将2和5、6和13编组将得到两组“素数伴侣”,能组成“素数伴侣”最多的方案称为“最佳方案”,当然密码学会希望你寻找出“最佳方案”。
输入:
有一个正偶数 n ,表示待挑选的自然数的个数。后面给出 n 个具体的数字。
输出:
输出一个整数 K ,表示你求得的“最佳方案”组成“素数伴侣”的对数。
数据范围: 1≤n≤100 ,输入的数据大小满足 2≤val≤30000
解题思路
方案一
采用暴力破解+素数缓存
public static void main(String[] args) {
Scanner in = new Scanner(System.in);
// 获取n
int n = in.nextInt();
// 存储输入列表
List<Integer> list = new ArrayList<>();
// 素数缓存
Set<Integer> cache = new HashSet<>();
while (in.hasNextInt()) {
list.add(in.nextInt());
}
int sum = process(list,cache);
System.out.println(sum);
}
暴力递归函数
private static int process(List<Integer> list, Set<Integer> cache) {
if (list.size() < 2){
return 0;
}
int max = 0;
/**
* 每次都以下标为0的元素开始与后面的元素进行相加操作得到素数值
*/
for (int i = 1; i < list.size(); i++) {
int sum = 0;
int num = list.get(0)+list.get(i);
if (isPrem(num,cache)){
sum++;
}
/**
* 由于下标为0和下标为i的元素已经被使用,因此要在下一次递归中要从集合中移除这两个元素
* 但是对于下次循环该集合的元素要还原,因此这里做一个集合的拷贝操作
*/
List<Integer> copy = new ArrayList<>(list);
/**
* 从后往前移除元素,防止下标越界
*/
list.remove(i);
list.remove(0);
/**
* 下一次递归时,集合中不含有已经计算的元素,递归函数会得到这种匹配方式种的最大匹配素数个数
*/
sum+=process(list,cache);
/**
* 这一次循环的匹配规则的最大值和其他循环的匹配规则相比较取最大值
*/
max = Math.max(max,sum);
/**
* 还原集合,进入下一次匹配规则
*/
list = copy;
}
return max;
}
素数判单函数带缓存
private static boolean isPrem(Integer num,Set<Integer> cache){
//从缓存中获取
if (cache.contains(num)){
return true;
}
//4以及内的定是素数
if (num < 4){
cache.add(num);
return true;
}
//偶数必不是素数
if (num % 2 == 0){
return false;
}
//素数判断
for (int i = 3; i <= Math.sqrt(num); i++) {
if (num % i == 0){
return false;
}
}
cache.add(num);
return true;
}
测试用例
20923 22855 2817 1447 29277 19736 20227 22422 24712 27054 27050 18272 5477 27174 13880 15730 7982 11464 27483 19563 5998 16163
用时:1497879ms
方案二
既然这个题目可以使用暴力递归的方式进行求解,那么先想想这个递归树中是否会存在很多重复的计算,如果有就可以采用缓存的机制来进行优化,拿空间换取时间。
采用缓存+素数缓存
public static void main(String[] args) {
Scanner in = new Scanner(System.in);
// 获取n
int n = in.nextInt();
// 存储输入列表
List<Integer> list = new ArrayList<>();
// 素数缓存
Set<Integer> cache = new HashSet<>();
// dp缓存
Map<String,Integer> dp = new HashMap<>();
while (in.hasNextInt()) {
list.add(in.nextInt());
}
//首先无论数据顺序如何都不影响结果,为了增加重复概率所以这里将数据排序了,由于数组长度过小其实排序不是很影响性能
Collections.sort(list);
int sum = process(list,cache,dp);
System.out.println(sum);
}
带缓存的递归函数
private static int process(List<Integer> list, Set<Integer> cache, Map<String, Integer> dp) {
if (list.size() < 2){
return 0;
}
/**
* 如果有内容直接从缓存中获取数据
*/
if (dp.containsKey(list.toString())){
return dp.get(list.toString());
}
int max = 0;
/**
* 每次都以下标为0的元素开始与后面的元素进行相加操作得到素数值
*/
for (int i = 1; i < list.size(); i++) {
int sum = 0;
int num = list.get(0)+list.get(i);
if (isPrem(num,cache)){
sum++;
}
/**
* 由于下标为0和下标为i的元素已经被使用,因此要在下一次递归中要从集合中移除这两个元素
* 但是对于下次循环该集合的元素要还原,因此这里做一个集合的拷贝操作
*/
List<Integer> copy = new ArrayList<>(list);
/**
* 从后往前移除元素,防止下标越界
*/
list.remove(i);
list.remove(0);
/**
* 下一次递归时,集合中不含有已经计算的元素,递归函数会得到这种匹配方式种的最大匹配素数个数
*/
sum+=process(list,cache);
/**
* 这一次循环的匹配规则的最大值和其他循环的匹配规则相比较取最大值
*/
max = Math.max(max,sum);
/**
* 还原集合,进入下一次匹配规则
*/
list = copy;
}
// 向缓存中获取数据
dp.put(list.toString(),max);
return max;
}
素数判单函数带缓存
private static boolean isPrem(Integer num,Set<Integer> cache){
//从缓存中获取
if (cache.contains(num)){
return true;
}
//4以及内的定是素数
if (num < 4){
cache.add(num);
return true;
}
//偶数必不是素数
if (num % 2 == 0){
return false;
}
//素数判断
for (int i = 3; i <= Math.sqrt(num); i++) {
if (num % i == 0){
return false;
}
}
cache.add(num);
return true;
}
测试用例
20923 22855 2817 1447 29277 19736 20227 22422 24712 27054 27050 18272 5477 27174 13880 15730 7982 11464 27483 19563 5998 16163
用时:269ms(通过)
1507 6611 20966 571 1023 9390 16632 16742 12152 18621 4670 25368 15333 7461 12737 24979 10819 1505 13802 20395 6112 18161 23373 9154 14116 24391 27280 27686
用时:6237ms(超时)
方案三
从效果上看采用缓存性能得到了极大的提升,起码提升了接近5000倍,然后上述缓存法唯一的优化点在于排序这里可以采用,数组存储,再添加元素的时候直接进行排序,这样就可保证不需要再次进行O(logN)的时间复杂度的排序了。但是,我们需要知道,这里N的数量并不多,排序这里的消耗基本忽略不计。
笔者到这里,已经黔驴技穷了。测试用例确实还是跑不过。查阅解析后发现这个题目不应该采用动态规划的解题思路,
由于素数的性质导致只有两种数之和才会产生素数,即奇数和偶数相加。因此这个题目就可转换为:求解奇数列表中的数据和偶数列表数据的最大匹配数的问题。对于这个问题有一个比较好的算法就是-----匈牙利算法。哈哈,后知后觉,素数伴侣,题目就在暗示匈牙利算法这种求匹配的算法。
尴尬的是,笔者确实是第一次听到这个算法,略微了解后好像明白了。但是,有感觉说不清楚,就不在这里误人子弟了。
采用匈牙利算法
public static void main(String[] args) {
Scanner in = new Scanner(System.in);
// 获取n
int n = in.nextInt();
//存放奇数
List<Integer> odd = new ArrayList<>();
//存放偶数
List<Integer> even = new ArrayList<>();
while (in.hasNextInt()) { // 注意 while 处理多个 case
Integer num = in.nextInt();
if (num % 2 == 0) {
even.add(num);
} else {
odd.add(num);
}
}
//一个小优化,如果全为奇数或者全为偶数,必然不可能有素数伴侣
if (even.size() == 0 || odd.size() == 0) {
System.out.println(0);
return;
}
// 素数缓存
Set<Integer> cache = new HashSet<>();
//构建二分图
boolean[][] map = getMap(odd, even, set);
//匈牙利算法求解最后的答案
int result = hungarian(map);
System.out.println(result);
}
构建二分图
/**
* 构建奇偶匹配成素数的二分图
* @param odd 奇数集合
* @param even 偶数集合
* @param cache 缓存
* @return 构建奇偶匹配成素数的二分图
*/
private static boolean[][] getMap(List<Integer> odd, List<Integer> even, Set<Integer> cache) {
boolean[][] result = new boolean[odd.size()][even.size()];
for (int i = 0; i < odd.size(); i++) {
for (int j = 0; j < even.size(); j++) {
if (isPrem(odd.get(i) + even.get(j), cache)) {
result[i][j] = true;
}
}
}
return result;
}
匈牙利算法
private static int hungarian(boolean[][] map) {
int sum = 0;
//记录下已经匹配的行号,用于回溯
Integer[] rowRecode = new Integer[even.size()];
for (int i = 0; i < map.length; i++) {
//记录下这列是否被访问过
Integer[] vis = new Integer[even.size()];
if (match(i, map, row, vis)) {
sum++;
}
}
return sum;
}
private static boolean match(int row, boolean[][] map, Integer[] rowRecode, Integer[] vis) {
for (int i = 0; i < map[row].length; i++) {
if (map[row][i] && vis[i] == null ) {
vis[i] = 1;
//如果这列没有匹配的行则将该列的行号记录为当前行,
//如果有则回溯已被占用行,如果已被占用行存在下一个匹配值则替换
if (rowRecode[i] == null || match(rowRecode[i], map, rowRecode, vis)) {
rowRecode[i] = row;
return true;
}
}
}
return false;
}
素数判单函数带缓存
private static boolean isPrem(Integer num,Set<Integer> cache){
//从缓存中获取
if (cache.contains(num)){
return true;
}
//4以及内的定是素数
if (num < 4){
cache.add(num);
return true;
}
//偶数必不是素数
if (num % 2 == 0){
return false;
}
//素数判断
for (int i = 3; i <= Math.sqrt(num); i++) {
if (num % i == 0){
return false;
}
}
cache.add(num);
return true;
}
测试用例
1507 6611 20966 571 1023 9390 16632 16742 12152 18621 4670 25368 15333 7461 12737 24979 10819 1505 13802 20395 6112 18161 23373 9154 14116 24391 27280 27686
用时:1ms(通过)