银河

SKYIV STUDIO

  博客园 :: 首页 ::  ::  :: 订阅 订阅 :: 管理 ::
  222 随笔 :: 2 文章 :: 2263 评论 :: 48 引用

问题来源

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

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 用于列出这些平方数,如下所示:

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

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


更多的 ACM 题的解法请参见:Timus 目录

posted on 2011-11-26 23:28 银河 阅读(1351) 评论(49) 编辑 收藏

评论

#1楼 2011-11-27 00:24 glshader      
"每个正整数都是四个平方数之和"

如果该定理成立, 那么, 0.031秒难道是做了一个表, 然后查表得的?
 回复 引用 查看   

#2楼[楼主] 2011-11-27 08:35 银河      
@glshader
引用每个正整数都是四个平方数之和

这是 Lagrange 定理,《数论导引(第5版)》在第20章“用两个或四个平方和表示数”中已经用三种不同的方法证明了这个定理。所以该定理一定是成立的。
 回复 引用 查看   

#3楼[楼主] 2011-11-27 08:48 银河      
@glshader
引用那么, 0.031秒难道是做了一个表, 然后查表得的?

因为在这道题目中 n 最大可达到 10^15,所以也不可能做这么大的一张表。这需要大约 1 PB 的内存才行。

SI perfix
peta: 10^15
tera: 10^12
giga: 10^9
mega: 10^6
kilo: 10^3
 回复 引用 查看   

#4楼 2011-11-27 11:06 glshader      
哦, 我看错了, 我看成上面那题了。

不大于60000的正整数
 回复 引用 查看   

#5楼[楼主] 2011-11-27 11:18 银河      
在正文的最后增加了“列出平方数”小节。
 回复 引用 查看   

#6楼[楼主] 2011-11-27 11:21 银河      
@glshader
第一道题目1073. Square Country最佳运行时间是 0.001 秒。由于在这道题目中 n 的最大值为 60,000,这有可能使用查表法。
 回复 引用 查看   

#7楼 2011-11-27 13:21 garbageMan      
引用for (i = sqrt(n); i2 > i0; i--)

这个写法是错的
 回复 引用 查看   

#8楼[楼主] 2011-11-27 13:49 银河      
@garbageMan
请问这个写法错在哪里?
首先,这个写法语法上没有错误。1073.c 和 1073b.c 在 Windows 操作系统和 Linux 操作系统的以下 C/C++ 编译器下通过编译:

用于 80x86 的 Microsoft (R) 32 位 C/C++ 优化编译器 16.00.40219.01 版
版权所有(C) Microsoft Corporation。保留所有权利。

gcc (Ubuntu/Linaro 4.6.1-9ubuntu3) 4.6.1
Copyright © 2011 Free Software Foundation, Inc.
本程序是自由软件;请参看源代码的版权声明。本软件没有任何担保;
包括没有适销性和某一专用目的下的适用性担保。

其次,这个写法在语义上也是正确的。1073.c 在 Timus Online Judge 网站上提交的结果是:Accepted
 回复 引用 查看   

#9楼 2011-11-27 14:55 garbageMan      
@银河
sqrt()的原型是
double sqrt(double);
这表明sqrt()这个函数的值就一般意义来说是一个近似值
没有任何根据能使得我们认定sqrt(16.0)的值是正好为4.还是比这个值稍微大一点或小一点
 回复 引用 查看   

#10楼[楼主] 2011-11-27 15:31 银河      
@garbageMan
我当然知道 sqrt 的原型是
double sqrt(double)
虽然说 sqrt 这个函数的值就一般意义来说是一个近似值。
但是,对于不太大的完全平方数,sqrt 的值就是准确值。
可以肯定地说,sqrt(16.0) 的值正好就是 4.0。
请注意,C 语言标准要求在 float.h 中定义的 DBL_EPSILON 的值不差于 1E-9,DBL_DIG 的值至少是 10。实际上,在主流的 C/C++ 编译器中,这两个值分别是 2.220446E-16 和 15。
所以,这就保证了不太大的整数的平方根的准确性,至少对这道题目来说是足够用了。
 回复 引用 查看   

#11楼[楼主] 2011-11-27 17:31 银河      
http://ideone.com/jH1dT
 回复 引用 查看   

#12楼 2011-11-27 18:02 garbageMan      
@银河
引用但是,对于不太大的完全平方数,sqrt 的值就是准确值。
可以肯定地说,sqrt(16.0) 的值正好就是 4.0。

依据?
 回复 引用 查看   

#13楼 2011-11-27 18:05 garbageMan      
@银河
引用C 语言标准要求在 float.h 中定义的 DBL_EPSILON 的值不差于 1E-9,DBL_DIG 的值至少是 10。实际上,在主流的 C/C++ 编译器中,这两个值分别是 2.220446E-16 和 15。
所以,这就保证了不太大的整数的平方根的准确性,至少对这道题目来说是足够用了。

这和主题无关
i = sqrt(n)
是把 double 赋值给int
并不遵守四舍五入规则
 回复 引用 查看   

#14楼[楼主] 2011-11-27 18:19 银河      
@garbageMan
这正好和主题有关。我也没有要求在把 double 赋值给 int 遵守四舍五入的规则。
只要 DBL_EPSILON 的精度足够,对于不太大的整数来说,sqrt 就不会有误差,也用不着四舍五入,直接截断就行了。
请你用以下程序测试一下就知道了:
#include <stdio.h>
#include <stdlib.h>
#include <math.h>

int main(int args, char* argv[])
{
  int m, n = 16;
  double tmp;
  if (args > 1) n = atoi(argv[1]);
  m = tmp = sqrt(n);
  printf("sqrt(%d) = %d (%f)\n", n, m, tmp);
}

这个程序的一个运行结果足以说明问题:
E:\work> testSqrt 100000000
sqrt(100000000) = 10000 (10000.000000)
 回复 引用 查看   

#15楼 2011-11-27 18:49 garbageMan      
引用只要 DBL_EPSILON 的精度足够,对于不太大的整数来说,sqrt 就不会有误差

这有因果关系吗?

引用直接截断就行了。

既然截断,怎么会没有误差呢?
 回复 引用 查看   

#16楼 2011-11-27 18:51 garbageMan      
@银河
你的测试只说明了,对于你的编译器以及你测试的这个数据没有误差
总不能看见一只羊是白的就说全世界的羊都是白的吧
 回复 引用 查看   

#17楼[楼主] 2011-11-27 19:52 银河      
@garbageMan
我说的是:“只要 DBL_EPSILON 的精度足够,对于不太大的整数来说,sqrt 就不会有误差”,也就是说,在 DBL_EPSILON 的精度范围内,没有误差。超出 DBL_EPSILON 精度范围,还是有误差的。
固然不能看见一只羊是白的,就说全世界的羊都是白的。但是看见一只乌鸦是黑的,也不能说全世界的乌鸦就都是黑的。实际上全世界的乌鸦还就都是黑的。
testSqrt 程序固然不能证明我的论点,而是只能提供一些支持我的论点的论据。而 1073.c 在 Timus Online Judge 网站上 Accepted 对我的论点也是一个支持,虽然也还不能证明我的论点。
但是 DBL_EPSILON 和 DBL_DIG 这些量就可以证明我的论点,它们本来的意义就是用于我的论点的。
如若不然,请你举出一个反例,随便什么 C/C++ 编译器,随便什么不太大的整数,使用 sqrt 函数出现了误差,只要一个反例就足够反驳我的论点了。
 回复 引用 查看   

#18楼 2011-11-27 20:11 garbageMan      
引用银河:
@garbageMan
我说的是:“只要 DBL_EPSILON 的精度足够,对于不太大的整数来说,sqrt 就不会有误差”

这个逻辑不通
DBL_EPSILON的存在本身就是说明可能会有误差
根本无法用来说明“不会有误差”

引用银河:
而 1073.c 在 Timus Online Judge 网站上 Accepted 对我的论点也是一个支持,虽然也还不能证明我的论点。

在我看来这不说明什么
很多Online Judge都很垃圾

引用银河:
如若不然,请你举出一个反例,随便什么 C/C++ 编译器,随便什么不太大的整数,使用 sqrt 函数出现了误差,只要一个反例就足够反驳我的论点了。

请你注意我在9楼的看法“没有任何根据能使得我们认定sqrt(16.0)的值是正好为4.还是比这个值稍微大一点或小一点 ”
从逻辑上来说我根本不需要举例
我的看法基于double数据类型的本质和定义
以及没有编译器保证sqrt不会出现误差的事实
 回复 引用 查看   

#19楼 2011-11-27 20:15 yuk.lin      
这是人家证明过的定理然后用C语言实现吧
 回复 引用 查看   

#20楼 2011-11-27 20:19 好坏      
@garbageMan
你的思维太混乱了,我觉得楼主已经解释的很清楚的了,你没有必要为了反驳而反驳了
 回复 引用 查看   

#21楼[楼主] 2011-11-27 20:20 银河      
@garbageMan
引用DBL_EPSILON的存在本身就是说明可能会有误差

不错,但是 DBL_EPSILON 的本意就是为了控制误差。在 DBL_EPSILON 精度允许范围内可以说没有误差,否则 DBL_EPSILON 这个量就没有存在的意义了。
 回复 引用 查看   

#22楼[楼主] 2011-11-27 20:22 银河      
@garbageMan
引用在我看来这不说明什么
很多Online Judge都很垃圾

另外,不要随便说别人很垃圾,特别是著名的网站、公司等等很垃圾。首先要检讨自己是不是很垃圾。
 回复 引用 查看   

#23楼[楼主] 2011-11-27 20:23 银河      
引用yuk.lin:这是人家证明过的定理然后用C语言实现吧

是的,这是根据《数论导引(第5版)》中证明过的定理,使用 C 语言实现相应的算法。
 回复 引用 查看   

#24楼[楼主] 2011-11-27 20:27 银河      
@garbageMan
引用以及没有编译器保证sqrt不会出现误差的事实

任何合格的 C/C++ 编译器都必须保证 sqrt 在 DBL_EPSILON 的精度范围内不会出现误差,不然谁敢用这个编译器?
 回复 引用 查看   

#25楼 2011-11-27 20:34 garbageMan      
引用银河:
@garbageMan
引用在我看来这不说明什么
很多Online Judge都很垃圾

另外,不要随便说别人很垃圾,特别是著名的网站、公司等等很垃圾。首先要检讨自己是不是很垃圾。

只有垃圾才需要不断检讨自己是不是很垃圾
我说他们垃圾是因为他们就是垃圾
 回复 引用 查看   

#26楼 2011-11-27 20:36 garbageMan      
@银河
引用银河:
@garbageMan
引用DBL_EPSILON的存在本身就是说明可能会有误差

不错,但是 DBL_EPSILON 的本意就是为了控制误差。在 DBL_EPSILON 精度允许范围内可以说没有误差,否则 DBL_EPSILON 这个量就没有存在的意义了。

逻辑有问题是不能当程序员的
int i = 2.0 - DBL_EPSILON/2;
2.0 - DBL_EPSILON/2 在不在精度范围内
难道 i 会被赋值为2吗
 回复 引用 查看   

#27楼 2011-11-27 20:38 garbageMan      
引用银河:
@garbageMan
引用以及没有编译器保证sqrt不会出现误差的事实

任何合格的 C/C++ 编译器都必须保证 sqrt 在 DBL_EPSILON 的精度范围内不会出现误差,不然谁敢用这个编译器?

武断
压根没有的事情
你彻底误解了DBL_EPSILON 的含义
 回复 引用 查看   

#28楼[楼主] 2011-11-27 20:43 银河      
@garbageMan
引用int i = 2.0 - DBL_EPSILON/2;
2.0 - DBL_EPSILON/2 在不在精度范围内
难道 i 会被赋值为2吗

虽然你举的这个例子和我们讨论的问题没有关系,但是,i 还真的会被赋值为 2。不信你随便找个 C/C++ 编译器试试看。
 回复 引用 查看   

#29楼[楼主] 2011-11-27 20:44 银河      
@garbageMan
引用你彻底误解了DBL_EPSILON 的含义

你才彻底误解了 DBL_EPSILON 的含义。
 回复 引用 查看   

#30楼 2011-11-27 20:51 garbageMan      
引用银河:
@garbageMan
引用你彻底误解了DBL_EPSILON 的含义

你才彻底误解了 DBL_EPSILON 的含义。


好吧
你赢了
 回复 引用 查看   

#31楼 2011-11-27 23:17 yuk.lin      
因为编译器的精度辩论的那么多楼,没必要吧。

就整数问题而言我们可以不考虑浮点数精度吧
 回复 引用 查看   

#32楼 2011-11-28 08:58 陳胡図      
引用银河:
@garbageMan
@银河
固然不能看见一只羊是白的,就说全世界的羊都是白的。但是看见一只乌鸦是黑的,也不能说全世界的乌鸦就都是黑的。实际上全世界的乌鸦还就都是黑的。

我插个题外话,是有白色的乌鸦的。
 回复 引用 查看   

#33楼[楼主] 2011-11-28 09:24 银河      
@yuk.lin
引用就整数问题而言我们可以不考虑浮点数精度吧

整数问题还真的要考虑浮点数的精度。
就求整数的平方根问题而言,如果只使用整数运算的话,应该是象下面这样的:
int sqrtInt(int n)
{
  int i = 0;
  while (i * i <= n) i++;
  return i - 1;
}

这个算法的时间复杂度是 O(n)。而使用 C 语言标准库函数
double sqrt(double x)
的话,时间复杂度是 O(1)。

此外,大整数的乘法如果使用普通的算法来计算的话,时间复杂度是 O(N^2),而使用快速傅里叶变换的话,时间复杂度是 O(N logN)。而快速傅里叶变换要用到复数,在计算机中也就要用到浮点数,也必须考虑浮点数的精度。具体的计算可参考我在2008年7月写的以下两篇随笔:
BigArithmetic - 提供任意精度的算术运算的静态类
再谈 BigInteger - 使用快速傅里叶变换

类似的例子还有很多,比如,费马大定理谈论的是整数问题,但是要证明它就需要涉及到代数几何中的椭圆曲线和模形式等高深的内容。
 回复 引用 查看   

#34楼[楼主] 2011-11-28 09:30 银河      
@陳胡図
引用我插个题外话,是有白色的乌鸦的。

长见识了。谢谢!
 回复 引用 查看   

#35楼[楼主] 2011-11-28 09:35 银河      
@yuk.lin
引用因为编译器的精度辩论的那么多楼,没必要吧。

也不是我想辩论这么多楼,而是 garbageMan 园友说我的程序中使用 sqrt 函数来求整数的平方根是错误的,而我要他举一个反例,他最终也没有给出一个反例。
引用如若不然,请你举出一个反例,随便什么 C/C++ 编译器,随便什么不太大的整数,使用 sqrt 函数出现了误差,只要一个反例就足够反驳我的论点了。
 回复 引用 查看   

#36楼[楼主] 2011-11-28 09:57 银河      
@garbageMan
引用我的看法基于double数据类型的本质和定义

的确, double 数据类型本质上是不精确的,所以需要 DBL_EPSILON 和 DBL_DIG 来规范 dobule 数据类型的精确度。也就是说,在 DBL_EPSILON 精度范围内 double 数据类型必须是精确的,超过这个范围才是不精确的。
而你是仅仅看到了 double 数据类型不精确的本质,而没有看到 double 数据类型必须在一定精度范围内精确。不然,double 数据类型还有什么用处?
按照你的说法,极端地说,反正 double 数据类型是不精确的,sqrt(2.0) 返回 1.04787 之类的值也是正常的。如果是这样,我们程序员还如何来写程序?
 回复 引用 查看   

#37楼 2011-11-28 19:33 garbageMan      
引用double sqrt(double x)
的话,时间复杂度是 O(1)。

这种想法太天真了
别忘了sqrt()本身也有时间复杂度
 回复 引用 查看   

#38楼 2011-11-28 19:37 garbageMan      
@银河
引用银河:
@garbageMan
引用我的看法基于double数据类型的本质和定义

的确, double 数据类型本质上是不精确的,所以需要 DBL_EPSILON 和 DBL_DIG 来规范 dobule 数据类型的精确度。也就是说,在 DBL_EPSILON 精度范围内 double 数据类型必须是精确的,超过这个范围才是不精确的。
而你是仅仅看到了 double 数据类型不精确的本质,而没有看到 double 数据类型必须在一定精度范围内精确。不然,double 数据类型还有什么用处?
按照你的说法,极端地说,反正 double 数据类型是不精确的,sqrt(2....

从你这段言论看
你对数值计算几乎一无所知
并且你混淆了一个概念
把DBL_EPSILON这个浮点数本身的性质当作了数值计算最大误差
我有理由相信
你自己从来没计算过平方根
并且你也不知道应该怎样设计一个计算平方根的函数
 回复 引用 查看   

#39楼 2011-11-29 12:06 键盘农夫      
这个题目根本不必也根本不应该用到浮点类型数据
 回复 引用 查看   

#40楼[楼主] 2011-12-02 19:25 银河      
可以使用以下命令:
$ cpp -dM /dev/null
查看 gcc 预定义的宏
 回复 引用 查看   

#41楼[楼主] 2011-12-03 10:21 银河      
@garbageMan
引用我有理由相信
你自己从来没计算过平方根
并且你也不知道应该怎样设计一个计算平方根的函数

你的理由是不成立的。请参阅我在2008年7月写的以下两篇随笔:
BigArithmetic - 提供任意精度的算术运算的静态类
再谈 BigInteger - 使用快速傅里叶变换
在前一篇随笔里,使用牛顿迭代法实现了 BigArithmetic.Sqrt 方法。
在后一篇随笔里,利用 BigArithmetic.Sqrt 方法实现了 BigInteger.Sqrt 方法。
 回复 引用 查看   

#42楼[楼主] 2011-12-03 10:32 银河      
@garbageMan
引用从你这段言论看
你对数值计算几乎一无所知
并且你混淆了一个概念
把DBL_EPSILON这个浮点数本身的性质当作了数值计算最大误差

你说我“对数值计算几乎一无所知”也是不正确的,还是请参阅我在2008年7月写的以下两篇随笔:
BigArithmetic - 提供任意精度的算术运算的静态类
再谈 BigInteger - 使用快速傅里叶变换
而且正是你不知道或者误解了 DBL_EPSILON,没有正确地认识到 double 数据类型必须在 DBL_EPSILON 规定的范围内保证其精确度。而这正是数值计算的基础。否则也无从利用快速傅里叶变换来实现大整数的运算了。正如我在“使用快速傅里叶变换计算大整数乘法”中提到的:
由于快速傅里叶变换是采用了浮点运算,因此我们需要足够的精度,以使在出现舍入误差时,结果中每个组成部分的准确整数值仍是可辨认的。长度为 N 的 B 进制数可产生大到 B^2 * N 阶的卷积分量。我们知道,双精度浮点数的尾数是 53 个二进位,所以:
2 x log2 B + log2 N + 几个 x log2 log2 N < 53
上式中左边最后一项是为了快速傅里叶变换的舍入误差。
所以,为了能够计算尽量大的整数,一般 B 不会取得太大。在计算机程序中经常使用 256 进制进行运算。但是如果经常需要将计算结果和十进制互相转换,则往往使用 100 进制进行运算。
 回复 引用 查看   

#43楼[楼主] 2011-12-03 10:37 银河      
引用键盘农夫:这个题目根本不必也根本不应该用到浮点类型数据

这个题目完全可以使用浮点类型数据。
因为直接使用 C 标准库中的 dobule sqrt(double) 函数非常优雅和高效。如若不然,就要使用类似 33 楼的评论中的 sqrtInt 函数了,这不够优雅,也不高效。
 回复 引用 查看   

#44楼[楼主] 2011-12-03 10:46 银河      
引用garbageMan:
引用double sqrt(double x)
的话,时间复杂度是 O(1)。

这种想法太天真了
别忘了sqrt()本身也有时间复杂度

我知道 sqrt 本身也有时间复杂度,但是在我的印象中,C 标准库中的数学函数,如 sqrt、exp、log、sin 等,时间复杂应该是 O(1)。这里可能是我错了。有那位大侠知道目前使用的 C 标准库中的这些数学函数的时间复杂是多少,请在评论中告诉我。谢谢!
 回复 引用 查看   

#45楼[楼主] 2011-12-03 10:48 银河      
另外,还必须更正一下,33 楼的 sqrtInt 函数的时间复杂度应该是 O(n ^ 1/2),而不是 O(n)。
 回复 引用 查看   

#46楼[楼主] 2011-12-03 11:57 银河      
我认为 C 语言标准库中的数学函数,如 sqrt、exp、log、sin 等,时间复杂应该是 O(1)。因为我认为计算这些数学函数的值的时间和双精度浮点数的指数大小无关,只和双精度浮点数的尾数的精度有关。而双精度浮点数的尾数是 53 个二进位,是个常数。所以时间复杂度应该是 O(1)。也就是说,计算
sqrt(1.0)
sqrt(2.225074E-308)
sqrt(1.797693E+308)
等等的值所需的时间是差不多的,即:计算 sqrt(x) 所需的时间和 x 的值的大小无关,而是和 x 的精度有关。
不知道我的看法是否正确,请各位大侠指正。谢谢!
 回复 引用 查看   

#47楼 2011-12-04 09:35 键盘农夫      
@银河
http://www.cnblogs.com/KBTiller/archive/2011/12/04/2275232.html
 回复 引用 查看   

#48楼[楼主] 2011-12-04 15:15 银河      
@键盘农夫
根据你的随笔,我在正文最后增加了一小节:使用递归求解。
 回复 引用 查看   

#49楼 2011-12-04 19:43 键盘农夫      
引用银河:
@键盘农夫
根据你的随笔,我在正文最后增加了一小节:使用递归求解。

不胜荣幸!
 回复 引用 查看