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; }
上述程序中:
上述程序在 Timus Online Judge 网站的运行时间是 0.015 秒。
上述题目有一个进一步的版本:1593. Square Country. Version 2,输入改为不大于 1015 的正整数,时间限制还是 1 秒。上一节的程序做以下改动:
就可以适用于这道题目,但是运行结果是“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; }
在上述程序中:
这个程序在 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; }
上述程序中:
这个程序的运行结果如下所示:
E:\work> 1073b 1: 1: 1 2: 2: 1 1 3: 3: 1 1 1 1: 4: 2 2: 5: 2 1 3: 6: 2 1 1 4: 7: 2 1 1 1 2: 8: 2 2 1: 9: 3 2: 10: 3 1 3: 11: 3 1 1 3: 12: 2 2 2 2: 13: 3 2 3: 14: 3 2 1 4: 15: 3 2 1 1 1: 16: 4 E:\work> 1073b 100001 9 3:100001: 316 12 1 3:100002: 316 11 5 3:100003: 315 27 7 3:100004: 316 12 2 3:100005: 316 10 7 3:100006: 311 57 6 4:100007: 315 27 7 2 3:100008: 314 34 16 2:100009: 315 28 E:\work> 1073b 987654 3:987654: 991 58 47 4:987655: 993 39 9 2 2:987656: 734 670 3:987657: 992 53 28 3:987658: 993 40 3 3:987659: 991 67 33 3:987660: 986 110 58 3:987661: 990 75 44 3:987662: 993 38 13 4:987663: 993 38 13 1 2:987664: 992 60 3:987665: 993 40 4 3:987666: 992 59 11 3:987667: 993 33 23 3:987668: 992 60 2 2:987669: 990 87 E:\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; }
这个程序本质上和键盘农夫园友的程序是没有区别的。分析如下:
上述程序在 Timus Online Judge 网站的运行时间是 0.031 秒,而第一小节中的 1073.c 的运行时间是 0.015 秒。
如果将上述程序作如下改动:
就可以适用于“1593. Square Country. Version 2”,但是运行结果是“Crash (stack overflow)”。
更多的 ACM 题的解法请参见:Timus 目录。