Алгоритм для определения количества единиц в двоичном представлении в диапазоне положительных чисел - PullRequest
0 голосов
/ 06 января 2019

Я только что натолкнулся на проблему, где мы должны вычислить число единиц в двоичном представлении чисел в большом диапазоне. Есть ли какой-либо алгоритм или метод, чтобы найти его легко? Например, Для входа N = 6 число единиц в двоичном представлении его предыдущих чисел. Подобно, 1 - 0001 - No. of 1's = 1; 2 - 0010 - No. of 1's = 1; 3 - 0011 - No. of 1's = 2; 4 - 0100 - No. of 1's = 1;
5 - 0101 - No. of 1's = 2;

Ограничения: 1 <= N <= 10 ^ 20 </p>

Итак, всего 7 (1 + 1 + 2 + 1 + 2). Есть ли другие хитрости, чтобы узнать это? Заранее спасибо!

Ответы [ 3 ]

0 голосов
/ 06 января 2019

Составить таблицу для значений 0..2 ^ P-1, где P = 8

 byte[] table = new byte[] {0,1,1,2,1,2,1,3, ... 7,8};

и маска всех единиц длины P:

 long mask = (1 << P)-1;

затем разделите входное число на байты и составьте сумму для каждого байта:

int numUnits(long number) {
  int sum=0;
  for (int k=0; k<64/P, k++) {
      sum += table[number & mask];
      num = num >> P;
  }
  return sum;
}

Вместо 8 вы можете взять P = 4 или 16, в зависимости от того, сколько памяти вы можете выделить для таблицы.

0 голосов
/ 06 января 2019

Пусть S (n) будет набором чисел от 0 до n (без дубликатов, но в любом порядке). Тогда S(2n+1) = {2*s for s in S(n)} + {2*s+1 for s in S(n)} и S(2n) = {2*s for s in S(n)} + {2*s+1 for s in S(n-1)}.

Два примера:

S(7) = {2*s for s in S(3)} + {2*s+1 for s in S(3)}
     = {0, 2, 4, 6} + {1, 3, 5, 7}

S(10) = {2*s for s in S(5)} + {2*s+1 for s in S(4)}
      = {0, 2, 4, 6, 8, 10} + {1, 3, 5, 7, 9}

Допуская, что a(n) определяется как сумма битов, установленных во всех числах в S(n), и используя формулы для S, мы имеем a(2n+1) = 2a(n) + n+1 и a(2n) = a(n) + a(n-1) + n. Это связано с тем, что количество битов, установленных в {2*s for s in S(n)}, равно числу битов, установленных в S(n), а количество битов, заданных в {2*s+1 for s in S(n)}, равно числу битов, заданных в S(n), плюс один для каждого элемент S(n) (а именно: n+1).

Те же уравнения появляются в https://oeis.org/A000788,, зачисленных Ральфу Стефану:

a(0) = 0
a(2n) = a(n)+a(n-1)+n
a(2n+1) = 2a(n)+n+1

Используя это, можно написать функцию B с помощью B(N) = a(N), a(N-1):

def B(N):
    if N == 0:
        return 0, 0
    r, s = B(N//2)
    if N % 2:
        return 2*r+N//2+1, r+s+N//2
    else:
        return r+s+N//2, 2*s+N//2

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

Второе возвращаемое значение - это то, что вас интересует. Например:

>> print(B(7)[1])
9

>> print(B(28)[1])
64

>> print(B(10**20)[1])
3301678091638143975424

Очевидно, что это выполняется в арифметических операциях O (log N) и использует стек O (log N).

Получение к постоянной космической сложности

Можно немного уменьшить сложность пространства до O (1).

Мы можем записать уравнения Ральфа Стефана в матричной векторной форме:

[ a(2n+1) ] = [2 0 1 1]   [ a(n)  ]
[ a(2n)   ]   [1 1 1 0] * [ a(n-1)]
[ 2n+1    ]   [0 0 2 1]   [ n     ]
[ 1       ]   [0 0 0 1]   [ 1     ]

и

[ a(2n)   ] = [1 1 1 0]   [ a(n)  ]
[ a(2n-1) ]   [0 2 1 0] * [ a(n-1)]
[ 2n      ]   [0 0 2 0]   [ n     ]
[ 1       ]   [0 0 0 1]   [ 1     ]

Повторное применение одного или другого из этих правил дает:

[ a(n)  ] = M[0] * M[1] * ... * M[k] *   [ a(0) ]
[ a(n-1)]                                [ a(-1)]
[ n     ]                                [ 0    ]
[ 1     ]                                [ 1    ]

Где M[0], M[1], ..., M[k] - это одна или другая из двух матриц 4x4, которые встречаются в версиях матриц-векторов уравнений Ральфа Стефана, в зависимости от k бит n.

Таким образом:

def mat_mul(A, B):
    C = [[0] * 4 for _ in range(4)]
    for i in range(4):
        for j in range(4):
            for k in range(4):
                C[i][k] += A[i][j] * B[j][k]
    return C

M1 = [[2, 0, 1, 1], [1, 1, 1, 0], [0, 0, 2, 1], [0, 0, 0, 1]]
M0 = [[1, 1, 1, 0], [0, 2, 1, 0], [0, 0, 2, 0], [0, 0, 0, 1]]

def B2(N):
    M = [[1, 0, 0, 0], [0, 1, 0, 0], [0, 0, 1, 0], [0, 0, 0, 1]]
    while N:
        M = mat_mul(M, M1 if N%2 else M0)
        N >>= 1
    return M[1][3]

Функция B2 выполняет O (log n) арифметическую операцию, но использует постоянное пространство.

Мы можем сделать немного лучше, отметив, что матрица M всегда имеет вид:

[ a   b   c   d   ]
[ a-1 b+1 c   e   ]
[ 0   0   a+b a-1 ]
[ 0   0   0   1   ]

Затем B3 оптимизирует матричные умножения на B2 в зависимости от наблюдаемой структуры M:

def B3(N):
    a, b, c, d, e = 1, 0, 0, 0, 0
    while N:
        if N%2:
            a, c, d, e = 2*a+b, a+b+2*c, a+c+d, a+c+e-1
        else:
            b, c = a+2*b, a+b+2*c
        N >>= 1
    return e

Это так хорошо, как этот подход может нас занять: единственными арифметическими операциями являются сложения, умножение на два, деление на два и тестирование младшего бита. Пространственная сложность постоянна. Даже для огромных N (например, 10 ^ 200) затраченное время незначительно.

Быстрая версия на C.

Для скорости версия C (использующая расширение gcc для __int128) вычисляет b3(10**20) примерно за 140 наносекунд на моей машине. Код представляет собой простое преобразование Python-функции B3 (отмечается, что d не требуется), слегка затрудненный отсутствием множественного присваивания в C.

typedef unsigned __int128 uint128;

uint128 b3(uint128 n) {
    uint128 a=1, b=0, c=0, e=0;
    while (n) {
        if (n&1) {
            e = a+c+e-1;
            c = a+b+2*c;
            a = 2*a+b;
        } else {
            c = a+b+2*c;
            b = a+2*b;
        }
        n >>= 1;
    }
    return e;
}
0 голосов
/ 06 января 2019

Да. Давайте сначала проанализируем число единиц между 1 и степенью двойки 2 k (нижняя граница включительно, верхняя граница * исключая). Мы решим основную проблему на основе этого подхода позже.

Тогда это означает, что в конечном итоге все битовые комбинации выбираются для последних k битов (кроме 000, но это не содержит никаких установленных битов). Действительно, для k = 3 мы видим 001, 010, 011, 100, 101, 110 и 111. Таким образом, в среднем половина битов установлена. Таким образом, мы знаем, что общее количество установленных битов:

 k
2
---
\      k       k-1
/     ---  = 2     * k
---    2
i=0

Таким образом, для диапазона между 1 (или 0 , но это не имеет значения, поскольку 0 не имеет установленных битов) и 2 k , у нас есть 2 k-1 & times; k установленных битов. Например, при k = 3 мы считаем 2 2 & times; 3 = 12 битов, что действительно то, что мы видим, когда мы вручную перечисляем по нему.

Как это помогает нам в общем случае?

Скажем, мы хотим вычислить количество битов, установленных между 0 и l и 2 k k + 1 , тогда мы можем сначала посчитать общее количество битов, установленных до 2 k , а затем суммировать это с общим количеством битов, установленным между 2 k и l .

Теперь, конечно, все еще существует проблема с последним: так как мы не знаем, как это вычислить. Но мы можем выполнить «сдвиг»: мы можем вычислить общее число битов между 0 и l-2 k (мы знаем, как это сделать) и добавьте l-2 k дополнительно к этому результату. Мы рассчитываем общее количество бит между 0 и l-2 k таким же образом, однако мы знаем, что наибольшая степень двух из l-2 k будет меньше 2 k , поскольку 2 k было самым высоким сила два в l , поэтому "прогресс" гарантирован.

Как работает добавление l-2 k к результату? Давайте возьмем пример: если мы хотим вычислить количество битов между 000 и 110 (исключая), то мы должны суммировать биты 000, 001, 010, 011, что является первой «итерацией». Вторая итерация - это биты, установленные между 100 и 110, таким образом, мы делаем это, выполняя сдвиг и вычисляя количество элементов между 00 и 10, но есть дополнительный бит, который устанавливается для каждого числа здесь в «исходных» числах: самый высокий установленный бит, поэтому мы подсчитываем количество элементов, по которым мы повторяем, и таким образом компенсируем потерю битов.

Алгоритм : теперь мы можем получить алгоритм для этого с помощью:

def count_bit_range(n):
    if n <= 1:
        return 0
    k = n.bit_length()-1
    pk = 1 << k
    pk1 = 1 << (k-1)
    return k * pk1 + (n-pk) + count_bit_range(n-pk)

или нерекурсивный подход:

def count_bit_range(n):
    c = 0
    while n > 1:
        k = n.bit_length()-1
        pk = 1 << k
        pk1 = 1 << (k-1)
        c += k * pk1 + n - pk
        n -= pk
    return c

Например:

>>> count_bit_range(0)
0
>>> count_bit_range(1)
0
>>> count_bit_range(2)
1
>>> count_bit_range(3)
2
>>> count_bit_range(4)
4
>>> count_bit_range(5)
5
>>> count_bit_range(6)
7
>>> count_bit_range(12)
20
>>> count_bit_range(28)
64

Например, для 12 мы получаем:

      0001  0010  0011  0100  0101  0110  0111
1000  1001  1010  1011

так что 20 установленных бит.

или на 28:

       00001  00010  00011  00100  00101  00110  00111
01000  01001  01010  01011  01100  01101  01110  01111
10000  10001  10010  10011  10100  10101  10110  10111
11000  11001  11010  11011

что на самом деле 64.

Тесты : если запустить алгоритм с верхним пределом ( 10 20 ), мы получим 11,9 микросекунд на локальной машине:

>>> timeit(partial(count_bit_range, 10**20), number=1000000)
11.911393816000782

это (вероятно) не самое дорогое число в диапазоне, однако, количество рекурсивных вызовов масштабируется с количеством установленных битов верхней границы, и, следовательно, самое дорогое число в диапазоне скорее всего (1<<66)-1:

>>> timeit(partial(count_bit_range, (1<<66)-1), number=1000000)
32.43066442897543

, но 32,4 микросекунды все еще кажется разумным для вычисления количества бит, установленных между 1 и 73'786'976'294'838'206'463.

На локальной машине он дает мгновенный результат при нерекурсивном подходе до 10 20'0000 .

сложность по времени : количество рекурсивных вызовов масштабируется с количеством установленных битов в верхней границе: на самом деле, в каждой итерации удаляется бит с наибольшим набором, и алгоритм останавливается, когда верхняя граница достигает единицы или нуля. Для w -битного числа этот алгоритм принимает, таким образом, O (w) рекурсивных вызовов (это не само по себе количество "базовых операций").

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

Можно предположить, что большинство операций будут выполняться линейно по числу битов этого числа (например, .bit_length() и двоичные сдвиги), но умножение занимает больше, чем линейное время (по длине). Тем более что k имеет длину, которая масштабируется логарифмически с длиной верхней границы, это "сложно".

Если мы предположим, что рекурсивный шаг занимает квадратичное время на длине верхней границы (это, вероятно, является завышенной оценкой), мы, таким образом, получим временную сложность O (w 3). ) или для диапазона до n , временная сложность O (log 3 n) .

...