Ruby: Divide array into 2 arrays with the closest possible average - ruby

Background: I'm working on a "matchmaking system" for a small multiplayer video game side project. Every player has a rank from 0-10, every team has 4 players. I'm trying to find a good way to balance out the teams so that the average rank of both of them is as close as possible and the match is as fair as possible.
My current, flawed approach looks like this:
def create_teams(players)
teams = Hash.new{|hash, team| hash[team] = []}
players.sort_by(&:rank).each_slice(2) do |slice|
teams[:team1] << slice[0]
teams[:team2] << slice[1]
end
teams
end
This works decently well if the ranks are already pretty similar but it's not a proper solution to this problem.
For example, it fails in a situation like this:
require "ostruct"
class Array
def avg
sum.fdiv(size)
end
end
dummy_players = [9, 5, 5, 3, 3, 3, 2, 0].map{|rank| OpenStruct.new(rank: rank)}
teams = create_teams(dummy_players)
teams.each do |team, players|
ranks = players.map(&:rank)
puts "#{team} - ranks: #{ranks.inspect}, avg: #{ranks.avg}"
end
This results in pretty unfair teams:
team1 - ranks: [0, 3, 3, 5], avg: 2.75
team2 - ranks: [2, 3, 5, 9], avg: 4.75
Instead, I'd like the teams in this situation to be like this:
team1 - ranks: [0, 3, 3, 9], avg: 3.75
team2 - ranks: [2, 3, 5, 5], avg: 3.75

If there are n players, where n is an even number, there are
C(n) = n!/((n/2)!(n/2)!)
ways to partition the n players into two teams of n/2 players, where n! equals n-facorial. This is often expressed as the number of ways to choosing n/2 items from a collection of n items.
To obtain the partition that has a mimimum absolute difference in total ranks (and hence, in mean ranks), one would have to enumerate all C(n) partitions. If n = 8, as in this example, C(8) = 70 (see, for example, this online calculator). If, however, n = 16, then C(16) = 12,870 and C(32) = 601,080,390. This gives you an idea of how small n must be in order perform a complete enumeration.
If n is too large to enumerate all combinations you must resort to using a heuristic, or a subjective rule for partitioning the array of ranks. Here are two possibilities:
assign the highest rank element ("rank 1") to team A, assign elements with ranks 2 and 3 to team B, assign elements with ranks 4 and 5 to team A, and so on.
assign elements with ranks 1 and n to team A, elements with ranks 2 and n-1 to team B, and so on.
The trouble with heuristics is evaluating their effectiveness. For this problem, for every heuristic you devise there is an array of ranks for which the heuristic's performance is abysmal. If you know the universe of possible arrays of ranks and have a way of drawing unbiased samples you can evaluate the heuristic statistically. That generally is not possible, however.
Here is how you could examine all partitions. Suppose:
ranks = [3, 3, 0, 2, 5, 9, 3, 5]
Then we may perform the following calculations.
indices = ranks.size.times.to_a
#=> [0, 1, 2, 3, 4, 5, 6, 7]
team_a = indices.combination(ranks.size/2).min_by do |combo|
team_b = indices - combo
(combo.sum { |i| ranks[i] } - team_b.sum { |i| ranks[i] }).abs
end
#=> [0, 1, 2, 5]
team_b = indices - team_a
#=> [3, 4, 6, 7]
See Array#combination and Enumerable#min_by.
We see that team A players have ranks:
arr = ranks.values_at(*team_a)
#=> [3, 3, 0, 9]
and the sum of those ranks is:
arr.sum
#=> 15
Similarly, for team B:
arr = ranks.values_at(*team_b)
#=> [2, 5, 3, 5]
arr.sum
#=> 15
See Array#values_at.

Related

Why does my algorithm solution fail validation?

Here is a task i got:
Cards are laid out on the table in a row, each card has a natural number written on it. In one move, it is allowed to take a card either from the left or from the right end of the row. In total, you can make k moves. The final score is equal to the sum of the numbers on the selected cards. Determine what is the maximum score you can get at the end of the game.
Here`s my code:
def card_counter(arr, k):
if len(arr) == k:
return sum(arr)
rang = len(arr) // 2
left = arr[:rang]
right = list(reversed(arr[rang:]))
c = 0
for _ in range(k):
min_arr = left if sum(left) >= sum(
right) and len(left) > 0 else right
c += min_arr.pop(0)
return c
if __name__ == '__main__':
assert card_counter([1, 2, 3, 4, 5], 5) == 15
assert card_counter([0, 0, 0], 1) == 0
assert card_counter([150], 1) == 150
This code works on all variants that I have come up with, including extreme cases. But the system does not accept this option, automatic tests do not pass. Where can there be a mistake?
I cannot comment on the algorithm you implemented as you did not state it in non-algorithm terms and I am not familiar with the language you are using (which I am guessing is Python). I will present a simple solution written in Ruby, hoping that the description I give will make it understood by readers who do not know Ruby.
Suppose
deck = [1, 2, 5, 7, 1, 4, 6, 3]
nbr_moves = 4
ds = deck.size
#=> 8
After removing nbr_moves from the ends,
m = ds - nbr_moves
#=> 4
consecutive cards will remain. In Ruby we could write
best = (0..ds-1).each_cons(m).max_by { |arr| deck.values_at(*arr).sum }
#=> [3, 4, 5, 6]
to obtain the indices of deck, for which the sum of the associated values is maximum:
deck.values_at(*best).sum
#=> 18
where
deck.values_at(*best)
#=> [7, 1, 4, 6]
In view of the value of best ([3, 4, 5, 6]), we need to remove left = best.first #=> 3 elements from the left and nbr_moves - left #=> 1 element from the right. The order of the removals is not relevant.
Note that
enum = (0..ds-1).each_cons(m)
#=> #<Enumerator: 0..7:each_cons(4)>
returns an enumerator. We can convert this enumerator to an array to see the values it will generate.
enum.to_a
#=> [[0, 1, 2, 3], [1, 2, 3, 4], [2, 3, 4, 5], [3, 4, 5, 6], [4, 5, 6, 7]]
When, for example,
arr = [2, 3, 4, 5]
then
a = deck.values_at(*arr)
#=> [5, 7, 1, 4]
a.sum
#=> 17
Note that, having computed, for example,
t = [deck[i], deck[i+1],..., deck[j]].sum
the sum of
[deck[i+1], deck[i+2],..., deck[j+1]]
is seen to equal
t - deck[i] + deck[j+1]
This suggests a more efficient way to perform the calculations when m is large.

Algorithm problem: find minimum number of trips to ship all packages

There are n packages to ship from one city to another city, denoted by:
int nums[n],
where nums[i] is the weight (in kilos) of ith package.
For example:
nums = [1, 1, 2, 3, 4, 7, 8].
There's a truck of capacity K in kilos. For example: int K = 9.
Return the minimum number of trips needed to ship all packages.
For the given example, the result should be: 3 trips. And:
trip 1:
[1, 8]
trip 2:
[2, 7]
trip 3:
[1, 3, 4]

Find the maximum number of points per game

The input is an array of cards. In one move, you can remove any group of consecutive identical cards. For removing k cards, you get k * k points. Find the maximum number of points you can get per game.
Time limit: O(n4)
Example:
Input: [1, 8, 7, 7, 7, 8, 4, 8, 1]
Output: 23
Does anyone have an idea how to solve this?
To clarify, in the given example, one path to the best solution is
Remove Points Total new hand
3 7s 9 9 [1, 8, 8, 4, 8, 1]
1 4 1 10 [1, 8, 8, 8, 1]
3 8s 9 19 [1, 1]
2 1s 4 23 []
Approach
Recursion would fit well here.
First, identify the contiguous sequences in the array -- one lemma of this problem is that if you decide to remove at least one 7, you want to remove the entire sequence of three. From here on, you'll work with both cards and quantities. For instance,
card = [1, 8, 7, 8, 4, 8, 1]
quant = [1, 1, 3, 1, 1, 1, 1]
Now you're ready for the actual solving. Iterate through the array. For each element, remove that element, and add the score for that move.
Check to see whether the elements on either side match; if so, merge those entries. Recur on the remaining array.
For instance, here's the first turn of what will prove to be the optimal solution for the given input:
Choose and remove the three 7's
card = [1, 8, 8, 4, 8, 1]
quant = [1, 1, 1, 1, 1, 1]
score = score + 3*3
Merge the adjacent 8 entries:
card = [1, 8, 4, 8, 1]
quant = [1, 2, 1, 1, 1]
Recur on this game.
Improvement
Use dynamic programming: memoize the solution for every sub game.
Any card that appears only once in the card array can be removed first, without loss of generality. In the given example, you can remove the 7's and the single 4 to improve the remaining search tree.

split the contents of the array into two other arrays

Simple example but I want to understand how it is done so I can apply it else where I have a main array with 6 elements. I want to take 3 of the elements from the main array and put it in a array and then take the other 3 from main array and put them in b array. I will use this to apply it to dealing cards to two players
main = [1, 2, 3, 4, 5, 6]
a = [ ]
b = [ ]
main = [1, 2, 3, 4, 5, 6]
#=> [1, 2, 3, 4, 5, 6]
main.first(3)
#=> [1, 2, 3]
main.last(3)
#=> [4, 5, 6]
a = [1, 2, 3, 4, 5, 6]
#=> [1, 2, 3, 4, 5, 6]
b = a.take(3)
#=> [1, 2, 3]
c = a.drop(3)
#=> [4, 5, 6]
All may have given the right answer, But as I understood from your question (I will use this to apply it to dealing cards to two players) When you dealing cards, as you deal cards to player main array should remove that element from self array to overcome Redundancy Problem (duplication). When you deal the all cards main array must be empty.
For this solution have a look at Array#shift
> main = [1,2,3,4,5,6] # I have 6 cards on my hand before dealing cards to players
=> [1, 2, 3, 4, 5, 6]
> a = main.shift(3) # given 3 cards to Player a
=> [1, 2, 3]
> b = main.shift(3) # given 3 cards to Player b
=> [4, 5, 6]
> main # after dealing all cards to two players I should not have any card on my hand
=> []
You have many ways to do the same thing in Ruby. Splitting arrays isn't an exception. Many answers (and comments) told you some of the ways to do that. If your program is dealing cards, you won't stop there. First, you'll probably have more than 6 cards. Second, you're probably going to have more than 2 players. Let's say the cards are C and the players are P. You need to write a method that, no matter how many Cs or Ps there are, the method is going to give each Player an equal number of Ccards (or return an error if it can't give it an equal number of cards). So for 6 cards and 2 players, it will give 3 cards each. For 12 cards and 3 players, 4 cards each. For 3 cards and 2 players, it's going to produce an error because the cards can't be evenly split:
def split_cards_evenly_between_players(cards, players)
if cards.size % players != 0
raise 'Cannot split evenly!'
else
groups_to_split_into = cards.size / players
cards.each_slice(groups_to_split_into).to_a
end
end
Let's go through the code. If the cards can't be evenly split between players, then the remainder by dividing them won't be 0 (6 cards / 3 players = remainder 0. 7 cards / 3 players = remainder 1). That's what line 2 checks. If the cards CAN be split, then we first find the groups to split into (which is dividing the number of cards by the number of players). Then we just split the array into that many groups with Enumerable#each_slice. Finally, since this doesn't produce an array, we need .to_a to convert it. The return value in Ruby is always the value of the last expression executed. The only expression in this method is the if/then expression which also returns the value of the last expression executed (which is the line where each_slice is). Let's try it out:
p split_cards_evenly_between_players([1,2,3,4,5,6,7,8,9,10,11,12],2) #=> [[1, 2, 3, 4, 5, 6], [7, 8, 9, 10, 11, 12]]
p split_cards_evenly_between_players([4,5,1,2,5,3], 3) #=> [[4, 5], [1, 2], [5, 3]]
p split_cards_evenly_between_players([1,2,3],2) #=> Error: Cannot split evenly!
The nice thing about Ruby is its simple syntax and the fact it tries to get out of your way while solving a problem so you can focus more on the actual problem than the code.

Partitioning a superset and getting the list of original sets for each partition

Introduction
While trying to do some cathegorization on nodes in a graph (which will be rendered differenty), I find myself confronted with the following problem:
The Problem
Given a superset of elements S = {0, 1, ... M} and a number n of non-disjoint subsets T_i thereof, with 0 <= i < n, what is the best algorithm to find out the partition of the set S called P?
P = S is the union of all disjoint partitions P_j of the original superset S, with 0 <= j < M, such that for all elements x in P_j, every x has the same list of "parents" among the "original" sets T_i.
Example
S = [1, 2, 3, 4, 5, 6, 8, 9]
T_1 = [1, 4]
T_2 = [2, 3]
T_3 = [1, 3, 4]
So all P_js would be:
P_1 = [1, 4] # all elements x have the same list of "parents": T_1, T_3
P_2 = [2] # all elements x have the same list of "parents": T_2
P_3 = [3] # all elements x have the same list of "parents": T_2, T_3
P_4 = [5, 6, 8, 9] # all elements x have the same list of "parents": S (so they're not in any of the P_j
Questions
What are good functions/classes in the python packages to compute all P_js and the list of their "parents", ideally restricted to numpy and scipy? Perhaps there's already a function which does just that
What is the best algorithm to find those partitions P_js and for each one, the list of "parents"? Let's note T_0 = S
I think the brute force approach would be to generate all 2-combinations of T sets and split them in at most 3 disjoint sets, which would be added back to the pool of T sets and then repeat the process until all resulting Ts are disjoint, and thus we've arrived at our answer - the set of P sets. A little problematic could be caching all the "parents" on the way there.
I suspect a dynamic programming approach could be used to optimize the algorithm.
Note: I would have loved to write the math parts in latex (via MathJax), but unfortunately this is not activated :-(
The following should be linear time (in the number of the elements in the Ts).
from collections import defaultdict
S = [1, 2, 3, 4, 5, 6, 8, 9]
T_1 = [1, 4]
T_2 = [2, 3]
T_3 = [1, 3, 4]
Ts = [S, T_1, T_2, T_3]
parents = defaultdict(int)
for i, T in enumerate(Ts):
for elem in T:
parents[elem] += 2 ** i
children = defaultdict(list)
for elem, p in parents.items():
children[p].append(elem)
print(list(children.values()))
Result:
[[5, 6, 8, 9], [1, 4], [2], [3]]
The way I'd do this is to construct an M × n boolean array In where In(i, j) &equals; Si &in; Tj. You can construct that in O(Σj|Tj|), provided you can map an element of S onto its integer index in O(1), by scanning all of the sets T and marking the corresponding bit in In.
You can then read the "signature" of each element i directly from In by concatenating row i into a binary number of n bits. The signature is precisely the equivalence relationship of the partition you are seeking.
By the way, I'm in total agreement with you about Math markup. Perhaps it's time to mount a new campaign.

Resources