题目描述
若两个正整数的和为素数,则这两个正整数称之为“素数伴侣”,如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
输入描述:
输入说明1 输入一个正偶数n
2 输入 n 个整数
输出描述:
求得的“最佳方案”组成“素数伴侣”的对数。
示例1
输入:
4
2 5 6 13
输出:2
示例2
输入:
2
3 6
输出:0
转自牛客网_
针对这种求两两最大配对数的类型题目,通用解法就是匈牙利算法,该算法是1955年,库恩(W.W.Kuhn)利用匈牙利数学家康尼格(D.Kőnig)的一个定理构造了这个解法,故称为匈牙利法。具体大家可以在网上搜索到更加详细的信息。
匈牙利算法的核心就是:先到先得,能让则让;
分析
如图针对左侧ABCD 去匹配右侧abcd,其中A可匹配a,b,记作A->a,b同理可得:
B->a,c
C->a,b
D->c
我们现在要求只能两两配对,比如A虽然可以配对a和b,但是法律不允许“重婚“,所以A只能有一个配偶,基于”先到先得”的原则。A先和a匹配,结合为Aa;
而如果a已经和A配对,当B来和a配对时,如果发现a已经配对了A,那么此时再检查a原来配对的A是否还有其他心仪对象,发现A还可以和b配对;那么不好意思,基于能让则让的原则此时需要拆散原来Aa的匹配,改为Ab匹配,然后既然此时Aa匹配已经拆掉了,那么Ba匹配就可以成立了;
此时已经建立的匹配就是
Ab
Ba
以此类推,当C来和a匹配的时候,发现a已经匹配B,那么在检查B是否还有其他心仪对象,发现B还可以匹配c,所以B让出a然后去匹配c,也就是拆除Ba->重组Bc。 然后C和a组成Ca,此时配对如下:
Ab
Bc
Ca
那么可以类推D来匹配c的时候,会尝试拆除Bc,让B再去查找其他心仪对象,但是此时B除了c之外只能和a匹配,不过a已经匹配了C了,在往下C只能和b匹配,但是b已经匹配了A了,此时我们找遍了c,a,b,都没能够找到能重新建立的匹配,虽然剩下d但是d不是D甚至不是左侧任何人的“心仪对象“,所以此次匹配宣告失败,D无匹配。所以得到匹配的组合为:
Ab
Bc
Ca
最多3对,对于D只能说声”抱歉了!“
回到题目
我们回到素数伴侣的题目上来,如果两个数字的和是一个素数,那么这两个数字就是一对素数伴侣。可以此为依据,构建各个数字能否成为”伴侣“的关系网格矩阵;
构建匹配关系矩阵
而要像两个数之和为素数,那么他们的和就不能是一个大于2的偶数,其和只能是奇数才有可能,比如:2,5,6,13
分成奇数偶数两组:
奇数odds = [5,13]
偶数evens = [2,6]
网格如下:
所以构建的可建立关系的网格T如下:
T[i][j]标识为True则表示x[i]和y[j]可以匹配,如[2,5],[6,5],[6,13]
我们从横向奇数出发,遍历横向的每个元素,针对每个元素去寻找他的伴侣,也就是在纵向的偶数中去找。
##记录能建立的匹配
寻找的过程中用一个数组evens_match来记录偶数匹配的伴侣,该数组长度和evens数组一样;
对于evens_match组数,其所有的元素都初始化为-1;
evens_match数组使用下标序号作为数字的唯一识别,比如[6,5]这一组匹配:
偶数evens = [2,6]
奇数odds = [5,13]
偶数6的下标在evens中是1;奇树5的下标在odds中是0;
所以对于evens_match可以记录[6,5]这一组匹配为 evens_match[1]=0,就是下标为1的偶数其对应匹配的是下标为0的奇数,这个”下标“奇数偶数在其各自odds和evens数组中的序号;
#记录寻找过程中的足迹
而每一个奇数去匹配之前,我们初始化一个数组evens_visited,这个evens_visited作用是作为一个我们访问过哪些偶数元素的一个记录,其数组长度和evens数组一样;
evens_visited的所有元素初始化为False,当每访问过一个偶数元素后,我们就在evens_visited数组中,同偶数下标的元素设置为True, 比如[6,5]这一组,在找奇数5的匹配时,如果发现6没有访问过,而6在偶数数组evens中的下标是1,那么evens_visited[1] = True 表示在evens数组中下标为1的元素已访问。
上代码
直接上code(py):
def is_primer(v1,v2):
# 判断v1+v2的结果是否素数
n = v1+v2
if n == 1 or n ==2:
return True
for v in range(2, n // 2 + 1):
r, m = divmod(n, v)
if r > 0 and m == 0:
return False
return True
def main():
quantity = int(input().strip())
nums = list(map(int,input().strip().split()))
# 奇数偶数分组
odds,evens = [],[]
for each in nums:
if each %2 == 0:
evens.append(each)
else:
odds.append(each)
# arr作为奇偶匹配关系的网格矩阵
arr = [[False for _ in range(len(evens))] for _ in range(len(odds))]
for i in range(len(arr)):
for j in range(len(arr[i])):
arr[i][j] = is_primer(odds[i],evens[j])
# print(arr)
evens_match = [-1 for _ in range(len(evens))]
def match(oi):
# oi是奇数下标
# 对于每个奇数,迭代偶数数组去找能和其匹配的偶数
for j in range(len(evens)):
# 如果奇偶之间的匹配关系存在,也就是奇偶之和是素数;并且本次找到的偶数之前没访问过
if arr[oi][j] and not evens_visited[j]:
# 标记该偶数已访问过
evens_visited[j] = True
# 检查偶数是否之前没有匹配过
# 或者当前的偶数如果匹配过,那么其对应匹配的奇数是否能重新建立其他匹配
if evens_match[j] == -1 or match(evens_match[j]):
# 标记当前偶数匹配的奇数,以其在odds数组中的序号下标作为记号,记录匹配的映射关系
evens_match[j] = oi
return True
# 如果所有的偶数都找完了都没有匹配到,返回False
return False
cnt = 0
for i in range(len(odds)):
# 每开始一个新的奇数的匹配查找都重新初始化evens_visited
evens_visited = [False for _ in range(len(evens))]
if match(i):
# 如果下标i对应的奇数能找到偶数匹配那么计数cnt加1
cnt += 1
print(cnt)
if __name__ == '__main__':
main()
每轮匹配都是新的过程
可能有朋友不理解为什么每次对一个新的奇数去找匹配的偶数的时候都要重新初始化evens_visited.这个其实可以这么看,如果evens_visited 始终都是单独一份公共的记录,那么某一个奇数在查找匹配的时候,他可能会访问很多的偶数,但是访问过不代表这个偶数就能和其匹配成功。
在match函数中除了判别匹配关系arr[oi][j]之外还要判别偶数是否已访问,如果偶数在本轮奇数的匹配查找中已经访问过那么不会在尝试去匹配,这是正常的逻辑,但是如果这个访问过的标记一直存在,那么当下一个奇数开启新一轮匹配的时候,它无法找到这个偶数,即使这个偶数可能在之前某次匹配循环过程中已经断开某个匹配变成了未配对的偶数,也无法被访问到。
偶数能不能匹配在match函数中有两个if判断,一层是判断arr[io][j]能够建立匹配,以及偶数未访问过,另外一层是偶数未配对或者偶数已配对但是其之前的匹配对象,也就是奇数对象能断开并重新建立其他新匹配;只有满足这两个条件才能使本轮的奇数配对成功。