Что означает (число и -число) в битовом программировании?

Например:

int get(int i) {
    int res = 0;
    while (i) {
        res = (res + tree[i]) % MOD;
        i -= ( (i) & (-i) );
    }
    return res;
}

Функция обновления дерева:

void update(int i, int val) {
    while (i <= m) {
        tree[i] = (tree[i] + val) % MOD;
        i += ( (i) & (-i) );
    }
}

Не могли бы вы объяснить, что они делают в коде, используя ( (i) & (-i) )?


person SwadhIn    schedule 08.03.2016    source источник
comment
i & (-i) — это самый младший установленный бит (т. е. самый правый 1).   -  person Amadan    schedule 08.03.2016
comment
И если вы хотите увидеть, как это работает, рассмотрите twos-complement подписанного число n (используется для обозначения -n) и что произойдет, если вы и совместите его с n.   -  person Matthew Watson    schedule 08.03.2016
comment
ой.. теперь я могу понять. Спасибо за вашу помощь. :)   -  person SwadhIn    schedule 08.03.2016
comment
Не надейтесь изобретать такие хаки самостоятельно. Эти идеи копируются гораздо чаще, чем изобретаются.   -  person MSalters    schedule 08.03.2016
comment
Наслаждайтесь другими лайфхаками: Level Easy и Уровень Hard   -  person PTwr    schedule 08.03.2016
comment
Стандартный справочник по битхакам: graphics.stanford.edu/~seander/bithacks.html , но вашего там нет.   -  person CodesInChaos    schedule 08.03.2016
comment
@PTwr - первая ссылка конкретно касается Javascript. Хотя код похож и можно использовать одни и те же приемы, я не уверен, насколько это будет полезно. Приличный компилятор C++ все равно выполнил бы многие из этих оптимизаций. Мне было бы интересно увидеть сравнение скорости использования этих трюков в C++.   -  person Darrel Hoffman    schedule 08.03.2016
comment
@DarrelHoffman ActionScript, а не JS, но в любом случае биты есть биты. Битхаки в этом посте на самом деле скопированы с более описательная статья о C, которая, кажется, не имеет ничего выше уровня Hard. Если вы хотите проверить скорость, попробуйте BitHackAbsolute() (конечно, с большим набором данных), он быстрый, потому что он удаляет ветку, но делает это за счет потенциальных проблем с минимальным значением, что приводит к тому, что оптимизатор не использует его.   -  person PTwr    schedule 08.03.2016
comment
В связи с этим вопрос: 2*b | ~(2*b).   -  person Mark Adler    schedule 09.03.2016
comment
В соответствии с хорошей практикой подобные хаки рекомендуется размещать во встроенной функции с самодокументируемым именем, например least_significant_bit_set.   -  person Federico Poloni    schedule 09.03.2016
comment
см. также Почему "i & (i ^ (i - 1))" эквивалентно "i & (-i)"   -  person phuclv    schedule 09.03.2016


Ответы (3)


Эти две функции представляют собой модифицированную реализацию структуры данных Двоичное индексное дерево (дерево Фенвика).
Вот две картинки в дополнение к ответу MikeCAT, показывающие, как переменная i обновляется для разных значений.

Функция get:
Предположим, что max значение ввода в функции равно 15 для простоты представления.
введите здесь описание изображения
Узел с номером t на нем представляет tree[t] в массиве деревьев.
Если вы вызываете функцию get для i, возвращаемое значение представляет собой сумму tree[i ] плюс сумма всех элементов массива tree, индекс которых в массиве является родителем i на картинке, кроме нуля.
Вот некоторые Примеры:

get(15) = tree[15] + tree[14] + tree[12] + tree[8]
get(14) = tree[14] + tree[12] + tree[8]
get(13) = tree[13] + tree[12] + tree[8]
get(12) = tree[12] + tree[8]
get(11) = tree[11] + tree[10] + tree[8]
get(10) = tree[10] + tree[8]
get(9) = tree[9] + tree[8]
get(8) = tree[8]
get(7) = tree[7] + tree[6] + tree[4]
get(6) = tree[6] + tree[4]
get(5) = tree[5] + tree[4]
get(4) = tree[4]
get(3) = tree[3] + tree[2]
get(2) = tree[2]

Числа на метках узлов на приведенном выше рисунке обладают тем свойством, что родительским элементом каждого узла является эта метка узла за вычетом наименее значимой 1 (очень хорошо объяснено в ответе @MikeCAT)
Функция обновления:
Для простоты картины предположим, что максимальная длина массива tree равна 16.
Функция update немного сложнее.
Двоичное индексированное дерево
Добавляет val к tree[i] и ко всем элементам tree, которые их индекс является родительским для узла с меткой i на картинке.

update(16, val) --> tree[16] += val;
update(15, val) --> tree[15] += val, tree[16] += val;
update(14, val) --> tree[14] += val, tree[16] += val;
update(13, val) --> tree[13] += val, tree[14] += val; tree[16] += val;
update(12, val) --> tree[12] += val, tree[16] += val;
update(11, val) --> tree[11] += val, tree[12] += val, tree[16] += val;
update(10, val) --> tree[10] += val, tree[12] += val, tree[16] += val;
update(9, val)  --> tree[9] += val, tree[10] += val, tree[12] += val, tree[16] += val;
update(8, val)  --> tree[8] += val, tree[16] += val;
update(7, val)  --> tree[7] += val, tree[8] += val, tree[16] += val;
update(6, val)  --> tree[6] += val, tree[8] += val, tree[16] += val;
update(5, val)  --> tree[5] += val, tree[6] += val, tree[8] += val, tree[16] += val;
update(4, val)  --> tree[4] += val, tree[8] += val, tree[16] += val;
update(3, val)  --> tree[3] += val, tree[4] += val, tree[8] += val, tree[16] += val;
update(2, val)  --> tree[2] += val, tree[4] += val, tree[8] += val, tree[16] += val;
update(1, val)  --> tree[1] += val, tree[2] += val, tree[4] += val, tree[8] += val, tree[16] += val;
person FazeL    schedule 09.03.2016

Позвольте мне предположить, что отрицательное значение представлено с использованием дополнения до двух. В этом случае -i можно рассчитать как (~i)+1 (перевернуть биты, затем добавить 1).

Например, позвольте мне рассмотреть i = 44. Затем в двоичном формате

i           = 0000 0000 0000 0000 0000 0000 0010 1100
~i          = 1111 1111 1111 1111 1111 1111 1101 0011
-i = (~i)+1 = 1111 1111 1111 1111 1111 1111 1101 0100
(i) & (-i)  = 0000 0000 0000 0000 0000 0000 0000 0100

Как видите, наименьший бит, равный 1, можно вычислить с помощью (i) & (-i).

person MikeCAT    schedule 08.03.2016
comment
спасибо за ваше объяснение. теперь я полностью понимаю :) - person SwadhIn; 08.03.2016
comment
Разве это не зависит от реализации? Я предполагаю, что все современные компиляторы используют дополнение до двух для отрицательных чисел, но никто не запрещает компилятору или архитектуре делать это по-другому, что может привести к неопределенному поведению. - person vsz; 09.03.2016
comment
@vsz: стандарт C явно допускает это, и на самом деле целые числа со знаком могут иметь представление ловушек (например, отрицательный ноль), так что это хорошо подходит для UB. Конечно, -i уже является потенциальным UB, поскольку i может быть самым отрицательным целым числом, что дает вам переполнение целого числа со знаком (которое также не определено). - person Kevin; 09.03.2016
comment
Пример рабочего кода см. на странице stackoverflow.com/a/36046076/984780. - person Luis Perez; 16.03.2016

На случай, если кому-то понадобится и более общее доказательство,

Предположим, что x имеет формат a10k (здесь имеется в виду некоторая битовая строка a, за которой следует 1, а затем k нулей).

-x (по определению) то же самое, что и ~x + 1, поэтому

  • х & -х = (введите)
  • a10k & -(a10k) = (по определению отрицания)
  • a10k & ~(a10k) + 1 = (применить инверсию)
  • a10k & ~a01k + 1 = (добавить 1)
  • a10k & ~a10k = (И между чем-то и его инверсией)
  • 0в10к

Таким образом, у нас осталась только самая правая единица, которая, как мы предполагали, существовала.

Предположение о форме x исключает случай, что x = 0, и в этом случае результат, очевидно, по-прежнему равен нулю.

person harold    schedule 08.03.2016