3

Given a set of consecutive numbers from 1 to n, I'm trying to find the number of subsets that do not contain consecutive numbers.

E.g., for the set [1, 2, 3], some possible subsets are [1, 2] and [1, 3]. The former would not be counted while the latter would be, since 1 and 3 are not consecutive numbers.

Here is what I have:

def f(n)
  consecutives = Array(1..n)
  stop = (n / 2.0).round
  (1..stop).flat_map { |x|
    consecutives.combination(x).select { |combo|
      consecutive = false
      combo.each_cons(2) do |l, r|
        consecutive = l.next == r
        break if consecutive
      end
      combo.length == 1 || !consecutive
    }
  }.size
end

It works, but I need it to work faster, under 12 seconds for n <= 75. How do I optimize this method so I can handle high n values no sweat?

I looked at:

and some others. I can't seem to find an answer.

Suggested duplicate is Count the total number of subsets that don't have consecutive elements, although that question is slightly different as I was asking for this optimization in Ruby and I do not want the empty subset in my answer. That question would have been very helpful had I initially found that one though! But SergGr's answer is exactly what I was looking for.

  • I'm pretty sure there is a closed formula, or at least a simple algorithm to compute the number of such subsets without constructing the sets. Which would make this a maths problem, not a programming one. It sounds like you are taking part in some sort of competition. Note that in most such competitions, most problems are maths problems, not programming problems. – Jörg W Mittag Mar 22 '18 at 09:24
  • I can think of an approach using dynamic programming (`O(n^2)` time & space complexity). I do not know ruby, but I can present you with a solution in Java / C-like syntax if this acceptable to you. – Alexandre Dupriez Mar 22 '18 at 10:24
  • @AlexandreDupriez, note that no dynamic programming solution could be no more efficient than the method given in the selected answer. – Cary Swoveland Mar 22 '18 at 20:08
  • @CarySwoveland Yes, what I had in mind was wrong - thanks for the note – Alexandre Dupriez Mar 22 '18 at 20:42

2 Answers2

4

Although @user3150716 idea is correct the details are wrong. Particularly you can see that for n = 3 there are 4 subsets: [1],[2],[3],[1,3] while his formula gives only 3. That is because he missed the subset [3] (i.e. the subset consisting of just [i]) and that error accumulates for larger n. Also I think it is easier to think if you start from 1 rather than n. So the correct formulas would be

f(1) = 1
f(2) = 2
f(n) = f(n-1) + f(n-2) + 1

Those formulas are easy to code using a simple loop in constant space and O(n) speed:

def f(n)
  return 1 if n == 1
  return 2 if n == 2

  # calculate 
  # f(n) = f(n-1) + f(n - 2) + 1
  # using simple loop
  v2 = 1
  v1 = 2
  i = 3
  while i <= n do
     i += 1
     v1, v2 = v1 + v2 + 1, v1
  end 
  v1
end

You can see this online together with the original code here

This should be pretty fast for any n <= 75. For much larger n you might require some additional tricks like noticing that f(n) is actually one less than a Fibonacci number

f(n) = Fib(n+2) - 1

and there is a closed formula for Fibonacci number that theoretically can be computed faster for big n.

SergGr
  • 23,570
  • 2
  • 30
  • 51
  • 1
    Don't forget that the empty set is a subset of all sets, so for n = 3 there are 5 subsets without consecutive integers in them, not 4 – Simple Lime Mar 22 '18 at 17:36
  • @SimpleLime, that's a question about definitions. And since we are not in the math class but in the real life I use the same definition as the OP uses: my method produces the same results as the OPs but faster which is exactly what this question is all about. – SergGr Mar 22 '18 at 17:46
  • 1
    You don't need the variable `i` and can use parallel assignment: `(n-2).times { v1, v2 = v1 + v2 + 1, v1 }`, – Cary Swoveland Mar 22 '18 at 23:37
  • @CarySwoveland, Ruby is not a language I use regularly so I decided to write a dumb code that will 100% work. If you are sure about a shorter syntax - feel free to improve this answer by editing. But I still think that the loop "3 to n" is more clear to the reader than just `(n-2).times`. – SergGr Mar 23 '18 at 14:07
  • SergGr, Rubyists commonly use [parallel assignment](http://www.linuxtopia.org/online_books/programming_books/ruby_tutorial/Ruby_Expressions_Parallel_Assignment.html) when, as here, the values of two variables are functions of the current values of each other. The use of methods having blocks (as in `(n-2).times { ... }`) is generally preferred in Ruby, in part because a new scope is created by the block, which reduces the potential for bugs. By contrast, `while` loops do not have blocks. – Cary Swoveland Mar 23 '18 at 17:50
  • @CarySwoveland, you don't have to convince me that "parallel assignment" is a nice syntactic sugar. If that was a Python question (which I know better) I would definitely use it. As for `times` my point is that the compiler is (or at least should be) not the only and actually even not the main target of the code - it is other developers. In Python I would probably use `range(2, n)` or `range(3, n+1)` (but definitely not `range(n-2)`!). I just don't know what is a good equivalent of this in Ruby. As I said - feel free to improve this answer with better Ruby syntax if you know how. – SergGr Mar 23 '18 at 17:57
1

let number of subsets with no consecutive numbers from{i...n} be f(i), then f(i) is the sum of:

1) f(i+1) , the number of such subsets without i in them.

2) f(i+2) + 1 , the number of such subsets with i in them (hence leaving out i+1 from the subset)

So,

f(i)=f(i+1)+f(i+2)+1
f(n)=1
f(n-1)=2

f(1) will be your answer. You can solve it using matrix exponentiation(http://zobayer.blogspot.in/2010/11/matrix-exponentiation.html) in O(logn) time.

Chirag
  • 618
  • 6
  • 21
  • You certainly have the right idea, but it's unfortunate you'd didn't check your answer for a small `n`, as that probably would have exposed your error. – Cary Swoveland Mar 22 '18 at 16:27
  • Sir, still couldn't find the error. Care to reveal ? – Chirag Mar 22 '18 at 16:41
  • Suppose `n=3`, then the non-consecutive sub-arrays are `[[1],[2],[3],[1,3]]`. We therefore should obtain `f(1) #=> 4`. We compute `f[3] #=> 1` (namely, `[[3]]`), `f[2] #=> 2` (namely, `[[2],[3]]`) and `f(1) = f(2)+f(3) #=> 3`, which we know is incorrect. – Cary Swoveland Mar 22 '18 at 17:23
  • Thanks , did the edit . – Chirag Mar 23 '18 at 07:04
  • You should probably reference "matrix exponentiation". If I'm unaware of the term I'm probably not alone. – Cary Swoveland Mar 23 '18 at 07:36