Cassidoo’s Interview question of the week | 447

Hello :waving_hand:

This week question is about swapping adjacent characters. Here it is

Given a string s consisting only of ‘a’ and ‘b’, you may swap adjacent characters any number of times. Return the minimum number of adjacent swaps needed to transform s into an alternating string, either “ababab…” or “bababa…”, or return -1 if it’s impossible.

minSwapsToAlternate('aabb')
1

minSwapsToAlternate('aaab')
-1

minSwapsToAlternate('aaaabbbb')
6

Happy coding!

2 Likes

It wasn’t easy for me. Got some help form AI :eyes:

Solution 😱
def min_swaps_to_alternate(s)
  n = s.length
  a = s.chars.each_with_index.filter_map { |c, i| i if c == 'a' }
  b_count = n - a.length

  swaps = ->(a_starts) {
    expected = a_starts ? (n + 1) / 2 : n / 2
    a.each_with_index.sum { |p, i| (p - (a_starts ? 2 * i : 2 * i + 1)).abs } if a.length == expected
  }

  if n.even?
    a.length == b_count ? [swaps.(true), swaps.(false)].min : -1
  elsif a.length == (n + 1) / 2 then swaps.(true)
  elsif b_count  == (n + 1) / 2 then swaps.(false)
  else -1
  end
end
2 Likes

At first, it looks pretty easy, but then I ended up inside the rabbit hole, for example, should it also work for strings pasted below as well? Don’t worry, only test examples are blurred.

@examples = [
  {
    string: "aabb",
    expected: 1,
  },
  {
    string: "aaaabbbb",
    expected: 6,
  },
  {
    string: "aaab",
    expected: -1,
  },
  # Extra examples
  {
    string: "baaa",
    expected: -1,
  },
  {
    string: "ab",
    expected: 0,
  },
  {
    string: "aab",
    expected: 1,
  },
  {
    string: "aabbb",
    expected: 3,
  },
  {
    string: "aaabb",
    expected: 3,
  },
  {
    string: "aabbaabab",
    expected: 3,
  },
]

I managed to have code to pass all but the last example, and I also managed to not read your solution @eayurt—and I am also on the brink of consulting with the AI, as this seems like a real-world problem that somebody somewhere somehow most definitely solved.

BTW, maybe it would be cool to use “Blur spoiler” functionality of this forum platform to blur the solution code (like I did with the code above) or put code inside “Hide details” (like I did with the solution code below)?

I instinctively (hard)coded this, but I have a feeling that there is some kind of more general pattern hidden inside this…will cook it up for a few more days to see if some Eureka will show up… :upside_down_face:

SOLUTION! UNHIDE AT YOUR OWN RISK! :P
def min_swaps_to_alternate(string)
  chars   = string.chars
  iterate = string.size - 1
  swaps   = 0

  string.size.times
    iterate.times do |index|
      next unless chars[index] != chars[index + 1] &&
                  (chars[index + 1] == chars[index + 2] ||
                  (chars[index + 2].nil? && chars[index] == chars[index - 1]))

      chars[index], chars[index + 1] = chars[index + 1], chars[index]
      swaps += 1

      next unless index > 1 && chars[index - 1] == chars[index - 2] && chars[index] != chars[index - 1]

      chars[index], chars[index - 1] = chars[index - 1], chars[index]
      swaps += 1
    end

    iterate -= 1
  end

  chars[0] == chars[1] ? -1 : swaps
end
2 Likes

I think “Hide details” is the perfect solution as it also helps keep the threat concise.

Solution
def swaps_to_pattern str, pattern
  cache = Hash.new -1

  (0...str.size).map do |index|
    char = pattern[index % pattern.size]
    match_index = ((cache[char] + 1)...str.size).find{ str[it] == char }
    return unless match_index
    cache[char] = match_index
    index < match_index ? match_index - index : 0
  end.sum
end

def minSwapsToAlternate str
  ab = swaps_to_pattern str, "ab"
  ba = swaps_to_pattern str, "ba"
  ab ? ba ? [ab, ba].min : ab : ba || -1
end

puts minSwapsToAlternate "aaaabbbb" # => 6
1 Like

I agree, “Hide details” is most convenient.

@lpogic I like your solution, if I understand correctly, it is kind of a simulation, e.g., you can actually make swaps as you go through the string?

So, I knew the pattern was much simpler, but I was too deep in this “simulation” mode, so after I got a hint from the AI mentor (AI agent in ASK mode instructed to behave as Socrates :upside_down_face:) that the answer was hidden in the indexes, I was able to code this solution by myself:

Array.new and array sums...
def min_swaps_to_alternate(string)
  chars        = string.chars
  a_char_count = chars.count("a")
  b_char_count = chars.size - a_char_count

  return -1 if (a_char_count - b_char_count).abs > 1

  if chars.size.even?
    # The result would be the same if b_indexes were used instead
    a_indexes_in_abab_pattern = Array.new(chars.size / 2) { it * 2     }
    a_indexes_in_baba_pattern = Array.new(chars.size / 2) { it * 2 + 1 }
    a_indexes_in_chars        = []
    chars.each_with_index { |char, index| a_indexes_in_chars << index if char == "a" }

    [
      (a_indexes_in_chars.sum - a_indexes_in_abab_pattern.sum).abs,
      (a_indexes_in_chars.sum - a_indexes_in_baba_pattern.sum).abs,
    ].min
  else
    majority_char_indexes_in_odd_sized_string = Array.new((chars.size / 2) + 1) { it * 2 }
    majority_char                             = a_char_count > b_char_count ? "a" : "b"
    majority_char_indexes                     = []
    chars.each_with_index { |char, index| majority_char_indexes << index if char == majority_char }

    (majority_char_indexes.sum - majority_char_indexes_in_odd_sized_string.sum).abs
  end
end

And this solution is inspired by the Gemini 3 Pro performant version (after I solved the problem, I wondered how other models would model the answer), but it is a little more efficient because it directly calculates the sum of the first n even and odd numbers/indexes:

each_char.with_index performant solution
def min_swaps_to_alternate_performant(string)
  # The final result would be the same if we used b_char instead
  a_char_count       = 0
  a_char_indexes_sum = 0
  string.each_char.with_index do |char, index|
    if char == "a"
      a_char_count       += 1
      a_char_indexes_sum += index
    end
  end
  b_char_count = string.size - a_char_count

  return -1 if (a_char_count - b_char_count).abs > 1

  # Formula to calculate the sum of the first `n` odd and even numbers: `n**2` and `n * (n + 1)`
  # In our case: `a_char_count**2` and (because the first even number/index is always `0`)
  # `(a_char_count - 1) * ((a_char_count - 1) + 1)` -> `a_char_count**2 - a_char_count`
  if a_char_count > b_char_count     # "aba..." pattern
    (a_char_indexes_sum - (a_char_count**2 - a_char_count)).abs
  elsif a_char_count < b_char_count  # "bab..." pattern
    (a_char_indexes_sum - a_char_count**2).abs
  else
    a_char_indexes_in_baba_pattern_sum = a_char_count**2
    a_char_indexes_in_abab_pattern_sum = a_char_indexes_in_baba_pattern_sum - a_char_count

    [
      (a_char_indexes_sum - a_char_indexes_in_baba_pattern_sum).abs,
      (a_char_indexes_sum - a_char_indexes_in_abab_pattern_sum).abs,
    ].min
  end
end

And here are the benchmarks.

You’re right, it’s basically a simulation, I just skip the swap step.

The swaps_to_pattern method can accept any pattern, not just “ab” and “ba.” I might use it to solve more Cassidoo’s exercises :wink:

Great job with the benchmark. I was curious about the computational complexity. They all seem to be O(n), or am I wrong?