《編程之美》學習筆記——2.21只考加法的面試題

一、問題

我們知道:

1 + 2 = 3;

4 + 5 = 9;

2 + 3 + 4 = 9;

等式兩邊都是兩個以上的連續的自然數相加,那麼是不是所有的整數都可以寫成這樣的形式呢?稍微考慮一下,我們發現,4、8等數並不能寫成這樣的形式。

問題1:寫一個程序,對於一個64位正整數,輸出它所有可能的連續自然數(兩個以上)之和的算式。

問題2:大家在測試上面程序的過程中,肯定會注意到有一些數字不能表達爲一系列連續的自然數之和,例如32好像就找不到。那麼,這樣的數字有什麼規律,能否證明你的結論。

問題3:在64位整數範圍內,子序列數目最多的數是哪一個?這個問題要用程序蠻力搜索,恐怕要運行很長時間,能否用數學知識推導出來?

二、解法——問題1

  解法一 數學分析(二元方程)求解

問題分析:

輸入:Sum(64位正整數),可轉化爲:Sum >= 1,Sum <= 2^64 - 1,Sum爲整數。

輸出:所有可能的連續自然數(兩個以上)之和的算式。我們可以利用兩個數來表示一個可能的解:第一個數X表示連續自然數中最小的一個數,有X >= 1,X爲整數;第二個數Y表示連續自然數的個數,有Y >= 2,Y爲整數。我們再利用數N表示解的個數(N = 0時表示無解)。這樣,問題的輸出可以表示爲:N對(Xi,Yi)( 0 <= i < N),方便我們在計算機上進行表示。例如:輸入:Sum = 9 時輸出:N = 2,(X0,Y0) = (4,2)(表示4 + 5 = 9這個解),(X1,Y1) = (2,3)(表示2 + 3 + 4 = 9這個解)。後續處理中,我們可以把每對(Xi,Yi)藉助字符串處理函數轉換爲形如“Xi + (Xi + 1) + ... + (Xi + Yi - 1) = Sum”這樣的形式作爲輸出,使得輸出爲所要求的格式。

約束條件:根據上面輸入輸出的表示形式,約束主要指Sum、X、Y這三個數的取值範圍和數據類型,上面均已給出。

思考:根據問題的輸入輸出表示,我們實際上求解的是:

  對輸入整數Sum,Sum >= 1,Sum <= 2^64 - 1,找出滿足如下等式(等式左邊爲連續自然數求和公式):

    (2 * X  + Y - 1) * Y / 2 = Sum (X >= 1,Y >= 2,X和Y爲整數)的所有解X,Y 。

Y爲奇數時,等式左邊(2 * X  + Y - 1)項爲偶數,可以被2整除,Y爲偶數時,等式左邊Y項爲偶數,可以被2整除,因此等式左邊不存在浮點數取整的問題,等式可以轉化爲:Y^2 - Y + 2 * X = 2 * Sum. 可以知道函數f(Y) = Y^2 - Y + 2 * X(X爲常數)爲單調增函數;f(X) = 2 * X + Y^2 - Y(Y爲常數)也是單調增函數。因此嵌套遍歷X,Y來求解所有可能解時,可以將f(X,Y) = (2 * X  + Y - 1) * Y  >  2 * Sum 時的X,Y作爲雙層遍歷結束的位置,而X,Y的遍歷長度分別由公式中Y = 2及X = 1求得相應的X和Y值。

時間複雜度:此方法在不同輸入Sum下根據遍歷X和Y的長度,可以得到時間複雜度爲:O(nsqrt(n))。而採用暴力破解法求解此題,時間複雜度爲O(n^2)。

分析:算法存在數據溢出問題,Sum值過大時計算中間結果可能超出所用數據類型的取值範圍,同時要注意數組長度要放得下所有的解。

算法C實現:

/**
 * @file find_continue_numbers_formula.c
 * @brief find continue numbers that their sum are the input 64-bit value.
 * @author [email protected]
 * @version 1.0
 * @date 2015-02-03
 */

#include <stdlib.h>
#include <stdio.h>
#include <math.h>
// #define NDEBUG
#include <assert.h>

// #define NDBG_PRINT
#include "debug_print.h"

typedef unsigned long TYPE;

#define MAX_COUNT      10000000
TYPE array[MAX_COUNT] = {0};

/**
 * @brief find continue numbers that their sum are the input value.
 *
 *
 * @param[in]     sum    the sum of the continue numbers
 * @param[out]    array  array that save the first number value and
 * count one by one
 *
 * @return numbers of the solutions
 */
TYPE find_continue_numbers(TYPE sum, TYPE* array)
{
    assert(array != NULL && sum >= 1);
    TYPE count = 0;
    TYPE i, j;
    TYPE sum_temp;
    TYPE number_count_max = (TYPE)(-1 + sqrt(1 + (sum << 3))) >> 1;
    TYPE number_first_max = (TYPE)(sum - 1) >> 1;
    DEBUG_PRINT_VALUE("%lu", number_count_max);
    DEBUG_PRINT_VALUE("%lu", number_first_max);
    for (i = 1; i <= number_first_max; i++) {
        for (j = 2; j <= number_count_max; j++) {
            sum_temp = (((i << 1) + j - 1) * j) >> 1;
            DEBUG_PRINT_VALUE("%lu", sum_temp);
            if (sum_temp == sum) {
                array[count << 1] = i;
                array[(count << 1) + 1] = j;
                count++;
            } else if (sum_temp > sum) {
                break;
            }
        }
    }
    return count;
}

int main(void) {
    /// read sum
    TYPE sum = 0;
    printf("input the sum of the continue numbers:");
    if(scanf("%lu", &sum) != 1) {
        DEBUG_PRINT_STATE;
        DEBUG_PRINT_STRING("can not get the right value.\n");
        DEBUG_PRINT_VALUE("%lu", sum);
        fflush(stdout);
        assert(0);
        exit(EXIT_FAILURE);
    }
    /// output result
    TYPE i, j, count;
    if((count = find_continue_numbers(sum, array)) != 0) {
        printf("Solution count = %lu\n", count);
        for (i = 0; i < count; i++) {
            for (j = 0; j < array[(i << 1) + 1] - 1; j++) {
                printf("%lu + ", array[i << 1] + j);
            }
            printf("%lu = %lu\n", array[i << 1] + array[(i << 1) + 1] - 1, sum);
        }
    } else {
        printf("Can not get solutions.\n");
    }
    return EXIT_SUCCESS;
}

拓展:這篇文章(地址:  http://blog.sina.com.cn/s/blog_4024c0000100vk4l.html)也是採用了這種數學分析方式,區別之處在於他的解採用的遍歷上述的Y值,而對X值不進行遍歷,只考慮對應Y值和Sum值下求解出來的X是否爲整數解。通過判斷解是否爲整數把二層遍歷求解二元方程轉化爲單層遍歷求解,雖然用到了浮點運算,含有精度問題,但是時間複雜度降低爲O(sqrt(n))。

下面是根據這個思路的算法C實現:

/**
 * @brief find continue numbers that their sum are the input value.
 *
 *
 * @param[in]     sum    the sum of the continue numbers
 * @param[out]    array  array that save the first number value and
 * count one by one
 *
 * @return numbers of the solutions
 */
TYPE find_continue_numbers(TYPE sum, TYPE* array)
{
    assert(array != NULL && sum >= 1);
    TYPE count = 0;
    TYPE j;
    TYPE number_count_max = (TYPE)(-1 + sqrt(1 + (sum << 3))) >> 1;
    long double i_float, i_diff;
    DEBUG_PRINT_VALUE("%ld", number_count_max);
    for (j = 2; j <= number_count_max; j++) {
        DEBUG_PRINT_VALUE("%ld", j);
        i_float = ((long double)sum * 2.0 / j + 1 - j) / 2;
        i_diff = i_float - (unsigned long)i_float;
        DEBUG_PRINT_VALUE("%Lf", i_float);
        DEBUG_PRINT_VALUE("%Lf", i_diff);
        DEBUG_PRINT_VALUE("%ld", (unsigned long)i_float);
        if (i_diff < DBL_EPSILON && i_float > DBL_EPSILON) {
            array[count << 1] = (unsigned long)i_float;
            array[(count << 1) + 1] = j;
            count++;
        }
    }
    return count;
}


  解法二 數學分析(因式分解)求解

問題分析:

  輸出:這裏將問題的一個解表示爲連續自然數中第一個數s和隨後一個數e,那麼連續自然數的長度爲(e - s + 1)。這種表示方法和上面解法一的表示方法不同。

這種表示方法下,有Sum = (s + e) * (e - s + 1) / 2。令 2 * Sum =  x * y,且 s = (x - y + 1) / 2,e = (x + y - 1) / 2,x > y。我們可以發現,因爲s和e爲整數,因此x和y必須一奇數一偶數,因爲 2 * Sum 已含有偶數因子2,Sum中還必須含有奇數因子才能表示爲2 * Sum = x * y的形式。問題二可以發現當Sum = 2^n(n = 1,2,3,..,63)時問題無解,因爲此時Sum不含奇數因子。

這種分析思路進行求解時,我們可以將2 * Sum進行因式分解,其中所有的因子2必須都在x或y中,且x,y並不能同時含有。分解後可以令x = 2^i * 3^j1 * 5^k1 ...,y = 3^(j - j1) * 5^(k - k1)...以及x = 3^j1 * 5^k1 ...,y = 2^i * 3^(j - j1) * 5^(k - k1)...(考慮偶數因子位於x和y中兩種情況)這些組合中,當x > y時得到的解x,y再轉化爲s和e求得到了問題的最終解了。

三、解法——問題2

問題分析:

根據問題一的表示,這樣的數字Sum表示等式(2 * X + Y - 1)  * Y= 2 * Sum(X >= 1,Y >= 2,X和Y爲整數)無解。我們遍歷所有的Sum,可以發現,程序無解時,Sum的值總可以表示爲2^n(n = 1,2,3,4...63)。這裏的證明可見上面問題一因式分解解法的分析。

四、解法——問題3

問題分析:

我們利用問題一二元方程求解來進行分析:對於較大的Sum,我們要儘量使Y最大,可以令X = 1,此時求解的是:(1 + Y) * Y / 2 = Sum,滿足Sum <= 2^64 - 1且Y = Y_max(所有解中最大的Y)時的Sum值。等式可得:Y = (-1 + sqrt(1 + 8 * Sum)) / 2,因爲Y爲整數所以sqrt(1 + 8 * Sum)的解必須爲某個奇數Z的平方。現在問題轉化爲:求解滿足(1 + 8 * Sum)爲一奇數Z平方的最大的Sum,可以得到Sum = (Z^2 - 1) / 8。由Sum <= 2^64 - 1可以得到Z <= sqrt((2^64 - 1) * 2^3 + 1),通過計算器求得滿足此不等式最大的奇數Z = 12148001999,此時有:Y = (-1 + Z) / 2 = 6074000999,Sum =  (Z^2 - 1) / 8 = 1.8446744e+19




發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章