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

Хорошо известно, что сравнивать поплавки с помощью == обычно ошибка. В классе трехмерных векторов (с плавающими компонентами 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;
    }

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

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, C, если A.Equals(B) а также B.Equals(C) правда, это все еще может быть так, что A.Equals(C) ложно (Представьте, что расстояние между A и B равно 6e-6, между B и C - 6e-6, а между A и C - 1,2e-5). Но равенство хеш-кодов всегда транзитивно, поскольку они просто числа.

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

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

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

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

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

Что я могу сделать тогда?

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

Все правы...

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

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

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

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. Все равные точки будут сравниваться равными по одному из двух значений хеша. Для этого потребуется дважды ввести точку в хеш-таблицу, по одному разу для каждой подпрограммы хеширования.

Другие вопросы по тегам