Fork me on GitHub

Project Notes

#339 Fireworks Grand finale

Using ruby to find the best fireworks grand finale; cassidoo’s interview question of the week (2025-07-06). This time I turned off the AI and enjoyed thinking about the deceptively simple algorithmic challenge from first principles.

Notes

The interview question of the week (2025-07-06):

Given an array of fireworks representing a series going off, write a function to find the “grand finale” of the show! A grand finale is defined as the longest sub-array where the average size is at least 5, the minimum velocity is 3, and the difference between the min and max height is no more than 10. Return the starting index of the grand finale.

Example:

const fireworks = [ {height: 10, size: 6, velocity: 4}, {height: 13, size: 3, velocity: 2}, {height: 17, size: 6, velocity: 3}, {height: 21, size: 8, velocity: 4}, {height: 19, size: 5, velocity: 3}, {height: 18, size: 4, velocity: 4} ];

grandFinaleStart(fireworks) 2

PS: if you actually want FIREWORKS, I followed this up with an Arduino implementation with fireworks simulation on an LED matrix. See LEAP#777 GrandFinale.

Initial Analysis and Solution

An eligible “grand finale” sub-array must meet the following criteria:

  • average(size) >= 5
  • minimum(velocity) == 3
  • maximum(height) - minimum(height) <= 10

If this was a real problem, my “requirements analyst alarm” would be ringing, especially for the “minimum velocity is 3” requirement. It seems unusual for that to be the one requirement expressed as an exact match, while the others are expressed as upper or lower limits. I would want to ask for clarification; should it be, for example: “minimum velocity is at least 3”?

For my purposes here, I will start by taking the requirement at face value, and perhaps code a variant later. So minimum(velocity) == 3 it is!

One may initially think that a limited scan would be sufficient: just iterate over elements until the criteria are not met. However the calculation is complicated by the fact we have one criteria using an aggregate function (average(size) >= 5). This raises the possibility that a shorter “invalid” sub-array can become part of a longer “valid” sub-array as more elements are added that skew the average size upwards. For example:

Element 1 2 3 4 5 6 7
{height: 1, size: 5, velocity: 3},
{height: 1, size: 1, velocity: 3},  
{height: 1, size: 1, velocity: 3},    
{height: 1, size: 1, velocity: 3},      
{height: 1, size: 2, velocity: 3},        
{height: 1, size: 50, velocity: 3},          
{height: 1, size: 3, velocity: 3},            
average(size): 5 3 2.3 2 2 10 9
minimum(velocity): 3 3 3 3 3 3 3
maximum(height) - minimum(height): 0 0 0 0 0 0 0
valid? Yes No No No No Yes Yes

This shows how one outlier (size: 50) can rescue an otherwise failing sub-array, and even carry over for subsequent elements that are trying to bring the average back down.

The implication is that for every starting position, we really can’t avoid checking all the way to the end of the array to see if a longer sub-array is possible. At least that would be the naïve approach. A smarter approach may be to recognise that it is only necessary to keep checking when we know there are elements later in the array that could possibly pull the average back up. I will leave that optimisation for later (maybe).

See examples.rb for the initial solution, It is a fairly naïve approach, but I have included some obvious optimisations:

  • size, height, velocity are split into distinct number arrays making it easier to evaluate
  • skip the evaluation of sub-arrays if they would not result in a longer match
  • evaluate each candidate sub-array immediately, and only record the result if it is better; throw away the rest

Let’s try it with the example provided. The data is in data_eg1.json:

$ ./examples.rb data_eg1.json
Using algorithm: initial_solution
Input array: [{"height"=>10, "size"=>6, "velocity"=>4}, {"height"=>13, "size"=>3, "velocity"=>2}, {"height"=>17, "size"=>6, "velocity"=>3}, {"height"=>21, "size"=>8, "velocity"=>4}, {"height"=>19, "size"=>5, "velocity"=>3}, {"height"=>18, "size"=>4, "velocity"=>4}]
Longest sub-array found: {:start=>2, :finish=>5, :length=>4}
Result: 2

That’s correct, now let’s run it with the skewed average example above, with data in data_eg2.json. Also correct:

$ ./examples.rb data_eg2.json
Using algorithm: initial_solution
Input array: [{"height"=>1, "size"=>5, "velocity"=>3}, {"height"=>1, "size"=>1, "velocity"=>3}, {"height"=>1, "size"=>1, "velocity"=>3}, {"height"=>1, "size"=>1, "velocity"=>3}, {"height"=>1, "size"=>2, "velocity"=>3}, {"height"=>1, "size"=>50, "velocity"=>3}, {"height"=>1, "size"=>3, "velocity"=>3}]
Longest sub-array found: {:start=>0, :finish=>6, :length=>7}
Result: 0

I’ve setup some extended validation in test_examples.rb:

$ ./test_examples.rb
Run options: --seed 54948

# Running:

.....

Finished in 0.000281s, 17793.5943 runs/s, 17793.5943 assertions/s.

5 runs, 5 assertions, 0 failures, 0 errors, 0 skips

More Optimizations?

Some possible optimisations that come to mind:

  • transform the data into a form that is easier to process. A few options:
    • put the data in a Matrix object for column-wise processing
    • or consider narray or numo-narray gems
    • or switch to python and numpy
  • short-circuit evaluation if it is known that a deficient average size cannot be recovered (the issue discussed above)
  • and clarify the requirement: “minimum velocity is 3” or “minimum velocity is at least 3”

I haven’t implemented any of these (yet).

Example Code

Final code is in examples.rb:

#!/usr/bin/env ruby
require 'json'

class GrandFinale
  attr_reader :input
  attr_reader :logging

  def initialize(input, logging = false)
    @input = input
    @logging = logging
  end

  def max_length
    @max_length ||= input.size
  end

  def input_sizes
    @input_sizes ||= input.map { |item| item['size'] }
  end

  def input_heights
    @input_heights ||= input.map { |item| item['height'] }
  end

  def input_velocities
    @input_velocities ||= input.map { |item| item['velocity'] }
  end

  def log_result(result)
    puts "Longest sub-array found: #{result}" if logging
  end

  def valid?(start, finish)
    sizes = input_sizes[start..finish]
    heights = input_heights[start..finish]
    velocities = input_velocities[start..finish]

    average_size = sizes.sum / sizes.size.to_f
    minimum_velocity = velocities.min
    maximum_height = heights.max
    minimum_height = heights.min

    average_size >= 5 &&
    minimum_velocity == 3 &&
    maximum_height - minimum_height <= 10
  end

  def initial_solution
    result = {length: 0}
    max_length.times do |start|
      (start..max_length - 1).each do |finish|
        length = finish - start + 1
        result = {start: start, finish: finish, length: length} if length > result[:length] && valid?(start, finish)
      end
    end
    log_result(result)
    result[:start]
  end
end

if __FILE__==$PROGRAM_NAME
  (puts "Usage: ruby #{$0} (json-file) (algorithm)"; exit) unless ARGV.length > 0
  json_file_name = ARGV[0]
  algorithm = ARGV[1] || 'initial_solution'
  begin
    input = JSON.parse(File.read(json_file_name))
  rescue
    puts "Error: Invalid JSON file - #{json_file_name}"
    exit
  end
  puts "Using algorithm: #{algorithm}"
  calculator = GrandFinale.new(input, true)
  puts "Input array: #{calculator.input.inspect}"
  puts "Result: #{calculator.send(algorithm)}"
end

Credits and References

About LCK#339 rubycassidoo

This page is a web-friendly rendering of my project notes shared in the LittleCodingKata GitHub repository.

Project Source on GitHub Return to the LittleCodingKata Catalog
About LittleCodingKata

LittleCodingKata is my collection of programming exercises, research and code toys broadly spanning things that relate to programming and software development (languages, frameworks and tools).

These range from the trivial to the complex and serious. Many are inspired by existing work and I'll note credits and references where applicable. The focus is quite scattered, as I variously work on things new and important in the moment, or go back to revisit things from the past.

This is primarily a personal collection for my own edification and learning, but anyone who stumbles by is welcome to borrow, steal or reference the work here. And if you spot errors or issues I'd really appreciate some feedback - create an issue, send me an email or even send a pull-request.

Follow the Blog follow projects and notes as they are published in your favourite feed reader