对一个数组应用的最小正乘数,使得数组变为整数

4
给定一个由n个非负元素组成的数组,是否有任何C/C++库中的函数可以返回应用于数组每个元素时返回整数的最小正乘数?
例如,如果n=2的数组是1.66667, 2.33333,则乘数为3。当我们将数组的每个元素乘以3时,我们得到5, 7,都是整数。
如果数组是8,10,则乘数为0.5。这将给我们4,5。
(1)在任何知名的库(如boost、eigen等)中是否有有效的函数来实现此操作?
(2)如果库中没有可用的函数,那么找出多重的有效算法是什么?

4
“2.33333 * 3”的结果是“6.99999”,而不是“7”。 - Barmar
不,没有这样的功能。你需要自己编写。 - Barmar
4
似乎是一个X-Y问题... - user2736738
3
至少对于浮点数类型来说,我认为这是不可能的,因为浮点数对象无法准确表示一个实数。也许你可以使用有理数类型,并基于此类型实现该函数。 - xskxzr
1
浮点数可以看作是整数 * 2^指数。因此,如果您的乘数应为整数,则应寻找2的幂次方。 - Marc Glisse
请注意,没有以数字5结尾的小数分数可以被表示为某个二的幂次方的整数:您的输入数据是不精确的,但有少数例外。 - greybeard
3个回答

6
在一般情况下,您的问题没有好的解决方案,因为值以浮点格式存储,具有有限的精度,并且只能精确存储分母为2的幂的分数。例如,在您的平台上,0.1 * 10 可能不是一个整数值。
如果您从整数量计算数组中的值,则应将它们表示为带有足够大小的整数对的归一化分数,并计算其分母的最小公倍数。
如果您想要一个近似解决方案,则可以指定epsilon的值,并手动设计解决方案。我无法想到任何库函数来满足此需求,但是暴力解决方案很容易编写:
unsigned long long ullgcd(unsigned long long a, unsigned long long b) {
     /* compute the greatest common divisor using Euclid's elegant method */
     if (a < b)
         return ullgcd(b, a);
     else
     if (b == 0)
         return a;
     else
         return ullgcd(b, a % b);
}

double least_multiple(double *a, size_t n, double epsilon) {
    for (double mult = 1;; mult += 1) {
        size_t i;
        unsigned long long div = 0;
        for (i = 0; i < n; i++) {
            double d = fabs(a[i] * mult);
            unsigned long long v = round(d);
            if (fabs(v - d) > epsilon)
                break;
            div = ullgcd(v, div);
        }
        if (i == n)
            break;
    }
    /* mult is the smallest non zero integer that fits your goal.
       the smallest multiplier is obtained by dividing it 
       by the greatest common divisor of the resulting integer array.
    */
    return mult / div;
}

但无论如何,我们是否可以通过使用一个小于一的值来判断接近整数?例如,在上面的例子中,如果我有一个0.0001的小数,那么在我的OP示例中,1.6667和2.3333应该分别四舍五入为4和5,因为它们非常接近整数。 - Tryer
如果您想要一个问题的近似解,您可以指定epsilon的值,并手动设计一个解决方案。我想不到任何库函数来满足这个需求。 - chqrlie
感谢您提供的代码,但是那样行不通。请注意对于(8,10),倍数应该是0.5。这是您的代码所忽略的。我怀疑需要使用某种二分法等方法。 - Tryer
@Tryer,你认真的吗?给定(8,10),你想要0.5吗?8和10已经是整数了,所以它们实际上符合你的要求,不是吗?无论如何,你必须明白浮点数是实数的差异数字,因此精确计算是不可能的。 - Jean-Baptiste Yunès
@Jean-BaptisteYunès 问题是,我希望得到的整数数组可以在数学程序中使用,在这种情况下,对于数值稳定性来说,尽可能小的系数可能很重要。例如,给定(3,4.5)。上面的代码将返回2(结果数组为(6,9))。然而,0.67乘数将产生一个“更小的数组”(2,3)。 - Tryer
3
@Tryer:我忽略了“最小正乘数”的限制。我已经更新了代码,但请注意,“最小正”乘数始终为0.0,您可能想要的是“最小严格正”乘数。 - chqrlie

4
不,按照您指定的方式不行。问题在于小数值1.66667和2.33333没有这样的乘数:您假设的是一种近似值,这种近似值源自于从数学角度来看是任意舍入策略。
此外,还有浮点属性需要担心,所以您可以排除使用双精度或浮点类型。
您最好在此处使用分数类来表示数字。然后,任何常见的乘数都可以通过一些简单的数学计算消除。
请参见http://www.boost.org/doc/libs/1_64_0/libs/rational/index.html

谢谢,不幸的是,一些数组元素将成为另一个返回一般浮点数的程序的输出。您知道Boost是否可以有效地使用分数来近似任意双精度值吗? - Tryer
@Tryer:这样的功能会太“基于个人观点”或“用户依赖”,所以不行。你需要自己构建这一部分,取决于你能容忍多少误差。从 rational 类开始,尝试构建适合自己的构造方法,享受其中的乐趣吧。 - Bathsheba

1
请看Rosetta Code的"将十进制数转换为有理数"。由于我不熟悉C语言,所以我将那里的C代码转换为Python(尽管我认为Python可能有一些相关的库),并添加了几个函数,您可以轻松地将它们适应到C中。如果分数转换中的分母全部为1.0,则我们将数字列表的最大公约数除以它。否则,我们返回唯一分母的乘积。
import math
import operator

def f(arr):
  epsilon = 4
  fractions = [rat_approx(i, 16**epsilon) for i in arr]

  # The denominators in the fraction conversion are all 1.0
  if sum([denom for (num, denom) in fractions]) == len(fractions):
    return 1.0 / gcd_of_many([num for (num, denom) in fractions])
  else:
    # Otherwise, return the product of unique denominators
    return reduce(operator.mul, set([denom for (num, denom) in fractions]), 1)

def gcd(a, b):
  if a < b:
    return gcd(b, a)
  elif b == 0:
    return a;
  else:
    return gcd(b, a % b)

def gcd_of_many(arr):
  result = arr[0]
  for i in xrange(1, len(arr)):
    result = gcd(result, arr[i])
  return result

# Converted from
# https://rosettacode.org/wiki/Convert_decimal_number_to_rational#C
def rat_approx(f, md):
    #  a: continued fraction coefficients.
    h = [0, 1, 0]
    k = [1, 0, 0]
    n = 1
    neg = 0
    num = 0
    denom = 0

    if md <= 1:
      denom = 1
      num = f
      return num, denom

    if f < 0:
      neg = 1
      f = -f

    while f != math.floor(f):
      n <<= 1
      f *= 2

    d = f

    # continued fraction and check denominator each step
    for i in xrange(65):
        a = d // n if n else 0
        if i and not a:
          break

        x = d
        d = n
        n = x % n

        x = a;
        if k[1] * a + k[0] >= md:
            x = (md - k[0]) // k[1]
            if x * 2 >= a or k[1] >= md:
                i = 65
            else:
                break

        h[2] = x * h[1] + h[0]
        h[0] = h[1]
        h[1] = h[2]
        k[2] = x * k[1] + k[0]
        k[0] = k[1]
        k[1] = k[2]

    denom = k[1]
    num = -h[1] if neg else h[1]

    return (num, denom)

输出:

   f([8, 10])
=> 0.5
   f([1.66667, 2.33333])
=> 3.0

如果n=2,连分数(对于a[1]/a[0])确实是正确的近似工具。对于更大的n,情况变得混乱,因为搜索“同时有理逼近”,“多维连分数”等会显示出来。 - Marc Glisse
@MarcGlisse谢谢您的评论。您能否提供一个(相当短的)输入示例,以引发“混乱”的过程/结果? - גלעד ברקן
我的意思是,在数学上证明某种最优性对于n=2来说并不难,但对于更大的n可能是错误的,因为你是独立逼近每个数字的(你缩小到相同分母的方式也很奇怪,但我只是匆匆一瞥了代码,所以不会声称什么)。 - Marc Glisse

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接