算法练习之 564.求出大于或等于 N 的最小回文素数。

问题要求:

求出大于或等于N的最小回文素数。

知识点回顾:

素数:如果一个数大于1,且其因数只有1和他自身,那么这个数就是素数。例如:2,3,5,7
回文数:一个数如果从左往右读与从右往左读是一样的,那么这个数就是回文数。比如:12321

参考用例:

示例1:输入6,输出7
示例2:输入8,输出11
示例3:输入13,输出101

提示

1<=N<=10^8
答案肯定存在,并且小于2*10^8

问题分析

首先介绍三个数学定理:

1. 长度为偶数的回文数都能被11整除
2. 除了2和3以外,所有的素数都分布在6的两侧
3. 如果一个数字的个位能够整除2,那么这个数必定可以整除2

要求解这个问题,我们可以将问题分成两个大问题,1.判定一个数是否是素数。2.求大于或等于一个数的最小回文数。

求解

1.判定一个数是否是素数

要判定一个数是否是素数,可以使用定理2(证明这里就不给了,百度都有。)。对于定理2,我们同时可以理解为所有素数的形式都可以写成6x+1 或 6x-1(这里的x是是大于等于1的自然数。)。所以判定一个数num是否是素数,我们可以先判定这个数num是否分布在6的两侧,如果不是则这个数num肯定不是素数。如果这个数num是分布在6的两侧,也不一定是素数,不一定是素数是因为两个在6同一侧的素数相乘,其结果还在6的那一侧,比如:55=25,25=64+1,所以我们还需要判定这个数num是否有6的两侧的数的因数。还有一个定理就是我们要判定一个数是否是素数,只需要判定小于这个数√num中是否有其因数。由于已经判定num是分布在6的两侧,所以我们就可以直接判断6的两侧的数是否是其因素。

代码:

//判断一个数是否是素数
/**
思路:一个大于6的素数必定定义写成6*i+1或6*i+5的形式,其中6*i+5可以写成6*i-1,因为+5到-1正好是一个6的轮回。
 */
func IsPrimeNum(num int) bool {
	if num <= 3 {//如果num小于等于3时,只有2和3是素数
		return num > 1
	}
	//3
	if num%6 != 1 && num%6 != 5 {
		return false
	}
	sq := int(math.Sqrt(float64(num)))
	for i := 5; i <= sq; i += 6 {
		if num%i == 0 || num%(i+2) == 0 {
			return false
		}
	}
	return true
}
2.求大于等于一个数的最小回文数

求大于等于一个数的最小回文数,我们可以先将数字num转换成字符串(容易对每一位操作)。在求回文数之前,这里首先说明一个点:大于数字num的最小回文数numPa,当num长度是奇数时,num修改地方一定在[len(num)/2, len(num)];当num长度是奇数是,num一定需要修改的地方是[len(num)/2 -1, len(num)]。也就是说数字num的一半right是一定会修改的,并且是比原来的大。对于数字num,num的长度不同时求回文数的处理是不同的。
当num的长度是偶数时:想要求大于num的最小回文数,我们需要将数字分成两个部分,leftNum数字num左半部分和rightNum数字num右半部分,以及reverseLeftNum数字num左半部分反转。比如123456,leftNum = 123, rightNum = 456, reverseLeftNum=321。num长度是偶数时,如果我们直接将数字num的左边反转到右边,可以发现得到的回文数是小于num,其原因就是reverseLeftNum是小于rightNum,所以就会导致求出来的结果小于原来的数num。但是当reverseLeftNum大于rightNum时呢,比如数字437123,reverseLeftNum=734,rightNum=123,直接将数字左半部反转给右半部得到的是437734,是大于原来的数字的。所以这里我们可以得出结论:当num长度是偶数时,如果reverseLeftNum大于时,那么大于num的最近的回文数就是leftNum+reverseLeftNum;如果reverseLeftNum小于rightNum时,我们需要对leftNum++,然后再反转leftNum给reverseLeftNum,这是reverseLeftNum肯定就大于rightNum,那么大于num的最近的回文数就是leftNum+reverseLeftNum。
当num长度是奇数时,需要把数字拆分成三部分,leftNum,middle和rightNum,只需要将middle+1,然后把leftNum反转给rightNum即可。当middle是9时,leftNum++,middle赋值成0,然后把leftNum反转给rightNum。
代码:

//获取大于等于数字最近的最近的回文数
func GetNearestPaNum(strNum string) string {
	if IsPalindromeNum(strNum) {
		return strNum
	}
	l := len(strNum)
	var leftNum, reverseLeftNum, rightNum, middle int64
	var leftStr, reLeftStr, rightStr string
	//
	/*
	将数字拆分成三部分,left和right以及middle,其中left的len和right的len是相等的,
	如果数字的长度是奇数,则middle的值为数字最中间那个值,如果数字长度是偶数的,则middle的值为-1,即表示不存在
	 */
	if l%2 == 0 {
		leftNum, _ = strconv.ParseInt(strNum[:l/2], 10, 64)
		reLeftStr = getReverseStr(strNum[:l/2])
		reverseLeftNum, _ = strconv.ParseInt(reLeftStr, 10, 64)
		middle = -1
		rightNum, _ = strconv.ParseInt(strNum[(l)/2:], 10, 64)
		leftStr = strNum[:l/2]
		rightStr = strNum[(l)/2:]
	} else {
		leftNum, _ = strconv.ParseInt(strNum[:(l-1)/2], 10, 64)
		reLeftStr = getReverseStr(strNum[:(l-1)/2])
		reverseLeftNum, _ = strconv.ParseInt(getReverseStr(strNum[:(l-1)/2]), 10, 64)
		middle, _ = strconv.ParseInt(string(strNum[l/2]), 10, 64)
		rightNum, _ = strconv.ParseInt(strNum[l/2+1:], 10, 64)
		leftStr = strNum[:(l-1)/2]
		rightStr = strNum[l/2+1:]
	}
	/*
	再根据reverseLeftNum和rightNum的大小来判断,如果reverseLeftNum大于rightNum,则回文数的右半部就是reverseLeftNum
	如果reverseLeftNum小于rightNum,则回文数的左半部leftNum需要+1,右半部为左半部的反转
	 */
	if reverseLeftNum > rightNum {
		rightStr = reLeftStr
	} else if reverseLeftNum < rightNum {
		if middle == -1 {
			leftNum++
			leftStr = strconv.FormatInt(leftNum, 10)
			rightStr = getReverseStr(leftStr)
		} else if middle == 9 {
			leftNum++
			leftStr = strconv.FormatInt(leftNum, 10)
			rightStr = getReverseStr(leftStr)
			middle = 0
		} else {
			middle++
			rightStr = reLeftStr
		}
	}
	//左右拼接
	if middle != -1 {
		return leftStr + strconv.FormatInt(middle, 10) + rightStr
	} else {
		return leftStr + rightStr
	}
}

再根据定理1,我们就可以跳过长度为偶数的回文数。
代码:

/**
思路: 需要用到两个定理:1.偶数位的回文数都能被11整除。2.除了2和3以为,所有的素数都分布在6两侧
如果当前数字的长度是偶位数,其长度是l,则我们可以从10000...001处进行判断,其长度为l+1,也就是我们可以跳过偶数的回文数

 */
func PrimePalindrome(N int) int {
	if N == 10 || N == 11 {
		return 11
	}
	nStr := strconv.FormatInt(int64(N), 10) //将数字转为字符串
	l := len(nStr)
	if l < 2 { //如果n的长度为1,
		for !IsPrimeNum(N) && l == 1 {
			N++
			nStr = strconv.FormatInt(int64(N), 10)
			l = len(nStr)
		}
		if l == 1 { //上面的for是因为N的长度大于1而跳出的
			return N
		}
		return 11
	}
	//长度大于1,需要根据第一个定理进行判断
	if l%2 == 0 { //N的长度为偶数
		N = int(math.Pow10(l) + 1) //让N从长度为偶数位开始跳过一些数字
		nStr = strconv.FormatInt(int64(N), 10)
	}
	nStr = GetNearestPaNum(nStr) //找到N最近的回文数,包括本身
	n64, _ := strconv.ParseInt(nStr, 10, 64)
	N = int(n64)
	for !IsPrimeNum(N) {
		N++
		nStr = GetNearestPaNum(strconv.FormatInt(int64(N), 10))
		l = len(nStr)
		n64, _ = strconv.ParseInt(nStr, 10, 64)
		N = int(n64)
		if l%2 == 0 {
			N = int(math.Pow10(l) + 1) //让N从长度为偶数位开始跳过一些数字
			nStr = strconv.FormatInt(int64(N), 10)
		} else {
			//跳过偶数的回文数,比如808这个回文数,其大于他最近的回文数是818,明显不是素数,所以可以跳过成909,
			//有个数学定理,如果一个数字的尾数可以整除2,那么这个数字必定可以整除2
			last, _ := strconv.ParseInt(string(nStr[l-1]), 10, 64)
			if last%2 == 0 { //尾数可以整除2
				last++
				str := strconv.FormatInt(last, 10)
				strByte := []byte(nStr)
				strByte[0] = byte(str[0])
				strByte[len(strByte)-1] = strByte[0]
				nStr = string(strByte)
			}
		}

	}
	return N
}

//获取大于等于数字最近的最近的回文数
func GetNearestPaNum(strNum string) string {
	if IsPalindromeNum(strNum) {
		return strNum
	}
	l := len(strNum)

	//num, _ := strconv.ParseInt(strNum, 10, 64)
	var leftNum, reverseLeftNum, rightNum, middle int64
	var leftStr, reLeftStr, rightStr string
	//
	/*
	将数字拆分成三部分,left和right以及middle,其中left的len和right的len是相等的,
	如果数字的长度是奇数,则middle的值为数字最中间那个值,如果数字长度是偶数的,则middle的值为-1,即表示不存在
	 */
	if l%2 == 0 {
		leftNum, _ = strconv.ParseInt(strNum[:l/2], 10, 64)
		reLeftStr = getReverseStr(strNum[:l/2])
		reverseLeftNum, _ = strconv.ParseInt(reLeftStr, 10, 64)
		middle = -1
		rightNum, _ = strconv.ParseInt(strNum[(l)/2:], 10, 64)
		leftStr = strNum[:l/2]
		rightStr = strNum[(l)/2:]
	} else {
		leftNum, _ = strconv.ParseInt(strNum[:(l-1)/2], 10, 64)
		reLeftStr = getReverseStr(strNum[:(l-1)/2])
		reverseLeftNum, _ = strconv.ParseInt(getReverseStr(strNum[:(l-1)/2]), 10, 64)
		middle, _ = strconv.ParseInt(string(strNum[l/2]), 10, 64)
		rightNum, _ = strconv.ParseInt(strNum[l/2+1:], 10, 64)
		leftStr = strNum[:(l-1)/2]
		rightStr = strNum[l/2+1:]
	}
	/*
	再根据reverseLeftNum和rightNum的大小来判断,如果reverseLeftNum大于rightNum,则回文数的右半部就是reverseLeftNum
	如果reverseLeftNum小于rightNum,则回文数的左半部leftNum需要+1,右半部为左半部的反转
	 */
	if reverseLeftNum > rightNum {
		rightStr = reLeftStr
	} else if reverseLeftNum < rightNum {
		if middle == -1 {
			leftNum++
			leftStr = strconv.FormatInt(leftNum, 10)
			rightStr = getReverseStr(leftStr)
		} else if middle == 9 {
			leftNum++
			leftStr = strconv.FormatInt(leftNum, 10)
			rightStr = getReverseStr(leftStr)
			middle = 0
		} else {
			middle++
			rightStr = reLeftStr
		}
	}
	//左右拼接
	if middle != -1 {
		return leftStr + strconv.FormatInt(middle, 10) + rightStr
	} else {
		return leftStr + rightStr
	}
}

func getReverseStr(str string) string {
	s := ""
	for i := len(str) - 1; i >= 0; i-- {
		s += string(str[i])
	}
	return s
}

//检查一个数是否是回文数
func IsPalindromeNum(strNum string) bool {
	l := len(strNum)
	for i, j := 0, l-1; i < j; i, j = i+1, j-1 {
		if strNum[i] != strNum[j] {
			return false
		}
	}
	return true
}

//判断一个数是否是素数
/**
思路:一个大于6的素数必定定义写成6*i+1或6*i+5的形式,其中6*i+5可以写成6*i-1,因为+5到-1正好是一个6的轮回。
 */
func IsPrimeNum(num int) bool {
	if num <= 3 {//如果num小于等于3时,只有2和3是素数
		return num > 1
	}
	//3
	if num%6 != 1 && num%6 != 5 {
		return false
	}
	sq := int(math.Sqrt(float64(num)))
	for i := 5; i <= sq; i += 6 {
		if num%i == 0 || num%(i+2) == 0 {
			return false
		}
	}
	return true
}

该算法通过领扣的测试用例,并且用时最少,消耗内存最低。

你可能感兴趣的:(算法)