Моя цель — выполнить операцию над каждым подмножеством k-элементов из n элементов, хранящихся в vec.
Мой текущий подход использует эту битовую манипуляцию для генерации целых чисел с k набором битов в лексикографическом порядке. То есть, учитывая одно такое целое число, оно изменяет его на следующее по порядку.
pub fn set_next_bit_permutation(v: &mut u128) {
let t: u128 = *v | (*v-1);
*v = (t + 1) | (((!t & (!t).wrapping_neg()) - 1) >> (v.trailing_zeros() + 1))
}
Затем я использую позиции установленных битов в качестве индексов, извлекая каждый индекс следующим образом. Допустим, я использую perm: u128 для хранения текущей перестановки. Этот код будет внутри цикла, который будет выполняться до тех пор, пока я не проверю каждую перестановку:
perm_copy = perm;
while perm_copy > 0 {
cur_index = perm.trailing_zeros();
// do something with cur_index
perm_copy ^= 1 << cur_index;
}
set_next_bit_permutation(perm);
Хотя это работает и довольно быстро (важно, потому что мы повторяем это много раз), мы ограничены размером 128. Кроме того, это похоже на хак.
Вопросы:
Это хороший хак. Это работает, это быстро, и вы можете это ясно объяснить, даже если с первого взгляда не будет понятно, как это работает. Конечно, это может быть переусложнено, если вам действительно не нужна скорость. Но если да, то знаете ли вы n
(или хотя бы верхнюю границу) во время компиляции, а оно всего > 128, или вам нужна динамика n
?
@ChayimFriedman Я не знаю во время компиляции. Я мог бы придумать верхнюю границу для каждого размера входных данных. Это потребует перекомпиляции для каждого нового ввода, но это терпимо, если это позволит мне получить больший размер без резкого снижения производительности.
Тогда вам по сути нужен bigint. Естественное расширение вашего текущего кода. Используя подходящую библиотеку bigint, вы можете сделать это очень быстро, не выделяя каждый ход.
Извините, я всегда путаю эти термины. Для этого существует термин «комбинации». В itertools это тоже есть (и тоже будет медленно).
Хм... Это будет сложнее, чем кажется на первый взгляд, поскольку вы создадите много временных объектов. Если вы хотите сделать это эффективно, вам нужно будет найти способ их избежать. В качестве альтернативы вы можете выделить несколько запасных bigints, но это будет менее эффективно.
Вам просто нужен bigint. В Rust есть различные библиотеки bigint, самая быстрая из них — коврик. Однако он связан с GMP, что делает его лицензированным LGPL и не работает на Windows-MSVC.
Вот как с ним может выглядеть ваш код (не проверялось):
use rug::{Assign, Integer};
pub struct Combinations {
result: Integer,
temp1: Integer,
temp2: Integer,
temp3: Integer,
}
impl Combinations {
pub fn new(upper_bound_length: usize) -> Self {
Self {
result: Integer::with_capacity(upper_bound_length),
temp1: Integer::with_capacity(upper_bound_length),
temp2: Integer::with_capacity(upper_bound_length),
temp3: Integer::with_capacity(upper_bound_length),
}
}
}
/// This is an operation `rug` doesn't natively provide. It can be accelerated more if it's a hotspot,
/// via SIMD and such.
fn trailing_zeros(v: &Integer) -> u32 {
let mut result = 0;
for &limb in v.as_limbs().iter().rev() {
let trailing_zeros = limb.trailing_zeros();
result += trailing_zeros;
if trailing_zeros < gmp_mpfr_sys::gmp::limb_t::BITS {
break;
}
}
result
}
pub fn set_next_bit_permutation(v: &mut Combinations) {
v.temp1.assign(&v.result - 1);
v.temp1 |= &v.result;
v.temp2.assign(!&v.temp1);
v.temp1 += 1;
v.temp3.assign(-&v.temp2);
v.temp2 &= &v.temp3;
v.temp2 -= 1;
v.temp2 >>= trailing_zeros(&v.result) + 1;
v.result.assign(&v.temp1 | &v.temp2);
}
Зависимости:
gmp-mpfr-sys = "1.6.4"
rug = { version = "1.25.0", default-features = false, features = ["integer"] }
Помимо оптимизированных алгоритмов и реализации rug
, у него есть еще одна потрясающая особенность: возможность присваивать результат вычисления непосредственно другой переменной без выделения (что происходит с незавершенными операциями и assign()
). Альтернативой для таких библиотек, как num-bigint, будет сначала назначить левую переменную цели, а затем использовать op=
.
Если вы готовы перекомпилировать для каждого размера, вы можете работать еще быстрее, используя целые числа произвольного размера во время компиляции, что позволит компилятору полностью развернуть циклы. Крейт bnum — это пример крейта, который реализует целые числа произвольного размера, хотя я не знаю, будет ли он таким же быстрым, как rug
, поскольку его реализация не так оптимизирована, как GMP.
Я не получаю здесь правильного ответа. Возможно, проблема не в этом, но что в set_next_bit_permutation() эквивалентно wrapping_neg()?
@Dave Извините, я допустил опечатку. Починил это.
На самом деле это легко и эффективно сделать с помощью отсортированного массива из k индексов.
Давайте отсортируем по возрастанию и создадим массивы индексов в лексикографическом порядке, учитывая сначала самый высокий индекс.
Поэтому мы инициализируем массив наименьшими индексами: A=[0, 1, ... k-1]
Затем, чтобы привести в порядок следующий массив индексов подмножества, просто:
Когда вы увеличите A[k-1] до n, все готово.
Обычно вы касаетесь только A[0]. это амортизированное постоянное время, если n немного больше k. Скорее всего, это будет намного быстрее, чем то, что вы собираетесь делать с этими индексами. Если вы измените подмножество векторов на месте, вам обычно нужно изменить только первый элемент.
Оба подхода быстрые. Я принимаю это, потому что это также просто.
@ChayimFriedman Скорость жизненно важна, но мне нужно обрабатывать больше элементов.