【算法】将正整数表示为平方数之和

问题来源

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):


1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

// 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):


1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

// 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 页说:

n ≠ 4a(8m + 7) 是 n 可以用三个平方数表示的一个充分必要条件

第 318 页有以下定理:

定理 366: 一个数 n 是两个平方之和,当且仅当在 n 的标准分解式中,它的所有形如 4m + 3 的素因子都有偶次幂

我们还有以下定理:

形如 4m + 3 的整数有形如 4m + 3 的素因子

列出平方数

前面的 1593.c 程序只能给出答案是几个平方数之和,而对这些平方数是什么一无所知。而 1073.c 程序倒是中规中矩地想要求解这些平方数是什么,但是从 Lagrange 定理得知最多只要四个平方数就够了,所以该程序只求解到三个平方数的情况,其余情况下答案肯定是 4 了。因此,我们将 1073.c 稍做修改,得到 1073b.c 用于列出这些平方数,如下所示:


1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

#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> 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):


1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

// 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)”。

版权声明:本文为博主http://www.zuiniusn.com原创文章,未经博主允许不得转载。

时间: 2024-10-12 00:00:44

【算法】将正整数表示为平方数之和的相关文章

633. 平方数之和

633. 平方数之和 题目描述 给定一个非负整数 c ,你要判断是否存在两个整数 a 和 b,使得 a2 + b2 = c. 示例1: 输入: 5 输出: True 解释: 1 * 1 + 2 * 2 = 5 示例2: 输入: 3 输出: False 贴出代码 class Solution { public boolean judgeSquareSum(int c) { int i = 0, j = (int)Math.sqrt(c); while(i <= j){ int powSum = i

C#刷遍Leetcode面试题系列连载(4):No.633 - 平方数之和

上篇文章中一道数学问题 - 自除数,今天我们接着分析 LeetCode 中的另一道数学题吧~ 今天要给大家分析的面试题是 LeetCode 上第 633 号问题, Leetcode 633 - 平方数之和 https://leetcode.com/problems/sum-of-square-numbers/ 题目描述 给定一个非负整数 c ,你要判断是否存在两个整数 a和 b,使得 \(a^2 + b^2 = c\). 示例1: 输入: 5 输出: True 解释: 1 * 1 + 2 * 2

算法练习:两指针之三数之和为0

问题描述 给出一个整型数组,找出所有三个元素的组合,其组合之和等于0.要求在结果集里不含有重复的组合. 举例: 输入{-2, 1, -1, 2, 1} 输出{-2, 1, 1 } 问题分析 最容易想到的是穷举法,挑选第一个元素,然后在其后挑选第二个元素,再从除已经挑选出的两个元素之外挑第三个元素,判断三者之和是否为0:第二种想到的是用回溯递归,这两种方法的时间复杂度均为O(n^3),可参阅代码部分关于这两种方法的实现. 那有没有复杂度低一些的呢,答案是有的,就是使用两指针的方法,从而使复杂度下降

LeetCode 633. Sum of Square Numbers平方数之和 (C++)

题目: Given a non-negative integer c, your task is to decide whether there're two integers a and b such that a2 + b2 = c. Example 1: Input: 5 Output: True Explanation: 1 * 1 + 2 * 2 = 5 Example 2: Input: 3 Output: False 分析: 给定一个非负整数c ,你要判断是否存在两个整数a和b,使

[LeetCode] Sum of Square Numbers 平方数之和

Given a non-negative integer c, your task is to decide whether there're two integers a and b such that a2 + b2 = c. Example 1: Input: 5 Output: True Explanation: 1 * 1 + 2 * 2 = 5 Example 2: Input: 3 Output: False s

力扣(LeetCode)平方数之和 个人题解

给定一个非负整数 c ,你要判断是否存在两个整数 a 和 b,使得 a2 + b2 = c. 示例1: 输入: 5 输出: True 解释: 1 * 1 + 2 * 2 = 5 示例2: 输入: 3 输出: False 在这题里面,可以使用二分查找来缩小搜索的范围 由数学定理(我忘了具体的哪个定义)可知,a和b的具体取值范围落在0到根号c之间.然后简单运用二分法就能十分便捷找到答案了. 代码如下: class Solution { public: bool judgeSquareSum(int

正整数n拆分成几个不同的平方数——DFS&amp;&amp;打表

考虑将正整数n拆分成几个不同的平方数之和,比如30=1^2 + 2^2 + 5^2=1^2 + 2^2 + 3^2 + 4^2,而8不存在这样的拆分. 1 #include<bits/stdc++.h> 2 using namespace std; 3 4 const int maxn = 1000 + 10; 5 bool vis[maxn]; 6 vector<int>res; 7 8 bool dfs(int n) 9 { 10 //printf("%d\n&quo

算法练习:两数之和

题目:给定一个整型数组,是否能找出两个数使其和为指定的某个值?注:整型数组中不存在相同的数. 一.解题方法 1.暴力破解法(时间复杂度O(n^2) ) 这是最容易想到的一种方法,即使用两层循环,从数组里取出一个数,然后在此数之后部分找出另外一个数,计算两数之和,判断是否等于指定值.如下: //直观的办法,使用两个循环 bool IsExistSumOfTwoNum( int nArray[], int nCount, int nSum ) { bool bRet = false; for ( i

[算法] LeetCode 1.两数之和

LeetCode 1.两数之和(python) 1.朴素解法 最朴素的两个for循环大法: class Solution: def twoSum(self, nums: List[int], target: int) -> List[int]: for i in range(len(nums)): for j in range(i+1,len(nums)): if nums[i] + nums[j] == target: return [i, j] 但注意,不要用enumerate函数写,会超时