Power set: bitwise vs recursion
So, I’ve already written that bitwise operations is a very fast thing. But how can this be used in application?
Let’s say, you need to iterate over the powerset of some elements which can be useful in various fields (for example):
- operations with graphs;
- dynamic programming problems, where all combinations of elements should be stored in memoization table;
- other combinatorial problems…
There are two approaches for generating powerset: recursion and bitwise operations. Which of them performs better? Let’s test.
class PowerSet(
val set: IntArray,
) {
val subSets = mutableListOf<IntArray>()
fun generateRecursive(at: Int, used: BooleanArray) {
val n = set.size;
val range = set.indices
if (at == n) {
// Print found subset!
var subSet = IntArray(n)
for (i in range) {
if (used[i]) {
subSet[i] = set[i]
}
}
subSets.add(subSet)
} else {
// Include this element
used[at] = true
generateRecursive(at + 1, used)
// Backtrack and don't include this element
used[at] = false
generateRecursive(at + 1, used)
}
}
fun generateBinary() {
val n = set.size;
val range = set.indices
val maxVal = 1 shl n;
for (subset in 0 until maxVal) {
var subSet = IntArray(n)
for (i in range) {
val mask = 1 shl i
if ((subset and mask) == mask) {
subSet[i] = set[i]
}
}
subSets.add(subSet)
}
}
}
@State(Scope.Benchmark)
class PowerSetBenchmark {
@Param("3", "4", "5", "10", "20", "26")
var setSize = 0
lateinit var set: IntArray
@Setup
fun setUp() {
set = IntArray(setSize) { i -> i }
}
@Benchmark
fun powSetRecursive() {
val ps = PowerSet(set)
ps.generateRecursive(
0, BooleanArray(setSize)
)
}
@Benchmark
fun powSetBinary() {
val ps = PowerSet(set)
ps.generateBinary()
}
}Source code of PowerSet class and benchmark
Results
Benchmark (setSize) Mode Cnt Score Error Units
PowerSetBenchmark.powSetBinary 3 avgt 3 0.060 ± 0.065 us/op
PowerSetBenchmark.powSetBinary 4 avgt 3 0.137 ± 0.046 us/op
PowerSetBenchmark.powSetBinary 5 avgt 3 0.286 ± 0.064 us/op
PowerSetBenchmark.powSetBinary 9 avgt 3 6.654 ± 6.681 us/op
PowerSetBenchmark.powSetBinary 10 avgt 3 14.016 ± 18.000 us/op
PowerSetBenchmark.powSetBinary 11 avgt 3 82.002 ± 63.015 us/op
PowerSetBenchmark.powSetBinary 20 avgt 3 92927.224 ± 28005.295 us/op
PowerSetBenchmark.powSetBinary 26 avgt 3 8389329.133 ± 2665432.289 us/op
PowerSetBenchmark.powSetRecursive 3 avgt 3 0.076 ± 0.017 us/op
PowerSetBenchmark.powSetRecursive 4 avgt 3 0.191 ± 0.087 us/op
PowerSetBenchmark.powSetRecursive 5 avgt 3 0.400 ± 0.071 us/op
PowerSetBenchmark.powSetRecursive 9 avgt 3 7.283 ± 0.500 us/op
PowerSetBenchmark.powSetRecursive 10 avgt 3 20.408 ± 33.346 us/op
PowerSetBenchmark.powSetRecursive 11 avgt 3 57.855 ± 15.787 us/op
PowerSetBenchmark.powSetRecursive 20 avgt 3 82136.368 ± 75290.816 us/op
PowerSetBenchmark.powSetRecursive 26 avgt 3 7995903.683 ± 2029323.013 us/op
As you can notice from benchmark summary binary implementation is faster with n <= 10, but with n=11 recursive implementation becomes faster.
For me it’s pretty unexpected result which I cannot explain at the moment. So much the better, another interesting topic to explore is put to the piggy-bank :-) See you!