13

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

实现这一点的一种方法是:

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

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

4

9 回答 9

20

您可以使用两个模运算来包装它,这仍然等效于除法。我认为没有更有效的方法可以做到这一点而不假设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);

除非离andx不远,并且可以通过很少的加法或减法(也可以考虑错误传播),我认为模数是您唯一可用的方法。x_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 需要进行大约两倍的调用)。

如果输入范围小于输出范围(例如,我们有一个抛硬币机,我们想掷骰子),使用霍纳算法乘(不加)所需的次数,以获得更大的输入范围。抛硬币的范围是 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;
于 2013-01-19T16:04:44.217 回答
9

模数在浮点上工作得很好,那么如何:

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

尽管如此,它仍然是一个有效的分界线,你需要调整它的值小于 < min ...

当数字远离范围时,您会担心准确性。然而,这与模运算无关,但是它是执行的,但它是浮点的属性。如果你取一个 0 到 1 之间的数字,然后给它加上一个大常数,比如把它带入 100 到 101 的范围内,它会失去一些精度。

于 2013-01-19T15:27:34.177 回答
1

最小值和最大值是固定值吗?如果是这样,您可以提前计算出它们的范围和其倒数:

const decimal x_min = 5.6m;
const decimal x_max = 8.9m;
const decimal x_range = x_max - x_min;
const decimal x_range_inv = 1 / x_range;

public static decimal WrapValue(decimal x)
{
    return x - x_range * floor(x * x_range_inv);
}

乘法的性能应该比除法好一些。

于 2013-01-19T16:08:44.937 回答
0
x = x<x_min?  x_min:
    x>x_max?  x_max:x;

它有点令人费解,您绝对可以将其分解为一对 if 语句。但我认为没有必要从一开始就进行除法。

编辑:

我好像误会了,le

x = x<x_min?  x_max - (x_min - x):
    x>x_max?  x_min + (x - x_max):x;

如果您的 x 值变化不大,这将起作用。这可能取决于用例。否则,对于更强大的版本,我希望您至少需要除法或重复(递归?)减法。

这应该是一个更健壮的版本,它会继续执行上述计算,直到 x 稳定。

int x = ?, oldx = x+1; // random init value.

while(x != oldx){
    oldx = x;
    x = x<x_min?  x_max - (x_min - x):
        x>x_max?  x_min + (x - x_max):x;
}
于 2013-01-19T15:22:49.553 回答
0

如何在IComparable.

public static class LimitExtension
{
    public static T Limit<T>(this T value, T min, T max)
         where T : IComparable
    {
        if (value.CompareTo(min) < 0) return min;
        if (value.CompareTo(max) > 0) return max;

        return value;
    }
}

还有一个单元测试:

public class LimitTest
{
    [Fact]
    public void Test()
    {
        int number = 3;

        Assert.Equal(3, number.Limit(0, 4));
        Assert.Equal(4, number.Limit(4, 6));
        Assert.Equal(1, number.Limit(0, 1));

    }
}
于 2013-01-19T15:28:35.773 回答
0

LinqPad 示例代码(限制为 3 位小数)

void Main()
{ 
    Test(int.MinValue, 0, 1,0.1f, "value = int.MinValue");
    Test(int.MinValue, -2,- 1,0.1f, "value = int.MinValue");
    Test(int.MaxValue, 0, 1,0.1f, "value = int.MaxValue");
    Test(int.MaxValue, -2,- 1,0.1f, "value = int.MaxValue");
    Test(-2,-2,-1,0.1f, string.Empty);
    Test(0,0,1,0.1f, string.Empty);
    Test(1,1,2,0.1f, string.Empty);

    Test(int.MinValue, 0, 1, -0.1f, "value = int.MinValue");
    Test(int.MinValue, -2,- 1, -0.1f, "value = int.MinValue");
    Test(int.MaxValue, 0, 1, -0.1f, "value = int.MaxValue");
    Test(int.MaxValue, -2,- 1, -0.1f, "value = int.MaxValue");
    Test(-2,-2,-1, -0.1f, string.Empty);
    Test(0,0,1, -0.1f, string.Empty);
    Test(1,1,2, -0.1f, string.Empty);
}

private void Test(float value, float min ,float max, float direction, string comment)
{
    "".Dump("    " + min + " to " + max + " direction = " + direction + "   " + comment);
    for (int i = 0; i < 11; i++)
    {
        value = (float)Math.Round(min + ((value - min) % (max - min)), 3); 
        string.Format("    {1} -> value: {0}", value,  i).Dump(); 
        value = value + direction < min && direction < 0 ? max + direction : value + direction;
    }
} 

结果

0 to 1 direction = 0.1   value = int.MinValue

0 -> value: 0
1 -> value: 0.1
2 -> value: 0.2
3 -> value: 0.3
4 -> value: 0.4
5 -> value: 0.5
6 -> value: 0.6
7 -> value: 0.7
8 -> value: 0.8
9 -> value: 0.9
10 -> value: 0

-2 to -1 direction = 0.1   value = int.MinValue

0 -> value: -2
1 -> value: -1.9
2 -> value: -1.8
3 -> value: -1.7
4 -> value: -1.6
5 -> value: -1.5
6 -> value: -1.4
7 -> value: -1.3
8 -> value: -1.2
9 -> value: -1.1
10 -> value: -2

0 to 1 direction = 0.1   value = int.MaxValue

0 -> value: 0
1 -> value: 0.1
2 -> value: 0.2
3 -> value: 0.3
4 -> value: 0.4
5 -> value: 0.5
6 -> value: 0.6
7 -> value: 0.7
8 -> value: 0.8
9 -> value: 0.9
10 -> value: 0

-2 to -1 direction = 0.1   value = int.MaxValue

0 -> value: -2
1 -> value: -1.9
2 -> value: -1.8
3 -> value: -1.7
4 -> value: -1.6
5 -> value: -1.5
6 -> value: -1.4
7 -> value: -1.3
8 -> value: -1.2
9 -> value: -1.1
10 -> value: -2

-2 to -1 direction = 0.1   

0 -> value: -2
1 -> value: -1.9
2 -> value: -1.8
3 -> value: -1.7
4 -> value: -1.6
5 -> value: -1.5
6 -> value: -1.4
7 -> value: -1.3
8 -> value: -1.2
9 -> value: -1.1
10 -> value: -2

0 to 1 direction = 0.1   

0 -> value: 0
1 -> value: 0.1
2 -> value: 0.2
3 -> value: 0.3
4 -> value: 0.4
5 -> value: 0.5
6 -> value: 0.6
7 -> value: 0.7
8 -> value: 0.8
9 -> value: 0.9
10 -> value: 0

1 to 2 direction = 0.1   

0 -> value: 1
1 -> value: 1.1
2 -> value: 1.2
3 -> value: 1.3
4 -> value: 1.4
5 -> value: 1.5
6 -> value: 1.6
7 -> value: 1.7
8 -> value: 1.8
9 -> value: 1.9
10 -> value: 1

0 to 1 direction = -0.1   value = int.MinValue

0 -> value: 0
1 -> value: 0.9
2 -> value: 0.8
3 -> value: 0.7
4 -> value: 0.6
5 -> value: 0.5
6 -> value: 0.4
7 -> value: 0.3
8 -> value: 0.2
9 -> value: 0.1
10 -> value: 0

-2 to -1 direction = -0.1   value = int.MinValue

0 -> value: -2
1 -> value: -1.1
2 -> value: -1.2
3 -> value: -1.3
4 -> value: -1.4
5 -> value: -1.5
6 -> value: -1.6
7 -> value: -1.7
8 -> value: -1.8
9 -> value: -1.9
10 -> value: -2

0 to 1 direction = -0.1   value = int.MaxValue

0 -> value: 0
1 -> value: 0.9
2 -> value: 0.8
3 -> value: 0.7
4 -> value: 0.6
5 -> value: 0.5
6 -> value: 0.4
7 -> value: 0.3
8 -> value: 0.2
9 -> value: 0.1
10 -> value: 0

-2 to -1 direction = -0.1   value = int.MaxValue

0 -> value: -2
1 -> value: -1.1
2 -> value: -1.2
3 -> value: -1.3
4 -> value: -1.4
5 -> value: -1.5
6 -> value: -1.6
7 -> value: -1.7
8 -> value: -1.8
9 -> value: -1.9
10 -> value: -2

-2 to -1 direction = -0.1   

0 -> value: -2
1 -> value: -1.1
2 -> value: -1.2
3 -> value: -1.3
4 -> value: -1.4
5 -> value: -1.5
6 -> value: -1.6
7 -> value: -1.7
8 -> value: -1.8
9 -> value: -1.9
10 -> value: -2

0 to 1 direction = -0.1   

0 -> value: 0
1 -> value: 0.9
2 -> value: 0.8
3 -> value: 0.7
4 -> value: 0.6
5 -> value: 0.5
6 -> value: 0.4
7 -> value: 0.3
8 -> value: 0.2
9 -> value: 0.1
10 -> value: 0

1 to 2 direction = -0.1   

0 -> value: 1
1 -> value: 1.9
2 -> value: 1.8
3 -> value: 1.7
4 -> value: 1.6
5 -> value: 1.5
6 -> value: 1.4
7 -> value: 1.3
8 -> value: 1.2
9 -> value: 1.1
10 -> value: 1
于 2015-03-01T09:44:35.987 回答
0

如果您能够添加最小值为 0 的约束,则简化上面 LSerni 的答案是:x = ((x % x_max) + x_max) % x_max

当小于 0 最小值时,第一个x % x_max操作将始终为负。x这允许将该简化的第二个模运算替换为小于 0 的比较。

float wrap0MinValue(float x, float x_max)
{
    int result = toWrap % maxValue;
    if (result < 0) // set negative result back into positive range
        result = maxValue + result;
    return result;
}
于 2017-07-25T04:28:58.070 回答
-2

对于范围 0..1 的非常具体的情况,这似乎有效:

float wrap(float n) {
    if (n > 1.0) {
        return n - floor(n);
    }
    if (n < 0.0) {
        return n + ceil(abs(n));
    }
    return n;
}
于 2019-07-25T13:33:35.603 回答
-3

使用Wouter de Kort的答案,但要改变

if (value.CompareTo(max) > 0) return max;

if (value.CompareTo(max) > 0) return min;
于 2013-01-19T16:02:09.490 回答