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

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

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

Ответ 1

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

  • Если 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 должны иметь равные хэш-коды и т.д. Поэтому у вас не может быть полезного хэш-кода - он должен быть постоянным.

Ответ 2

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

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

Ответ 3

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

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

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

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

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

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

Ответ 4

Все верны...

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

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

Ответ 5

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

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