Отображение двух целых в одно уникальным и детерминированным способом - PullRequest
213 голосов
/ 28 мая 2009

Представьте два положительных целых числа A и B. Я хочу объединить эти два в одно целое число C.

Не может быть других целых чисел D и E, которые объединяются в C. Таким образом, объединение их с оператором сложения не работает. Например, 30 + 10 = 40 = 40 + 0 = 39 + 1 Также не работает конкатенация. Например, «31» + «2» = 312 = «3» + «12»

Эта комбинированная операция также должна быть детерминированной (всегда давать один и тот же результат с одинаковыми входными данными) и всегда должны давать целое число с положительной или отрицательной стороны целых чисел.

Ответы [ 16 ]

209 голосов
/ 28 мая 2009

Вы ищете биективное отображение NxN -> N. Они используются, например, для подгонка . Взгляните на этот PDF для ознакомления с так называемыми функциями сопряжения . В Википедии введена особая функция сопряжения, а именно функция сопряжения Кантора :

pi(k1, k2) = 1/2(k1 + k2)(k1 + k2 + 1) + k2

Три замечания:

  • Как уже говорили другие, если вы планируете реализовать функцию сопряжения, вы можете вскоре обнаружить, что вам нужны произвольно большие целые числа (bignums).
  • Если вы не хотите проводить различие между парами (a, b) и (b, a), то сортируйте a и b перед применением функции сопряжения.
  • На самом деле я солгал. Вы ищете биективное отображение ZxZ -> N. Функция Кантора работает только с неотрицательными числами. Однако это не проблема, поскольку легко определить биекцию f : Z -> N, например:
    • f (n) = n * 2 , если n> = 0
    • f (n) = -n * 2 - 1 , если n <0 </li>
200 голосов
/ 14 декабря 2012

Функция сопряжения Кантора действительно одна из лучших, учитывая ее простую, быструю и компактную работу, но кое-что еще лучше опубликовано на Wolfram Мэтью Шудзиком, здесь, Ограничение функции сопряжения Кантора (относительно) состоит в том, что диапазон закодированных результатов не всегда находится в пределах 2N битового целого, если входные данные представляют собой два N битовых целых числа. То есть, если мои входные данные представляют собой два 16 битовых целых числа в диапазоне от 0 to 2^16 -1, тогда возможны 2^16 * (2^16 -1) комбинации входных данных, поэтому в силу очевидного принципа Pigeonhole нам нужен выходной размер в наименьший 2^16 * (2^16 -1), который равен 2^32 - 2^16, или, другими словами, карта из 32 битовых чисел должна быть в идеале выполнимой. Это не может иметь мало практического значения в мире программирования.

Функция сопряжения Кантора :

(a + b) * (a + b + 1) / 2 + a; where a, b >= 0

Отображение для двух максимально 16-битных целых чисел (65535, 65535) будет 8589803520, которое, как вы видите, не может быть вписано в 32 бита.

Введите функция Шудзика :

a >= b ? a * a + a + b : a + b * b;  where a, b >= 0

Отображение для (65535, 65535) теперь будет 4294967295, которое, как вы видите, является 32-битным (от 0 до 2 ^ 32 -1) целым числом. Вот где это решение идеально, оно просто использует каждую точку в этом пространстве, поэтому ничто не может быть более эффективным.


Теперь, учитывая тот факт, что мы обычно имеем дело со знаковыми реализациями чисел различных размеров в языках / инфраструктурах, давайте рассмотрим signed 16 битовых целых чисел в диапазоне от -(2^15) to 2^15 -1 (позже мы увидим, как расширить даже выходной поток до охватывать подписанный диапазон). Так как a и b должны быть положительными, они варьируются от 0 to 2^15 - 1.

Функция сопряжения Кантора :

Отображение для двух максимально 16-разрядных целых чисел со знаком (32767, 32767) будет 2147418112, что немного меньше максимального значения для 32-разрядного целого числа со знаком.

Теперь Функция Шудзика :

(32767, 32767) => 1073741823, намного меньше ..

Давайте учтем отрицательные целые числа. Это выходит за рамки первоначального вопроса, который я знаю, но просто помогаю будущим посетителям.

Функция сопряжения Кантора :

A = a >= 0 ? 2 * a : -2 * a - 1;
B = b >= 0 ? 2 * b : -2 * b - 1;
(A + B) * (A + B + 1) / 2 + A;

(- 32768, -32768) => 8589803520, то есть Int64. 64-битный выход для 16-битных входов может быть таким непростительным !!

функция Шудзика :

A = a >= 0 ? 2 * a : -2 * a - 1;
B = b >= 0 ? 2 * b : -2 * b - 1;
A >= B ? A * A + A + B : A + B * B;

(- 32768, -32768) => 4294967295, 32-битный для диапазона без знака или 64-битный для диапазона со знаком, но все же лучше.

Теперь все это пока результат всегда был положительным. В мире со знаком будет еще больше экономии места, если мы сможем перевести половину вывода на отрицательную ось . Вы можете сделать это так для Шудзика:

A = a >= 0 ? 2 * a : -2 * a - 1;
B = b >= 0 ? 2 * b : -2 * b - 1;
C = (A >= B ? A * A + A + B : A + B * B) / 2;
a < 0 && b < 0 || a >= 0 && b >= 0 ? C : -C - 1;

(-32768, 32767) => -2147483648

(32767, -32768) => -2147450880

(0, 0) => 0 

(32767, 32767) => 2147418112

(-32768, -32768) => 2147483647

Что я делаю: После применения веса 2 к входам и прохождения через функцию, я делю результат на два и беру некоторые из них на отрицательную ось, умножая на -1.

См. Результаты, для любого ввода в диапазоне числа битов со знаком 16 выход находится в пределах целого числа битов со знаком 32, что здорово. Я не уверен, что можно сделать то же самое для функции сопряжения Кантора, но я не пытался так сильно, как это не так эффективно. Более того, больше вычислений, связанных с функцией сопряжения Кантора, означает и ее медленную .

Вот реализация C #.

public static long PerfectlyHashThem(int a, int b)
{
    var A = (ulong)(a >= 0 ? 2 * (long)a : -2 * (long)a - 1);
    var B = (ulong)(b >= 0 ? 2 * (long)b : -2 * (long)b - 1);
    var C = (long)((A >= B ? A * A + A + B : A + B * B) / 2);
    return a < 0 && b < 0 || a >= 0 && b >= 0 ? C : -C - 1;
}

public static int PerfectlyHashThem(short a, short b)
{
    var A = (uint)(a >= 0 ? 2 * a : -2 * a - 1);
    var B = (uint)(b >= 0 ? 2 * b : -2 * b - 1);
    var C = (int)((A >= B ? A * A + A + B : A + B * B) / 2);
    return a < 0 && b < 0 || a >= 0 && b >= 0 ? C : -C - 1;
}

Поскольку промежуточные вычисления могут превышать пределы 2N целое число со знаком, я использовал 4N целочисленный тип (последнее деление на 2 возвращает результат в 2N).

Ссылка, которую я предоставил для альтернативного решения, хорошо изображает график функции, использующей каждую точку в пространстве. Удивительно видеть, что вы можете однозначно кодировать пару координат в одно число обратимо! Волшебный мир чисел !!

45 голосов
/ 28 мая 2009

Если A и B могут быть выражены 2 байтами, вы можете объединить их в 4 байта. Поместите A в наиболее значимую половину, а B в наименее значимую.

На языке C это дает (при условии, что sizeof (short) = 2 и sizeof (int) = 4):

int combine(short A, short B)
{
    return A<<16 | B;
}

short getA(int C)
{
    return C>>16;
}

short getB(int C)
{
    return C & 0xFFFF;
}
14 голосов
/ 28 мая 2009

Возможно ли это вообще?
Вы объединяете два целых числа. Они оба имеют диапазон от -2 147 483 648 до 2 147 483 647, но вы получите только положительные результаты. Это составляет 2147483647 ^ 2 = 461169E + 18 комбинаций. Поскольку каждая комбинация должна быть уникальной И приводить к целому числу, вам понадобится какое-то магическое целое число, которое может содержать это количество чисел.

Или моя логика ошибочна?

8 голосов
/ 28 мая 2009

Стандартным математическим способом для натуральных чисел является использование уникальности простой факторизации.

f( x, y ) -> 2^x * 3^y

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

Вы можете изменить это, чтобы иметь дело с отрицательными x и y, кодируя флаги со степенями 5 и 7 слагаемых.

1010 *, например *

f( x, y ) -> 2^|x| * 3^|y| * 5^(x<0) * 7^(y<0)
8 голосов
/ 28 мая 2009

Пусть число a будет первым, b - вторым. Пусть p будет a+1 -ым простым числом, q будет b+1 -ым простым числом

Тогда результат будет pq, если a<b, или 2pq, если a>b. Если a=b, пусть будет p^2.

4 голосов
/ 10 марта 2014

Для положительных целых чисел в качестве аргументов и там, где порядок аргументов не имеет значения:

  1. Вот неупорядоченная функция сопряжения :

    <x, y> = x * y + trunc((|x - y| - 1)^2 / 4) = <y, x>
    
  2. Для x ≠ y вот уникальная неупорядоченная функция спаривания :

    <x, y> = if x < y:
               x * (y - 1) + trunc((y - x - 2)^2 / 4)
             if x > y:
               (x - 1) * y + trunc((x - y - 2)^2 / 4)
           = <y, x>
    
4 голосов
/ 29 мая 2009

Построить отображение не так уж и сложно:

   1  2  3  4  5  use this mapping if (a,b) != (b,a)
1  0  1  3  6 10
2  2  4  7 11 16
3  5  8 12 17 23
4  9 13 18 24 31
5 14 19 25 32 40

   1  2  3  4  5 use this mapping if (a,b) == (b,a) (mirror)
1  0  1  2  4  6
2  1  3  5  7 10
3  2  5  8 11 14
4  4  8 11 15 19
5  6 10 14 19 24


    0  1 -1  2 -2 use this if you need negative/positive
 0  0  1  2  4  6
 1  1  3  5  7 10
-1  2  5  8 11 14
 2  4  8 11 15 19
-2  6 10 14 19 24

Понять, как получить значение для произвольного a, b, немного сложнее.

4 голосов
/ 28 мая 2009

f(a, b) = s(a+b) + a, где s(n) = n*(n+1)/2

  • Это функция - она ​​детерминированная.
  • Это также инъективно - f отображает разные значения для разных (a, b) пар. Вы можете доказать это с использованием факта: s(a+b+1)-s(a+b) = a+b+1 < a.
  • Возвращает довольно маленькие значения - хорошо, если вы собираетесь использовать его для индексации массива, так как массив не должен быть большим.
  • Это дружественно к кешу - если две (a, b) пары близки друг к другу, то f отображает числа, близкие друг к другу (по сравнению с другими методами).

Я не понял, что Вы подразумеваете под:

всегда должен давать целое число на либо положительный, либо отрицательный сторона целых чисел

Как написать (больше), (меньше) символов на этом форуме?

3 голосов
/ 01 февраля 2011

Хотя ответ Stephan202 является единственным действительно общим, для целых чисел в ограниченном диапазоне вы можете добиться большего успеха. Например, если ваш диапазон составляет 0,10 000, вы можете сделать:

#define RANGE_MIN 0
#define RANGE_MAX 10000

unsigned int merge(unsigned int x, unsigned int y)
{
    return (x * (RANGE_MAX - RANGE_MIN + 1)) + y;
}

void split(unsigned int v, unsigned int &x, unsigned int &y)
{
    x = RANGE_MIN + (v / (RANGE_MAX - RANGE_MIN + 1));
    y = RANGE_MIN + (v % (RANGE_MAX - RANGE_MIN + 1));
}

Результаты могут помещаться в одно целое число для диапазона до квадратного корня из числа целочисленных типов. Это упаковывает немного более эффективно, чем более общий метод Stephan202. Также значительно проще декодировать; для начала не требуются квадратные корни:)

...