整数分解成连续自然数之和问题(《编程之美》2.21节)研究

来源:互联网 发布:类似92game源码的平台 编辑:程序博客网 时间:2024/06/07 16:04


算法的天空广阔而深奥,本人不慎也卷入进来,嘿嘿!本篇就当是本人研究算法的处女篇吧,希望能给大家一些启发。

先来看题目:

我们知道: 1 + 2 = 3;4 + 5 = 9;2 + 3 + 4 = 9 ……

上述等式,左边都是两个或两个以上连续自然数相加,那么,是不是所有的整数都可以写成这样的形式呢?

稍微考虑一下,不难发现,4、8 等数并不能写成这样的形式。

问题 1:写一个程序,对于一个 64 位正整数,输出它所有可能的连续自然数(两个以上)之和的算式。

问题 2:测试上述程序的过程中,肯定会有一些数无法表达为一系列连续自然数之和,例如 32 好像就不行……那么,这样的数有什么规律呢?能否证明?

问题 3:在 64 位正整数范围内,连续自然数相加序列数目最多的数是哪一个?如果用程序蛮力搜索,恐怕时间很长,能否用数学知识推导出来?

题目不长不短,问题却是一堆,刚一拿到,不免头大,这个题目,我们应该怎么去做呢?我这个算法菜鸟,又是个比较笨的家伙,考虑了 N 久,还好,最终有了一点思路……

对于问题 1:

要找出一个数 n 的连续自然数相加序列,不妨设这个序列中最小的数为 a,序列长度为 L,则有 n = a + (a + 1) + ... + (a + L - 1);

根据等差数列的求和公式,可以表示成 n = La + L(L - 1) / 2;做一个简单的数学变换,可以得到 a = n / L - (L - 1) / 2;

也就是说,如果 n 存在长度为 L 的连续自然数相加序列,则序列中的最小值 a = n / L - (L - 1) / 2;

由此可以得出结论,如果 a >= 1 且 a 为整数时,数 n 必存在以 a 为最小值的连续自然数相加序列。

得到了这个结论,程序就不难写出来了,只需要以 L = 2 为初值进行循环,算出所有满足要求的 a 就一切 OK 了,下面给出 C++ 程序段:

#define ZERO_EPS 0.00001
void PrintAllContPlusSeq(int64_t n)
{
    int64_t L = 2;
    double a = (double)n / L - (L - 1) / 2;
    while (fabs(a - 1) < ZERO_EPS || (int64_t)(a + ZERO_EPS) > 1))
    {
        if (fabs(a - (int64_t)a) < ZERO_EPS)
        {
            cout << n << " = ";
            for (int64_t i = (int64_t)(a + ZERO_EPS), j = 0; j < L; j++)
            {
                if (j == L - 1)
                {
                    cout << i + j;
                }
                else
                {
                    cout << i + j << " + ";
                }
            }
            cout << endl;
        }
        L++;
        a = (double)n / L - (L - 1) / 2;
    }
}

这个程序段相对比较简单,按照原本分析的思路进行了实现,但实现过程中用到了浮点数,浮点数相等判断逻辑十分复杂,且受精度限制较大,而且浮点数的内存表示也是 64 位,输入的数 n 在计算过程中所产生的中间结果 a,很有可能超过浮点数的可表示范围。因此,该程序段只能近似地实现该功能。

如何才能提高算法精度呢?再来看刚才的算式 a = n / L - (L - 1) / 2,不妨做一个数学变换 a = (2 * n - L(L - 1)) / (2 * L),这时只要判断 2 * n - L(L - 1) 对 2 * L 取余结果是否为 0 即可,这样就不会涉及任何的浮点数运算,精度提高了,但是值域却不可避免地缩小了。算式中的 2 * n 或者 2 * L,都有可能超出 64 位整数能够表示的范围。

只能再想一想了!不妨用只有小学生才会使用的带分数,来想想那诡异的除法,嘿嘿!在整数除法中,如果无法整除,那么除出的结果一定可以表示成一个带分数,即一个整数加上一个分数的形式,形如 z + u / v,其中 z 是商数,v 是除数的倍数,u 是余数的倍数。如果有两个带分数,它们的分数部分相同或相等,那么它们相减的结果一定是整数。根据这一点,判断 a 是否是整数,就可以转化为判断是否满足算式 (n % L) / L = ((L - 1) % 2) / 2 的问题了。

将判断整数的算式 (n % L) / L = ((L - 1) % 2) / 2 做一个简单的变换,得到 2 * (n % L) = L * ((L - 1) % 2),由于 (L - 1) % 2 在 L 为奇数时必为 0,在 L 为偶数时必为 1,所以可近一步将判断条件化简:

当 L 为奇数时,2 * (n % L) = 0,近一步可得到 n % L = 0;

当 L 为偶数时,2 * (n % L) = L,近一步可得到 n % L = L / 2;由于 L 是偶数,所以 L / 2 一定是整数,不会有精度损失。

这下好了,我们得到了一个近乎完美的判断条件,这个判断条件中只有求余和除法运算,值域可以扩展到很大,甚至可以扩展到无符号 64 位整数,整整扩充两倍,任意的 64 位正整数都可以拿来计算了!非常棒,嘿嘿!

void PrintAllContPlusSeq(uint64_t n)
{
    uint64_t a = n, last_a = n;
    for (uint64_t L = 2; last_a >= a && a > 1; L++)
    {
        if ((L % 2 != 0 && n % L == 0) || (L % 2 == 0 && n % L == L / 2))
        {
            last_a = a;
            a = n / L - (L - 1) / 2;
            cout << n << " = ";
            for (uint64_t j = 0; j < L; j++)
            {
                if (j == L - 1)
                {
                    cout << a + j;
                }
                else
                {
                    cout << a + j << " + ";
                }
            }
            cout << endl;
        }
    }
}

嘿嘿,简明了很多,也清晰了很多,其中可能有点问题的是直接使用 a = n / L - (L - 1) / 2 这个算式。这个算式看似会损失精度,但实际上不会,因为有了前提的判断条件保证,这两个除法所形成的小数部分可以相减抵消,和直接进行整数部分相减的效果完全一样。

问题 1 看样子我们是解决了,但是,有一点还必须加以说明。细心的亲们可能会问,随着 L 的增大,a 的值一定递减吗?如果不是这样的话,那循环岂不是永远不会结束?哈哈,这个问题太好了,下面我们就来说明这个问题。

我们可以将 a 看做以 L 为自变量的函数,设 a = f(L) = n / L - (L - 1) / 2;

对这个函数求导之后,可以得到 f'(L) = - n / L^2 - 1 / 2;(L^2 的意思是 L 的平方)

由于 n > 0,L^2 > 0,所以有 - n / L^2 < 0,则 f'(L) < 0,f(L)是减函数。到这里,大家可以不必担心了,随着 L 的增大,a 必然减小,直到它满足 a < 1 的条件。

好了,问题 1 解决了,下面来看问题 2。

题目中已经给出了很多的启发信息,4、8和32,都无法表示成一系列连续自然数相加的形式,我们大概已经猜到,这类数字应该都是 2 的幂次数,那该如何证明呢?恐怕还得回到刚才的结论上去……

再来看这个算式,a = n / L - (L - 1) / 2,刚才已经得到结论,如果 a >= 1 且 a 是整数,则 n 就有长度为 L 且最小值为 a 的连续自然数相加序列。

那么,如果 n 为 2 的幂次,不妨设 n = 2^m(2^m 表示 2 的 m 次方),在这种情况下:

如果 L 为奇数,则 n / L 必然不是整数,而 (L - 1) / 2 是整数,那么,a 一定不是整数;

如果 L 为偶数,且为 2 的幂次,则 n / L 必然是整数,而 (L - 1) / 2 不是整数,那么 a 也一定不是整数;

如果 L 为偶数,且不是 2 的幂次,则 L 可表示成 L = 2^p * q,其中 q 为奇数;那么,n / L = 2^m / L = 2^(m - p) / q,因为 q 是奇数,所以 n / L 必不是整数,且小数部分必然不是 1 / 2,又由于 L 是偶数,(L - 1) / 2 必不是整数,且小数部分必然是 1 / 2,那么二者相减的结果 a 也一定不会是整数。

L 的所有情况都已分析过,无论何种情况,a 都不是整数,所以,如果 n 为 2 的幂次,必然无法表示成连续自然数相加序列。

呼!喘一口气!哈哈,休息一下,继续问题 3,胜利就在前方了!

我们继续来看这个算式,a = n / L - (L - 1) / 2 >= 1,看右半边的不等式,整理之后的形式是 - L^2 - L + 2 * n >= 0,设 g(L) = - L^2 - L + 2 * n,则 g(L) >= 0 时,再考虑 L >= 2 的条件,则 L 取值范围是 [ 2, (sqrt(1 + 8 * n) - 1) / 2 ];g(L) 的值域是 [ 0, (8 * n - 1) / 4 ];

无论从 L 的定义域还是 g(L) 的值域上看,想让 L 或 a 取得更多的值,那么 n 就必须足够大,最多的序列数也必然出现在最大的 n 上。64 位整数 n 的最大值,有符号的是 0111...1 (63个1),无符号的是 111...1(64个1),这两个数也恰好不是 2 的幂次……

虽然从数学推导上得到了这个结论,可是……总觉得如果真是这样,有点不大符合题目的道理,另外还有一个因素没有考虑,那就是虽然值域被扩展到了最大,但是 a 能否可以取到最多的整数值呢?

也许该换个思路考虑问题,不该再陷到这个公式当中?总之得到这样的结论,让人感觉心里有点没底,而且推导过程也找不出什么问题……

不过也好,这个问题我得到了心里没底的结论,也就给了大家以讨论的空间,聪明的亲们,帮我想想,看看我有没有什么没考虑到的地方?期待着大家的参与。

好了,今天就写到这儿了,大家晚安啦!

0 0
原创粉丝点击