【算法】将正整数表示为平方数之和
来源:互联网 发布:常州市软件行业协会 编辑:程序博客网 时间:2024/06/14 11:57
问题来源
Timus Online Judge 网站上有这么一道题目:1073. Square Country。这道题目的输入是一个不大于 60,000 的正整数,要求计算出该正整数最少能够使用多少个正整数的平方和来表示。这道题目的时间限制是 1 秒。
问题解答
《数论导引(第5版)》([英]G.H.Hardy、E.M.Wright 著,人民邮电出版社,2008年10月第1版)第 320 页有以下定理:
定理 369(Lagrange 定理): 每个正整数都是四个平方数之和
在这个定理中,平方数是指整数(包括零)的平方。所以,我们有以下 C 语言程序(1073.c):
// http://acm.timus.ru/problem.aspx?space=1&num=1073
#include <stdio.h>
#include <math.h>
int
compute(
int
n)
{
int
i, j, k, m = 4;
int
i0 = n / 4, i2 = n, j2, k2;
for
(i =
sqrt
(n); i2 > i0; i--)
if
((j2 = n - (i2 = i * i)) == 0)
return
1;
else
for
(j =
sqrt
(j2); j > 0; j--)
if
((k2 = n - i2 - j * j) == 0)
return
2;
else
if
(k =
sqrt
(k2), k * k == k2 && m > 3) m = 3;
return
m;
}
int
main(
void
)
{
int
n;
scanf
(
"%d"
, &n);
printf
(
"%d"
, compute(n));
return
0;
}
上述程序中:
- 第 7 行设置 m 的初值为 4,代表一个正整数最多只需要四个平方数就可以表示了。
- 第 9 行开始的主循环决定第一个平方数,如果 n 刚好是平方数(第 10 行),就直接返回 1。
- 第 11 行开始的内循环决定第二个平方数,如果这两个数加起来刚好等于 n (第 12 行),就直接返回 2。
- 第 13 行检查 n 是否可以表示为三个平方数的和,如果是的话,就更新 m 的值为 3 。注意,此时不能直接返回 3,因为可能在后面的循环中发现 n 可以用两个平方数表示。
- 第 14 行返回 m 值(只可能是 3 或者 4)作为最后的答案。
上述程序在 Timus Online Judge 网站的运行时间是 0.015 秒。
更好的算法
上述题目有一个进一步的版本:1593. Square Country. Version 2,输入改为不大于 1015 的正整数,时间限制还是 1 秒。上一节的程序做以下改动:
- 第 5 行的第 2 个 int 改为 long long
- 第 8 和 19 行的 int 改为 long long
- 第 20 行的 %d 改为 %lld
就可以适用于这道题目,但是运行结果是“Time limit exceeded”。此时,需要更好的算法。我们有以下 C 语言程序(1593.c):
// http://acm.timus.ru/problem.aspx?space=1&num=1593
#include <stdio.h>
#include <math.h>
int
compute(
long
long
n)
{
int
i, k;
long
long
i2;
while
((n & 3) == 0) n >>= 2;
if
((n & 7) == 7)
return
4;
for
(i = 8, i2 = 9; i2 <= n; i2 += i += 8)
while
(n % i2 == 0) n /= i2;
if
(n == 1)
return
1;
if
((n & 1) == 0) n >>= 1;
if
((n & 3) == 3)
return
3;
for
(k =
sqrt
(n), i = 3; i <= k && n % i; i += 4) ;
return
(i > k) ? 2 : 3;
}
int
main(
void
)
{
long
long
n;
scanf
(
"%lld"
, &n);
printf
(
"%d"
, compute(n));
return
0;
}
在上述程序中:
- 第 9 行消去 n 的所有值为 4 因数。
- 第 10 行检测 n 是否为 8m + 7 的形式,如是,直接返回 4 (请参见下节)。
- 第 11、12 行消去 n 的所有素因子的偶次幂(素因子 2 的偶次幂已经在第 9 行消去了)。
- 第 11 行中 i2 依次为:32、52、72、...、t2,这是因为 (t + 1)2 - (t - 1)2 = 4t,每次循环 t 增加 2,所以 i 增加 4 * 2 = 8。
- 第 13 行,如果 n 等于 1,说明输入是个完全平方数,直接返回 1。
- 此时,n 的标准分解式中所有的素因子都是一次幂了。
- 第 14 行消去 n 的素因子 2 (如果有的话)。
- 第 16 行的循环中 i 从 3 开始,每次递增 4,以检查 n 是否有 4m + 3 形式的因子。
- 第 15 行和第 17 行根据定理 366 决定答案是两个还是三个平方之和。
这个程序在 Timus Online Judge 网站的运行时间是 0.828 秒。这道题目的最佳运行时间是 0.031 秒,不知道使用什么算法可以这么快。
上述算法的原理
《数论导引(第5版)》第 329 页说:
第 318 页有以下定理:
定理 366: 一个数 n 是两个平方之和,当且仅当在 n 的标准分解式中,它的所有形如 4m + 3 的素因子都有偶次幂
我们还有以下定理:
形如 4m + 3 的整数有形如 4m + 3 的素因子
列出平方数
前面的 1593.c 程序只能给出答案是几个平方数之和,而对这些平方数是什么一无所知。而 1073.c 程序倒是中规中矩地想要求解这些平方数是什么,但是从 Lagrange 定理得知最多只要四个平方数就够了,所以该程序只求解到三个平方数的情况,其余情况下答案肯定是 4 了。因此,我们将 1073.c 稍做修改,得到 1073b.c 用于列出这些平方数,如下所示:
#include <stdio.h>
#include <stdlib.h>
#include <math.h>
static
int
a[5];
int
compute(
int
n)
{
int
i, j, k, l, m = 5;
int
i0 = n / 4, i2 = n, j2, k2, l2;
for
(i =
sqrt
(n); i2 > i0; i--)
if
((j2 = n - (i2 = i * i)) == 0)
return
a[0] = i, 1;
else
for
(j =
sqrt
(j2); j > 0; j--)
if
((k2 = n - i2 - (j2 = j * j)) == 0)
return
a[0] = i, a[1] = j, 2;
else
for
(k =
sqrt
(k2); k > 0; k--)
if
((l2 = n - i2 - j2 - (k2 = k * k)) == 0 && m > 3)
a[0] = i, a[1] = j, a[2] = k, m = 3;
else
if
(l =
sqrt
(l2), l * l == l2 && m > 4)
a[0] = i, a[1] = j, a[2] = k, a[3] = l, m = 4;
return
m;
}
int
main(
int
args,
char
* argv[])
{
int
i, n, start = 1, count = 16, k;
if
(args > 1) start =
atoi
(argv[1]);
if
(args > 2) count =
atoi
(argv[2]);
for
(n = start; n < start + count; n++)
{
k = compute(n);
printf
(
"%d:%6d:"
, k, n);
for
(i = 0; i < k; i++)
printf
(
" %d"
, a[i]);
puts
(k > 4 ?
" Error!"
:
""
);
}
return
0;
}
上述程序中:
- 第 5 行的全局静态数组用于记录所求的平方数,数组大小为 5, 而不是 4,是为了防止程序有 bug 时造成数组下标越界(第 32 行)。
- 第 9 行将 m 的初值从 4 改为 5,用以检测程序是否有 bug。
- 第 9、10 行增加了变量 l 和 l2 用于计算第四个平方数,并相应增加一层循环(第 15 行)。
- 第 12、14、17 和 19 行相应记录这些平方数于数组 a 中。
- 第 33 行在输出时检查程序是否有 bug。如果 k > 4 程序肯定有问题,违反了 Lagrange 定理。当然,k <= 4 并不意味着程序就没有问题了。:)
这个程序的运行结果如下所示:
E:\work> 1073b1: 1: 12: 2: 1 13: 3: 1 1 11: 4: 22: 5: 2 13: 6: 2 1 14: 7: 2 1 1 12: 8: 2 21: 9: 32: 10: 3 13: 11: 3 1 13: 12: 2 2 22: 13: 3 23: 14: 3 2 14: 15: 3 2 1 11: 16: 4E:\work> 1073b 100001 93:100001: 316 12 13:100002: 316 11 53:100003: 315 27 73:100004: 316 12 23:100005: 316 10 73:100006: 311 57 64:100007: 315 27 7 23:100008: 314 34 162:100009: 315 28E:\work> 1073b 9876543:987654: 991 58 474:987655: 993 39 9 22:987656: 734 6703:987657: 992 53 283:987658: 993 40 33:987659: 991 67 333:987660: 986 110 583:987661: 990 75 443:987662: 993 38 134:987663: 993 38 13 12:987664: 992 603:987665: 993 40 43:987666: 992 59 113:987667: 993 33 233:987668: 992 60 22:987669: 990 87E:\work>
如果不知道 Lagrange 定理,也就是说,假设我们不知道要多少个平方数之和才够的话,这道题目看来只好用动态规划算法来求解了。
使用递归求解
键盘农夫园友在 47 楼的评论中介绍了他的随笔“华丽的递归——将正整数表示为平方数之和”。我将该随笔中的 C 语言程序改写如下(1073c.c):
// http://acm.timus.ru/problem.aspx?space=1&num=1073
#include <stdio.h>
typedef
int
bool
;
const
bool
true
= 1;
const
bool
false
= 0;
bool
isSquare(
int
n,
int
v,
int
k)
{
return
(n < v) ?
false
: (n == v) ?
true
: isSquare(n, v + k + 2, k + 2);
}
bool
isSquareSum(
int
n,
int
m,
int
v,
int
k)
{
if
(n < v)
return
false
;
if
(m == 1)
return
isSquare(n, v, k);
return
isSquareSum(n - v, m - 1, v, k) ?
true
: isSquareSum(n, m, v + k + 2, k + 2);
}
int
compute(
int
n,
int
m)
{
return
isSquareSum(n, m, 1, 1) ? m : compute(n, m + 1);
}
int
main(
void
)
{
int
n;
scanf
(
"%d"
, &n);
printf
(
"%d"
, compute(n, 1));
return
0;
}
这个程序本质上和键盘农夫园友的程序是没有区别的。分析如下:
- 第 9 到 12 行的 isSquare 函数判断 n 是否是不小于 v 的完全平方数。其中 k 是用于计算平方数的辅助变量。
- 第 14 到 19 行的 isSquareSum 函数判断 n 是否是 m 个不小于 v 的平方数之和。其中 k 是用于计算平方数的辅助变量。
- 第 21 到 24 行的 compute 函数计算正整数 n 最少可以表示为多少个平方数之和。
上述程序在 Timus Online Judge 网站的运行时间是 0.031 秒,而第一小节中的 1073.c 的运行时间是 0.015 秒。
如果将上述程序作如下改动:
- 第 9 行的前两个 int 改为 long long
- 第 14 行的第 1 个和第 3 个 int 改为 long long
- 第 21 行的第 2 个 int 改为 long long
- 第 28 行的 int 改为 long long
- 第 29 行的 %d 改为 %lld
就可以适用于“1593. Square Country. Version 2”,但是运行结果是“Crash (stack overflow)”。
- 【算法】将正整数表示为平方数之和
- 【算法】将正整数表示为平方数之和
- [正整数划分]将正整数n表示一系列正整数之和
- Lintcode697-判断是否为平方数之和
- 判断是否为平方数之和-LintCode
- 一个正整数有可能可以被表示为n(n>=2)个连续正整数之和--算法求解
- 每天一到算法练习题1 -- 一个正整数有可能可以被表示为 n(>=2) 个连续正整数之和
- 算法爱好者——判断某整数是否为两数平方之和 ? 待解决
- 【基本算法】拆分为连续正整数之和
- 将一个数表示为几个n次方的数之和
- 每一个正整数可以表示为3个三角形数之和
- 判断某整数是否为两数平方之和
- LintCode第697题目:判断是否为平方数之和
- 将一个正整数n表示成一系列的正整数之和,求共有几种划分方法
- 一个偶数总能表示为两个素数之和。要求:输入一个偶数,将其表示成两个素数之和,直到输入的数为0。
- 求解一个正整数有可能可以被表示为 n个连续正整数之和
- 一个正整数表示为n(n>=2)个连续正整数之和!
- 将一个正整数分解为多个连续整数之和
- 杭电 1969 Pie 二分法 附翻译 解题思路
- 浅谈优化程序性能(上)
- 课堂笔记11
- Wrong number of segments in token:
- android知识图谱
- 【算法】将正整数表示为平方数之和
- 7.1线程2015/8/2
- 生产者消费者模式
- ios block回调实现方法
- 我和程序员打了13年交道后,为什么做了缘创派?
- 算法速成(7)图操作
- 使用厄拉多塞筛法计算素数的和
- C# I/O 助手类
- 银行卡输入自动断开 如1111 2222 3333 444