数学 - 数值映射

116

我该如何将介于a和b之间的数字进行线性映射,使它们落在c和d之间。

也就是说,我希望2到6之间的数字映射到10到20之间...但我需要一个通用的方法。

我的脑子已经崩溃了。


5
两点式。线性方程的两点式是指通过给定的两个点 $(x_1,y_1)$ 和 $(x_2,y_2)$ 的坐标来表示一条直线的方程。该方程可以写成 $y-y_1=\frac{y_2-y_1}{x_2-x_1}(x-x_1)$ 或者等价形式 $y=\frac{y_2-y_1}{x_2-x_1}(x-x_1)+y_1$。其中,斜率 $\frac{y_2-y_1}{x_2-x_1}$ 是两点间直线的斜率,截距 $y_1$ 则是直线和 $y$ 轴相交的位置。 - kennytm
10个回答

239

如果你的数字X落在区间A和B之间,并且你希望数字Y落在区间C和D之间,那么你可以应用以下线性变换:

Y = (X-A)/(B-A) * (D-C) + C

这应该可以满足您的需求,尽管您的问题有点模糊,因为您也可以将区间映射到相反的方向。只要注意避免除以零,您就应该没问题。


21
为了明确起见,我喜欢将new_value = (old_value - old_bottom) / (old_top - old_bottom) * (new_top - new_bottom) + new_bottom;翻译为:新数值 = (旧数值 - 旧下限)/(旧上限 - 旧下限)*(新上限 - 新下限)+ 新下限; - ftrotter
2
这个方程有没有推导出处? - shaveenk
1
@shaveenk,它应该是一条直线的方程,其中 Y=f(X)=m*X+b,其中 m 和 b 是同时从以下两个约束方程中确定的,这些方程是通过在所需端点处替换 X 和 Y 的值得出的:C=m*A+bD=m*B+b - Chris Chiasson
我最终需要使用 X=A+(A-B)*t 来证明这种方法与Peter的方法的等价性。 t 本质上是X的无量纲化表示。(t=(X-A)/(A-B) - Chris Chiasson
2
为了反转方向,公式是 ((X-A)/(A-B) * (C-D)) * -1 +D。 - Corey Levinson

23

将两个范围的大小取比率,然后减去您初始范围的起始值,乘以比率,再加上您第二个范围的起始值。换句话说,

R = (20 - 10) / (6 - 2)
y = (x - 2) * R + 10
这将在第二个范围内均匀分布来自第一个范围的数字。

这个不行。我的范围是从1000000000到9999999999,而数字可以从1到999999999。 - Dejell
@Odelya 当然可以。这只是一个足够简单的数学转换。你只需要使用足够大的数字类型(bignum或类似的类型)。你的数字对于32位整数来说太大了,但是例如64位整数就可以工作。 - Konrad Rudolph
它们的类型是double。double R = (20 - 10) / (6 - 2); double y = (X - 2) * R + 10; - Dejell
@Odelya 同样的问题。你应该了解浮点精度。事实上,这是必读的:计算机科学家应该知道的浮点运算知识 - 如果你需要一个具有如此大数字的浮点类型,你可能需要使用任意精度数类型 - Konrad Rudolph
你能推荐一个我可以用的Java类型吗? - Dejell
@Odelya 我链接的文章提到了相关的Java类 - Konrad Rudolph

10

希望java.lang.Math类中有这个功能,因为这是一个广泛需要的函数,并且在其他语言中可用。 以下是一个简单的实现:

final static double EPSILON = 1e-12;

public static double map(double valueCoord1,
        double startCoord1, double endCoord1,
        double startCoord2, double endCoord2) {

    if (Math.abs(endCoord1 - startCoord1) < EPSILON) {
        throw new ArithmeticException("/ 0");
    }

    double offset = startCoord2;
    double ratio = (endCoord2 - startCoord2) / (endCoord1 - startCoord1);
    return ratio * (valueCoord1 - startCoord1) + offset;
}

我把这段代码放在这里,作为以后参考的依据,或许也能帮助其他人。


3

3
作为旁注,这与经典的将摄氏度转换为华氏度的问题相同,您希望将一个等于0-100(C)的数字范围映射到32-212(F)。

这怎么算是一个答案? - shinzou
1
这是一个应用问题的示例。许多人在计算机科学入门课程中都会遇到这个简单的问题,但并没有考虑到解决方案可以推广到其他问题。我试图为原始问题添加上下文。原始问题已经得到了充分的回答。 - Metro

2
除了@PeterAllenWebb的回答之外,如果您想要将结果反转回来,请使用以下方法:
reverseX = (B-A)*(Y-C)/(D-C) + A

1

如果你的范围是[a到b],并且你想要将其映射到[c到d],其中x是你想要映射的值,使用这个公式(线性映射)

double R = (d-c)/(b-a)
double y = c+(x*R)+R
return(y)

1

其中X是从A-B映射到C-D的数字,而Y是结果: 采用线性插值公式,lerp(a,b,m)=a+(m*(b-a)),并将CD放在ab的位置上得到Y=C+(m*(D-C))。然后,将(X-A)/(B-A)代替m,得到Y=C+(((X-A)/(B-A))*(D-C))。这是一个可以接受的映射函数,但可以简化。将(D-C)部分放入被除数中,得到Y=C+(((X-A)*(D-C))/(B-A))。这给我们另一个可以简化的部分,(X-A)*(D-C),它等于(X*D)-(X*C)-(A*D)+(A*C)。将其代入,你会得到Y=C+(((X*D)-(X*C)-(A*D)+(A*C))/(B-A))。下一步需要做的是添加+C位。为此,你需要将C乘以(B-A),得到((B*C)-(A*C)),并将其移入被除数中,得到Y=(((X*D)-(X*C)-(A*D)+(A*C)+(B*C)-(A*C))/(B-A))。这是冗余的,包含一个+(A*C)和一个-(A*C),它们相互抵消。删除它们,你会得到一个最终结果:Y=((X*D)-(X*C)-(A*D)+(B*C))/(B-A)

简而言之:标准的映射函数,Y=C+(((X-A)/(B-A))*(D-C)),可以简化为Y=((X*D)-(X*C)-(A*D)+(B*C))/(B-A)


1

第一个范围上的每个单位区间在第二个范围上占据了(d-c)/(b-a)的"空间"。

伪代码:

var interval = (d-c)/(b-a)
for n = 0 to (b - a)
    print c + n*interval

如何处理舍入取决于您。


0
int srcMin = 2, srcMax = 6;
int tgtMin = 10, tgtMax = 20;

int nb = srcMax - srcMin;
int range = tgtMax - tgtMin;
float rate = (float) range / (float) nb;

println(srcMin + " > " + tgtMin);
float stepF = tgtMin;
for (int i = 1; i < nb; i++)
{
  stepF += rate;
  println((srcMin + i) + " > " + (int) (stepF + 0.5) + " (" + stepF + ")");
}
println(srcMax + " > " + tgtMax);

当然要检查除以零的情况。


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