写在前面
此为本人在学习Python过程中遇到的案例做的学习笔记,案例来自《Python算法指南——程序员经典算法分析与实现》,源代码参考该书及网上资料,若表达有误请指正,谢谢!
问题描述
给定一个正整数n,找到若干个完全平方数(例如:1,4,9,…),使得它们的和等于n,并且使完全平方数的个数最少。
问题示例
给出n = 12,返回3,因为12 = 4 + 4 + 4;
给出n = 13,返回2,因为13 = 4 + 9;
给出n = 15,返回4,因为15 = 1 +1 + 4 + 9.
问题解决
一开始,我的想法是用递归的方法,每次减去一个小于等于n的最大完全平方数,并将得到的差赋给n,用一个函数进行递归,每次调用记录数+1,直到得到的差为0。
例如13 - 9 = ,4,记录数为1;4 - 4 = 0,记录数为2;函数最后一级返回2.
但是问题在于12 = 4 + 4 +4,如果用这种思路得到的将是12 = 9 + 1 + 1 + 1,即返回4.
因此这个方法不可行。
于是尝试使用列举的方法进行寻找规律,在列了几十个数后发现,完全平方数由本身组成,返回1;部分数如10,13等由两个完全平方数组成,返回2;诸如7,15,23等数由四个完全平方数组成,返回3;其余数均由三个完全平方数组成。
这样一来问题就比较好解决了,由于sqrt()开方返回的是一个浮点数,若(int(sqrt(n)))** 2等于n本身,那么就说明n是一个完全平方数,返回1;取i在1至int(sqrt(n))中的整数,若i ** 2 + (int(sqrt(n - i)))** 2 = n,那么说明n由两个完全平方数组成,返回2;返回4的一系列数中可找出规律为n = 7 + 8k,即n对8取余得到7;余下的数便是返回3了。
书中给出的源代码也是如此操作的,源代码如下:
def numSquares(self,n):
while n % 4 == 0:
n //= 4
if n % 8 == 7:
return 4
for i in range(n+1):
temp = i * i
if temp <= n:
if int((n-temp) ** 0.5) ** 2 + temp == n :
return 1 + (0 if temp == 0 else 1)
else:
break
return 3
原理仅仅是我根据列出来的几十个数得到的规律,并且我对其中while循环一项不理解,于是继续百度。
百度到了完美平方数的几种解法,有动态规划法,广度、深度搜索法,这里运用的是四平方定理法。
Lagrange 四平方和定理: 任何一个正整数都可以表示成不超过四个整数的平方之和。
满足四数平方和定理的数n(这里要满足由四个数构成,小于四个不行),必定满足 n = 4a ( 8 b + 7 )
这么一来,while循环的操作就能使计算过程更加简便了。
将函数放到类中(这一步可省略),定义主函数并输入值测试。
if __name__ == '__main__':
print("输入初始值:")
n = int(input())
print ("初始值:",n)
solution = Solution()
print ("结果:",solution.numSquares(n))
测试结果如下:
问题拓展
可不可以在判断完美平方数的同时将该数由哪些完全平方数组成呢?
百思不得其解,于是我又去求助度娘了。
最终在博主“一只老猿”的“完美平方数”文章找到了解决方法,不过他用的是Java版代码,我就没去细看,但是思路十分清晰。在其底下的评论中,博主“高山流水cyh”给出了Python的代码。
完整代码如下:
from collections import deque as dq
import numpy as np
import math
import pandas as pd
def perfect(n):
a=np.repeat(-1,n+1)
a[n]=n
q=dq()
q.append(n)
sq=pd.Series(range(1,math.floor(math.sqrt(n))*2+1,2)).cumsum().values
while q.__len__()>0 and a[0]<0:
x=q.popleft()
l=x-sq
l=l[l>=0]
l=l[a[l]==-1]
q.extend(l)
a[l]=x
i=0
j=a[i]
while i<n:
print(j-i,end=" ")
i=j
j=a[i]
何为DFS算法?
DFS(Depth-First-Search)深度优先搜索算法,是搜索算法的一种。是一种在开发爬虫早期使用较多的方法。它的目的是要达到被搜索结构的叶结点 。
之前在学习数据结构时有接触过,依然可以理解为最短路径探求,求从第一个完全平方数到最后一个完全平方数的最短式子,代码具体实现思路及举例说明(取n = 14)如下:
1.创建一个包含n+1个数的行矩阵,前n个数赋值为-1表示未探索,第n+1个数赋值为n
a=np.repeat(-1,n+1)
a[n]=n
得到a = [-1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 14]
2.创建一个duque对象,方便使用队列操作,将初始值n存为队列的第一个元素
q=dq()
q.append(n)
得到q = deque([13]),存取待访问路径入口
3.获取从1到n范围内的所有完全平方数存入行矩阵sq,作减数待用
sq=pd.Series(range(1,math.floor(math.sqrt(n))*2+1,2)).cumsum().values
#math.floor(x)取小于等于x的最大整数
#series是一个一维数组,Pandas会默然用0到n-1来作为series的index,第一列为index,第二列为值
#cumsum求累计次数
#values取值
#最终得到的是小于等于n的所有完全平方数
得到sq = [1 4 9]
4.开始寻求最短路径,每次从队头中弹出一个数,初始值为原数n,用弹出的数减去得到的矩阵sq得到余数矩阵l,此时已经走出路径的第一步,会有m种可能(m为sq行维度)
循环条件为队列中还有元素且存储访问标志的a的第一个元素小于零
while q.__len__()>0 and a[0]<0:
x=q.popleft()
l=x-sq
然后判断余数矩阵中的数是否大于等于零,余数为负数的路径是不合法的,在余数矩阵中会被清除;
再判断所得余数是否被探索过或者是否已存入待探索矩阵,不符合条件的被会清除,不必重复探索;
经过上述两轮更新,矩阵l中剩下的即合法且还未探索过的余数。
l=l[l>=0]
l=l[a[l]==-1]
再将l中的余数送入队列q中,进行下一轮探索;
将余数作为索引,此轮被减数x作为值,更新访问标志矩阵
q.extend(l)
a[l]=x
直到队列中元素都被探索完了或者有某个余数变为0,循环终止,代表最短路径搜索完毕
第一次循环:
x = 14
q = deque([])
l = [13 10 5]
l = [13 10 5]#第一次更新
l = [13 10 5]#第二次更新
q = deque([13 10 5])
a = [-1 -1 -1 -1 -1 14 -1 -1 -1 -1 14 -1 -1 14 14]
#索引为5,10,13的访问标志被更新为14,表示该处值由14作为被减数减去sq得到
循环条件成立,循环继续
第二次循环:
x = 13
q = deque([10, 5])
l = [12 9 4]
l = [12 9 4]#第一次更新
l = [12 9 4]#第二次更新
q = deque([10, 5, 12, 9, 4])
a = [-1 -1 -1 -1 13 14 -1 -1 -1 13 14 -1 13 14 14]
#索引为的4,9,12的访问标志被更新为13,表示该处值由13作为被减数减去sq得到
循环条件成立,循环继续
第三次循环:
x = 10
q = deque([5, 12, 9, 4])
l = [9 6 1]
l = [9 6 1]#第一次更新
l = [6 1]#第二次更新,索引为9处已有值,去掉元素9
q = deque([5, 12, 9, 4, 6, 1])
a = [-1 10 -1 -1 13 14 10 -1 -1 13 14 -1 13 14 14]
#索引为的1,6的访问标志被更新为10,表示该处值由10作为被减数减去sq得到
循环条件成立,循环继续
第四次循环:
x = 5
q = deque([12, 9, 4, 6, 1])
l = [ 4 1 -4]
l = [ 4 1]#第一次更新,去掉-4
l = []#第二次更新,1,4都已存入q中待探索
q = deque([12, 9, 4, 6, 1])
a = [-1 10 -1 -1 13 14 10 -1 -1 13 14 -1 13 14 14]
#状态不变
循环条件成立,循环继续
第五次循环:
x = 12
q = deque([9, 4, 6, 1])
l = [11 8 3]
l = [11 8 3]#第一次更新
l = [11 8 3]#第二次更新
q = deque([9, 4, 6, 1, 11, 8, 3])
a = [-1 10 -1 12 13 14 10 -1 12 13 14 12 13 14 14]
#索引为3,8,11的访问标志被更新为12,表示该处值由12作为被减数减去sq得到
循环条件成立,循环继续
第六次循环:
x = 9
q = deque([4, 6, 1, 11, 8, 3])
l = [8 5 0]
l = [8 5 0]#第一次更新
l = [0]#第二次更新,5已探索过,8已存入q中待探索
q = deque([4, 6, 1, 11, 8, 3, 0])
a = [ 9 10 -1 12 13 14 10 -1 12 13 14 12 13 14 14]
#索引为0的访问标志被更新为9,表示该处值由9作为被减数减去sq得到
循环条件不成立,循环结束
5.接下来便是按照探索路径反着回去取出该最短路径
余数为0时a矩阵的值为最后一段,该值作为最后一段的被减数,减去它本身得到零,为最后一段路径的完全平方数,输出该路径;
同时,该值又是上一路径的索引,可以取出上一路径的被减数,与其相减即可得到上一路径的一个完全平方数,输出该路径;
以此类推,取到第一段路径的数为n,路径全都被取出。
i=0
j=a[i]
while i<n:
print(j-i,end=" ")
i=j
j=a[i]
i = 0
j = 9
第一次循环:
输出9 - 0 = 9 #差值为本路径的值
i = 9 #上一路径的索引,下一路径的被减数
j = 13 #上一路径的被减数
循环条件成立,循环继续
第二次循环:
输出13 - 9 = 4
i = 13
j = 14
循环条件成立,循环继续
第三次循环:
输出14 - 13 = 1
i = 14
j = 14
循环条件不成立,循环结束
最终输出的值为9 4 1,即13 = 9 + 4 + 1为最短路径
问题思考
输入n = 200时,程序给出的结果为196 4,而根据我们的运算可知,还有另外一最优解为100 100,能否罗列出不同的最优解呢?有时间再去探索一下。
写在最后
第一次写博客,仅为个人学习记录用,排版拙劣,思路及表达有误欢迎指正探讨!