What functional method would help me to implement a speedback algorithm?

Viewed 96

Speedback is the merging of speed-dating with feedback: a 2 min. 1-on-1 talk with all members of a group of people.

I've made a similar question in Computer Science to get some clues about the algorithm, but it's hard to convert it to functional programming.

Consider the input:

val team = setOf("alexandre", "sergiu", "joana", "tiago", "susana", "david")

I'd like to do like a round robin where all team members have to give feedback to eachother. I want to group people in a way that maximizes the amount of simultaneous pairs so we reduce the total number of the rounds.

The desired output would be:

1st round:
"alexandre" to "sergiu"
"joana" to "tiago"
"susana" to "david"

2nd round:
"alexandre" to "david"
"sergiu" to "tiago"
"susana" to "joana"

3rd round:
...

until everyone "dates" everyone.

I have this:

fun main() {
    val team = setOf("alexandre", "sergiu", "joana", "tiago", "susana", "david")

    println(team.combinations(2))
}

the output so far is the 15 possible pairs (combinations). Then I'm not sure which functional method to use to get the output above. I took a look at groupBy, associateBy, partition but none seem appropriate - or I'm too newbie in FP.

2 Answers

I just turned the answer you got on Computer Science Stack Exchange into Kotlin.

fun <T> roundRobin(list: List<T>): List<List<Pair<T, T>>> {
    val n = list.size
    return (if (n % 2.0 == 0.0) roundRobinEven(n) else roundRobinOdd(n)).map { 
        round -> round.map {
            (first, second) -> Pair(list[first], list[second])
        }
    }
}

//For each round (i), (i, n-1) and then Pair((j+i)%(n-1), (n-1−j+i)%(n-1) for the rest
fun roundRobinEven(n: Int): List<List<Pair<Int, Int>>> = 
    List(n-1) { i -> (1..(n/2-1)).map{ j -> Pair((j+i)%(n-1), (n-1-j+i)%(n-1)) }.plusElement(Pair(i, n-1)) }
    
//For each round (i), Pair((j+i)%(n), (n−j+i)%(n)) where 1<=j<=(n-1)/2
fun roundRobinOdd(n: Int): List<List<Pair<Int, Int>>> =
    List(n) { i -> (1..(n-1)/2).map { j -> Pair((j+i)%n, (n-j+i)%n) } }

And you can use it like this. Each list inside represents a round

val team = listOf("alexandre", "sergiu", "joana", "tiago", "susana", "david")
val rounds = roundRobin(team2)
for (round in rounds) {
    println(round)
}

Output:

[(sergiu, susana), (joana, tiago), (alexandre, david)]
[(joana, alexandre), (tiago, susana), (sergiu, david)]
[(tiago, sergiu), (susana, alexandre), (joana, david)]
[(susana, joana), (alexandre, sergiu), (tiago, david)]
[(alexandre, tiago), (sergiu, joana), (susana, david)]

Link to Kotlin playground

Here's an algorithm written with my functional programming library. It is not ideal roundwise (takes 7 rounds for 6 people), but it gets you 15 combinations. It is not impossible to do exactly 5 rounds, please see the accepted answer. First the results

people [ 'A', 'B', 'C', 'D', 'E', 'F' ]
round 0 [ [ 'A', 'B' ], [ 'C', 'D' ], [ 'E', 'F' ] ]
round 1 [ [ 'A', 'C' ], [ 'B', 'D' ] ]
round 2 [ [ 'A', 'D' ], [ 'B', 'C' ] ]
round 3 [ [ 'A', 'E' ], [ 'B', 'F' ] ]
round 4 [ [ 'A', 'F' ], [ 'B', 'E' ] ]
round 5 [ [ 'C', 'E' ], [ 'D', 'F' ] ]
round 6 [ [ 'C', 'F' ], [ 'D', 'E' ] ]

Here's the implementation

const {
  pipe, fork, tap, switchCase,
  map, filter, transform,
  and, not, eq, get,
} = require('rubico')

const identity = x => x

// [person] => Map { person => (Set { person }) }
const makeTracker = transform(
  map(fork([
    person => person,
    () => new Set(),
  ])),
  new Map(),
)

const db = new Map()

const saveTracker = async tracker => { db.set('tracker', tracker) }

const hasTracker = async () => db.has('tracker')

const getTracker = async tracker => db.get('tracker')

const isNotIn = set => x => !set.has(x)

// { people, tracker } => { people, tracker, pairs }
const algorithm = ({ people, tracker }) => {
  const matchedThisRound = new Set()
  return fork({
    people: identity,
    tracker: () => tracker,
    pairs: transform(
      pipe([
        filter(isNotIn(matchedThisRound)),
        map(person => {
          for (const other of people) {
            // console.log('person:other', `${person}:${other}`)
            if (other === person) continue
            if (tracker.get(person).has(other)) continue
            if (matchedThisRound.has(other)) continue
            tracker.get(person).add(other)
            tracker.get(other).add(person)
            matchedThisRound.add(person)
            matchedThisRound.add(other)
            return [person, other]
          }
        }),
        filter(Array.isArray),
      ]),
      [],
    )
  })(people)
}

// [person, person, person, person, ...] => [[person, person], [person, person], ...]
const matchmake = pipe([
  fork({
    people: identity,
    tracker: switchCase([
      hasTracker, getTracker,
      makeTracker,
    ]),
  }),
  algorithm,
  tap(pipe([get('tracker'), saveTracker])),
])

const didEveryoneMatch = (tracker, people) => {
  for (const [, set] of tracker) {
    if (set.size < people.length - 1) return false // 1 for yourself
  }
  return true
}

const main = async () => {
  const people = ['A', 'B', 'C', 'D', 'E', 'F']
  console.log('people', people)
  let i = 0
  while (true) {
    const round = await matchmake(people)
    const { tracker, pairs } = round
    console.log('round', i, pairs)
    if (didEveryoneMatch(tracker, people)) break
    i += 1
  }
}

main()

some fun stats

  • 100 people takes 127 rounds
  • 53 people takes 63 rounds
  • 1000 people takes 1023 rounds

You can play with the amount of people yourself if you drop this in for people

const people = Array.from((function*() {
  for (let i = 0; i < 53; i++) yield `${i + 1}`
})())
Related