和为S的连续正数序列(数学技巧,事半功倍)

  数学的基础在算法中的用处大概就是对数字的敏感性了,如果能找到数字的规律,在求解数字的时候往往就能根据规律快速求解。而普通的程序思维相比数学思维,一般就略微显得暴力。

题目描述

  小明很喜欢数学,有一天他在做数学作业时,要求计算出9~16的和,他马上就写出了正确答案是100。但是他并不满足于此,他在想究竟有多少种连续的正数序列的和为100(至少包括两个数)。没多久,他就得到另一组连续正数和为100的序列:18,19,20,21,22。现在把问题交给你,你能不能也很快的找出所有和为S的连续正数序列?

题目分析

  这个题目也算得上是给你一个数计算机能玩一天的题目,我们仍然从直观的思维开始一步步深入分析。
  要求这个序列,我们可以逐个数字去找从 i i 开始的序列,显然 i > 0 i>0 ,但是我们也没有必要所有序列都去找,毕竟以 i i 开始的序列,最多只有一个等于S,如果我们发现以 i i 开始,以 j j 结尾的序列和大于等于S了,显然后面就不用找了。此外,序列最少要包括两个数,显然如果 i S 2 i≥\frac{S}{2} ,那么两个数之和肯定大于S了,此时我们也不用找了。我们求和的时候每一次序列多一个数,所以求和的复杂度是 O ( 1 ) O(1) ,但是我们要找多少个序列,这个问题的复杂度就是多少,大致是 O ( S l g S ) O(SlgS) 的。

双指针/滑动窗口法

  我们要表示一个连续序列,我们只需要知道两个数的位置就够了,只需要知道从开始开始,到哪里结束。既然使用双指针的思想,那就可以参考和为S的两个数字,双指针解法。我们使用两个指针,一个记录序列的开始,一个记录序列的结束。
  如果两个指针记录的序列和小于S,则说明应该增大和,需要往序列里面添加数字,此时记录结尾的指针就需要后移,纳入更多的数字。如果序列和大于S,那么说明以序列开头的数字不可能存在和为S的序列了。此时需要减少数字,则开头指针后移。如果和等于S,则把这个序列记录,然后开头指针后移。这样的操作不会漏掉满足需求的值原理和引用的文章类似,有兴趣可以回去查看。
  我们来看一个例子,以S=9为例。初始化前指针pBeg=1,后指针pEnd=2。

pBeg pEnd sum 状态 S 下一步操作
1 2 3小于9 pEnd++
1 3 6小于9 pEnd++
1 4 10大于9 pBeg++
2 4 9等于9(保存) pBeg++
3 4 7小于9 pEnd++
3 5 12大于9 pEnd++
4 5 9等于9(保存) pBeg++,pBeg==5退出

  熟悉了这个过程,差不多可以写出代码了。

C++代码
class Solution {
public:
	vector<vector<int>>  FindContinuousSequence(int S) {
		vector<vector<int>> res;
		int pBeg = 1, pEnd = 2,sum=3,mid=S>>1;
		while (pBeg <= mid)
		{	
			if(sum>=S)
			{
				if (sum == S)
				{
					vector<int> tmp;
					for (int i = pBeg; i <= pEnd; i++)tmp.push_back(i);
					res.push_back(tmp);
				}
				sum -= pBeg;
				pBeg += 1;
			}
			else
			{
				pEnd += 1;
				sum += pEnd;
			}
		}
		return res;
	}
};
python代码
class Solution:
    def FindContinuousSequence(self, S):
        pBeg = 1
        pEnd = 2
        middle = (S + 1)>>1
        curSum = 3
        output = []
        while pBeg < middle:
            if curSum >= S:
                if curSum == S:
                    output.append(range(pBeg, pEnd + 1))
                curSum -= pBeg
                pBeg += 1
            else:
                pEnd += 1
                curSum += pEnd
        return output

  这里用的还是双指针或者叫滑动窗口的技巧求解,因为两个指针都是单向前进,没有回溯的过程,所以这个时间复杂度很好分析,就是 O ( S ) O(S) 了,这个技巧还是蛮重要的,可以看到相比第一种分析复杂度已经进步了不少,因为这里已经利用了和的比较关系,省去了很多计算。

纯数学技巧法

  如果我们觉得上面的方法还是在找连续序列的和,这个方法就可以说是直接计算了。我们都知道如果让我们去求连续序列的和,我们肯定会知道一个公式,(首项+尾项)*项数/2。但是这里需要怎么用上这个公式呢。
  我们假设序列的首项是i,尾项是j。我们可以轻易求出 s u m = ( i + j ) ( j i + 1 ) / 2 sum=(i+j)(j-i+1)/2 ,我们把2移到等式左边可以得到 2 s u m = ( i + j ) ( j i + 1 ) 2sum=(i+j)(j-i+1) ,显然我们有点二年级的数学水平就知道 ( i + j ) ( j i + 1 ) (i+j)和(j-i+1) 一个是奇数,一个是偶数。那现在就容易多了,我们直接求出sum的所有奇数因子,那么显然所有的奇数因子就构成了一组或两组解。可以通过质因数分解,把所有的质因数任意组合则得到所有的质数因子。
  为什么是一组或者两组,因为可能i解出来是个负数。我们不妨设这个奇数因子是odd,那么显然那个偶数因子就是 s u m 2 / o d d sum*2/odd ,这个时候用两个数列两个方程:
{ o d d = i + j 2 s u m o d d = j i + 1 a n d { o d d = i + j 2 s u m o d d = j i + 1 \left \{ \begin{aligned} odd & = & i+j \\ \frac{2sum}{odd} & = & j-i+1 \\ \end{aligned} \qquad and \qquad \right. \left \{ \begin{aligned} odd & = & i+j \\ \frac{2sum}{odd} & = & j-i+1 \\ \end{aligned} \right.
  通过这两个方程求解出 i i j j ,然后舍去不符合条件的数值。如果这是一道选择题,到这里也就结束了。我下面给出一个参考代码。只知道在大数测试上,双指针根本无法等待出结果,这个方法很快就能算出来。因为需要质因数分解,难以评估复杂度,代码中需要保存下来的质数可以用于后续计算,这样计算的次数越多,省下来的时间就越多。

class Solution:
    prim = [2, 3, 5, 7, 11]
    def primeFactorization(self, tsum):
        for i in range(self.prim[-1]+1,int(tsum**0.5),2):
            flag=True
            for j in self.prim:
                if i%j==0:
                    flag=False
                    break
            if flag:
                self.prim.append(i)
        i=0
        fac=[]
        while i<len(self.prim):
            if tsum%self.prim[i]:
                i+=1
                continue
            fac.append(self.prim[i])
            tsum/=self.prim[i]
        return fac

    def FindContinuousSequence(self, tsum):
        fac=self.primeFactorization(tsum)
        odd=[i for i in fac if i%2]
        fact=set()
        res=[]
        mul=lambda x,y:x*y
        for i in range(len(odd)):
            l=permutations(odd,i+1)
            for j in l:
                a=reduce(mul,j)
                fact.add(a)
                fact.add((tsum<<1)//a)
        fact=sorted(list(fact))
        for i in fact:
            j=tsum*2//i
            if i<j+1:
                continue
            res.append(list(range((i-j+1)>>1,(i+j+1)>>1)))
        return res

  这种方法仅供参考,有利于思维方式的提高。总之一句话,数学能力在算法分析中会起到至关重要的作用,之中能力是需要慢慢累积出来的。

发布了36 篇原创文章 · 获赞 4 · 访问量 47万+

猜你喜欢

转载自blog.csdn.net/m0_38065572/article/details/104527075