Есть ли способ получить хэш-код поплавка с помощью epsilon?

Хорошо известно, что сравнение чисел с плавающей запятой по == обычно является ошибкой. В классе 3D-векторов (с компонентами с плавающей запятой X, Y, Z) я написал, что два вектора считаются равными, если их расстояние считается нулевым.

public override bool Equals(object obj)
{
    if (obj == null) {
        return false;
    }

    if (GetType () != obj.GetType ()) {
        return false;
    }

    float d = DistSq ((Vec) obj);

    return IsConsideredZero (d);
}

public float DistSq(Vec p)
{
    Vec d = this - p;
    return d.LengthSq ();
}

public float LengthSq()
{
    return X * X + Y * Y + Z * Z;
}

private const float VEC_COMPARE_EPSILON_ABS = 1E-05f;
public static bool IsConsideredZero(float f)
{
    return Math.Abs (f) < VEC_COMPARE_EPSILON_ABS;
}

Пока все работало нормально. Однако теперь я хотел бы получить хэш-код вектора. Я вижу, что что-то подобное hash = (int)X^(int)Y^(int)Z обречено на провал.

Лучшее, что я мог придумать, было:

public override int GetHashCode()
{
    return 0;
}

Это, конечно, отстой. Есть ли способ получить разумный хэш-код? NaN и другие специальные значения возможны, но маловероятны в случае, если это важно.

Ответов (5)

Решение

Невозможно предположить, что вы хотите иметь обычные свойства хэш-кода / равенства:

  • Если X = Y и Y = Z, то X = Z (транзитивность)
  • Если X = Y, то Y = X (коммутативность)
  • X = X для всех X (рефлексивность)

Первое правило представляет собой проблему - потому что, если каждое значение считается «равным» следующему большему представимому числу, в конечном итоге все числа равны. Например, предположим, что одно число считается равным другому, они находятся в пределах 0,1:

0 равно 0,08 0,08 равно 0,16 0,16 равно 0,24

=> 0 равно 0,16 по правилу транзитивности => 0 равно 0,24 по правилу транзитивности

(так далее)

Если вы игнорируете правило транзитивности, вы все равно (предположительно) хотите, чтобы «равные» значения имели одинаковые хэш-коды. Это эффективно обеспечивает соблюдение правила транзитивности - в приведенном выше примере 0 и 0,08 должны иметь одинаковые хэш-коды, как и 0 и 0,16. Следовательно, 0 и 0,16 должны иметь одинаковые хэш-коды и так далее. Поэтому у вас не может быть полезного хэш-кода - он должен быть константой.

Боюсь, что это не в общем случае. Набросок доказательства выглядит так:

Возьмите любые два числа a и b. Пусть разница между ними будет d. Затем, если вы создаете числа d / epsilon с шагом epsilon между ними, каждый шаг должен быть «равен» предыдущему шагу, который по семантике хэш-кода имеет тот же хэш-код. Таким образом, все числа должны иметь один и тот же хэш-код.

Вы можете решить эту проблему, только если добавите какое-то другое ограничение.

Кстати, ваше определение Equals также неверно, поскольку может быть верно, что a.Equals (b) и b.Equals (c), но не a.Equals (c), что неверно для equals. Это известно как нарушение свойства транзитивности .

Что мне тогда делать?

Решение зависит от того, для чего вы используете хеш. Одним из решений было бы введение концептуальной сетки. Измените равно и хэш-код так, чтобы два числа были равны, если они находятся в одном кубе сетки, округляя до постоянного числа десятичных знаков, затем взяв равные и хэш-код для округленного числа. Если близость к нулю является важным случаем, добавьте смещение epsilon / 2 перед округлением, чтобы ноль был центром куба. Это правильно, но у вас может быть два числа, расположенных произвольно близко друг к другу (в пределах float), но не равных. Так что для некоторых приложений это будет нормально, для других - нет. Это похоже на идею от mghie .

Все правы ...

ОДНАКО часто делают одну вещь - немного расширяют концепцию хеширования. Рассмотрим разделение вашего трехмерного пространства прямоугольниками со стороной >> эпсилон.

Хеш точки - это ящик, которому она принадлежит. Когда вы хотите найти точку, вы не проверяете точку в соответствующем поле (как вы бы делали для обычного хеша), но также и для соседних полей. В 3D вам должно хватить максимум 8 коробок.

Я не думаю, что у вас может быть хэш-код, который согласуется с вашим методом сравнения, потому что последний не является транзитивным: для любых трех векторов A, B, C, если A.Equals(B) и B.Equals(C) истинны, это все равно может быть A.Equals(C) ложным. (Представьте, что расстояние между A и B равно 6e-6, между B и C равно 6e-6, а между A и C равно 1,2e-5). Но равенство хэш-кодов всегда транзитивно, поскольку они просто числа.

В этом случае я бы просто создал метод хэш-кода, который вычисляет хэш на основе точных значений координат с плавающей запятой, и упомянул в документации, что он несовместим с equals. Я знаю, что это не совсем решение, но, учитывая, что я не думаю, что существует реальное решение, лучше иметь нетривиальный хэш-код, чем просто 0.

Какую бы технику вы ни использовали, будут проблемы, потому что вы предложили то, что невозможно решить.

Вам нужно 1) равномерно распределенный хэш, так что для большинства чисел a и b, где a! = B, затем a.GetHashCode ()! = B.GetHashCode (), но 2) где a == b, затем a.GetHashCode () = = b.GetHashCode () должно быть истинным.

При возврате константы выполняется (2), но не (1).

Вы можете продемонстрировать, что округление на границах 1E-5 и использование этого в качестве хеша нарушает (1), но нарушает (2). Возьмем, к примеру, 1E-5 и 2E-5. Округление приведет к получению двух разных значений хеш-функции, но при сравнении они будут равны. Это нарушает ограничение (2) выше. Вы можете легко обобщить это, чтобы доказать, что любое округление числа приведет к аналогичной проблеме.

Я рекомендую вам выбрать другой подход. Я предполагаю, что основная проблема заключается в том, чтобы определить, близка ли какая-то точка к той, которая у вас уже есть. Я рекомендую повторно разделить координатное пространство пополам (где точки вдоль границы (т.е. <= 1E-5 от границы) в обеих половинах). Если вы постепенно разделите свое пространство (представьте себе двоичное дерево), вы можете построить структуру данных, которая будет быстро возвращать желаемый результат и ее будет довольно легко построить.

Если я пропустил свое предположение, и вы должны использовать хеш, тогда можете делать все, что хотите, с двумя значениями хеша, каждое из которых округляется до 1E-5, но смещается на 5E-6. Все равные точки будут сравнивать равные по одному из двух значений хеш-функции. Это потребует от вас дважды ввести точку в хеш-таблице, по одному разу для каждой хеш-процедуры.