在 C# 中有什么方法可以在 x_min 和 x_max 之间包装给定值 x。该值不应像 in 那样被钳制,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);
除非离andx
不远,并且可以通过很少的加法或减法(也可以考虑错误传播),我认为模数是您唯一可用的方法。x_min
x_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;
模数在浮点上工作得很好,那么如何:
x = ((x-x_min) % (x_max - x_min) ) + x_min;
尽管如此,它仍然是一个有效的分界线,你需要调整它的值小于 < min ...
当数字远离范围时,您会担心准确性。然而,这与模运算无关,但是它是执行的,但它是浮点的属性。如果你取一个 0 到 1 之间的数字,然后给它加上一个大常数,比如把它带入 100 到 101 的范围内,它会失去一些精度。
最小值和最大值是固定值吗?如果是这样,您可以提前计算出它们的范围和其倒数:
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);
}
乘法的性能应该比除法好一些。
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;
}
如何在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));
}
}
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
如果您能够添加最小值为 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;
}
对于范围 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;
}
使用Wouter de Kort的答案,但要改变
if (value.CompareTo(max) > 0) return max;
至
if (value.CompareTo(max) > 0) return min;