c# - 将值包装到 [min,max] 范围内而不除法

标签 c# c#-4.0

在 C# 中是否有任何方法可以将给定值 x 包装在 x_min 和 x_max 之间。该值不应像 Math.Min/Max 中那样被限制,而是像 float 模数一样包裹。

实现这个的方法是:

x = x - (x_max - x_min) * floor( x / (x_max - x_min));

但是,我想知道是否有一种算法或 C# 方法可以在不除法的情况下实现相同的功能,并且不会出现当值远离所需范围时可能出现的浮点精度受限问题。

最佳答案

您可以使用两个模运算将其包装起来,这仍然等同于除法。如果不对 x 做一些假设,我认为没有比这更有效的方法了。

x = (((x - x_min) % (x_max - x_min)) + (x_max - x_min)) % (x_max - x_min) + x_min;

公式中的附加总和和模数用于处理 x 实际上小于 x_min 并且模数可能为负数的情况。或者您可以使用 if 和单个模块化划分来执行此操作:

if (x < x_min)
    x = x_max - (x_min - x) % (x_max - x_min);
else
    x = x_min + (x - x_min) % (x_max - x_min);

除非 xx_minx_max 不远,并且可以用很少的加法或减法达到(也可以考虑错误传播),我认为模是你唯一可用的方法。

无除法

记住错误传播可能变得相关,我们可以用一个循环来做到这一点:

d = x_max - x_min;
if (abs(d) < MINIMUM_PRECISION) {
    return x_min; // Actually a divide by zero error :-)
}
while (x < x_min) {
    x += d;
}
while (x > x_max) {
    x -= d;
}

关于概率的注释

模运算的使用有一些统计意义(浮点运算也有不同的意义)。

例如,假设我们将 0 到 5 之间的随机值(例如六面骰子结果)包装到 [0,1] 范围内(即抛硬币)。然后

0 -> 0      1 -> 1
2 -> 0      3 -> 1
4 -> 0      5 -> 1

如果输入具有平坦谱,即每个数字 (0-5) 都有 1/6 的概率,则输出也将是平坦的,并且每个项目将有 3/6 = 50% 的概率。

但是如果我们有一个五面骰子 (0-4),或者如果我们有一个介于 0 和 32767 之间的随机数并希望将它减少到 (0, 99) 范围内以获得百分比,输出将不是平坦的,有些数字会比其他数字稍微(或不是那么小)更有可能。在五面骰子到掷硬币的情况下,正面与反面的概率为 60%-40%。在 32767 百分比的情况下,低于 67 的百分比将是 CEIL(32767/100)/FLOOR(32767/100) = 0.3%,比其他百分比出现的可能性高。

(为了更清楚地看到这一点,考虑从“00000”到“32767”的数字:每 328 次掷一次,数字的前三位数字将是“327”。发生这种情况时,最后两位数字只能从“00”到“67”,它们不能从“68”到“99”,因为 32768 超出范围。因此,从 00 到 67 的数字更有可能。

因此,如果想要平坦的输出,就必须确保 (max-min) 是输入范围的除数。在 32767 和 100 的情况下,输入范围必须截断为最接近的百位(减一),即 32699,以便 (0-32699) 包含 32700 个结果。每当输入 >= 32700 时,必须再次调用输入函数以获得新值:

function reduced() {
#ifdef RECURSIVE
    int x = get_random();
    if (x > MAX_ALLOWED) {
        return reduced(); // Retry
    }
#else
    for (;;) {
        int x = get_random();
        int d = x_max - x_min;
        if (x > MAX_ALLOWED) {
            continue; // Retry
        }
    }
#endif
    return x_min + (
             (
               (x - x_min) % d
             ) + d
           ) % d;

当 (INPUTRANGE%OUTPUTRANGE)/(INPUTRANGE) 很重要时,开销可能相当大(例如,将 0-197 减少到 0-99 需要进行大约两倍的调用)。

如果输入范围小于输出范围(例如,我们有一个掷硬币器并且我们想掷骰子),请使用 Horner 算法根据需要多次相乘(不要相加)以获得输入范围更大。掷硬币的范围是 2,CEIL(LN(OUTPUTRANGE)/LN(INPUTRANGE)) 是 3,所以我们需要三个乘法:

for (;;) {
    x = ( flip() * 2 + flip() ) * 2 + flip();
    if (x < 6) {
        break;
    }
}

或者从掷骰子中得到一个介于 122 和 221(范围 = 100)之间的数字:

for (;;) {
    // ROUNDS = 1 + FLOOR(LN(OUTPUTRANGE)/LN(INPUTRANGE)) and can be hardwired
    // INPUTRANGE is 6
    // x = 0; for (i = 0; i < ROUNDS; i++) { x = 6*x + dice();  }
    x = dice() + 6 * ( 
            dice() + 6 * ( 
                dice() /* + 6*... */
            )
        );
    if (x < 200) {
        break;
    }
}
// x is now 0..199, x/2 is 0..99
y = 122 + x/2;

关于c# - 将值包装到 [min,max] 范围内而不除法,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/14415753/

相关文章:

c# - 如何使 WPF 数据网格的第一行成为自动添加新行的行

WPF 绑定(bind)渲染 Gui 进度

c# - C#访问父对象的方法

c# - 为什么 Nullable<T> 不是有效的自定义属性参数,而 T 是?

asp.net - 将 S-JIS 字符串解码为 UTF-8

c# - Generic 基类继承自 Generic Interface

wpf - 将模型的变化列表同步到 ViewModel 的 ObservableList 的最佳实践?

c# - 在子列表中传递带有过滤器/组的对象

c# - Roslyn:如何在 Visual Studio 之外加载现有项目

c# - Entity Framework DbContext 未使用依赖注入(inject)进行处理