您的位置:首页 > 职场人生

《编程之美》学习笔记——2.21只考加法的面试题

2015-01-26 14:05 316 查看
一、问题

我们知道:

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 chenxilinsidney@gmail.com
* @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
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: