Nurse Scheduling With Varying Number of Shifts Per Day and Varying Nurse Availability - job-scheduling

I am building a CP-SAT model using Google OR Tools in C# to solve a variation of the nurse scheduling problem in which there are a variable number of shifts per day and a variable number of nurses available on any given day to work those shifts.
Following this example from ShiftSchedulingSat.cs, I see there is a way to implement this easily if the number of shifts per day and number of employees per day is known. How can I modify this to work with my requirements?
var model = new CpModel();
IntVar[,,] work = new IntVar[numEmployees, numShifts, numDays];
foreach (int e in Range(numEmployees))
{
foreach (int s in Range(numShifts))
{
foreach (int d in Range(numDays))
{
work[e, s, d] = model.NewBoolVar($"work{e}_{s}_{d}");
}
}
}

Create the maximum of nurses, and force the number of off-shifts, or force some nurses to have an off-shift.

Related

Using the first row in bin (instead of average) to calculate percentage gain

In the dc.js Nasdaq example, percentageGain is calculated as:
(p.absGain / p.avgIndex) * 100
Here avgIndex is the average of all the day-averages.
I'm more familiar with the equation:
A. (Price - Prev period's Close) / Prev period's Close * 100
I'm not sure whether this is possible (with filters set and so on), the way crossfilter/dc works. Therefor, an alternative and different equation ,that might fit crossfilter/dc better and would still be meaningful, could be:
B. absGain of group / open of first day of group * 100
B would also mean that: If only a filter is set on for example Q1, then only the absGain of Q1 is taken into account. The first day in this group is the the oldest Q1 date in the oldest year. Also, charts other than "yearly" with groups like quarter, month or day of the week should be able to display the value of this equation. For example in a month chart, the value of the month "June" is calculated by taking the open of the first day in the first June. The absGain is taken from all June months. (of course working with all current filters in place)
Question: Can A and/or B be solved the crossfilter/dc way and how (example)?
Even if only B could be solved (naturally with crossfilter/dc), that would already be great. I want to use the dc.js example for other stocks that have the same underlying data structure (open, close, high, low, volume)
thanks!
I agree that Equation B is easier to define using crossfilter, so I figured out one way to do it.
Equation A could probably work but it's unclear which day's close should be used under filtering - the last day which is not in the current bin? The day before the first day in the current bin?
Equation B needs the earliest row for the current bin, and that requires maintaining the array of all rows for each bin. This is not built into crossfilter but it's a feature which we have talked about adding.
The complex reduce example does this, and we can reuse some of its code. It calculates the median/mode/min/max value from the arrays of rows which fall in each bin, using these functions to generate those arrays:
function groupArrayAdd(keyfn) {
var bisect = d3.bisector(keyfn);
return function(elements, item) {
var pos = bisect.right(elements, keyfn(item));
elements.splice(pos, 0, item);
return elements;
};
}
function groupArrayRemove(keyfn) {
var bisect = d3.bisector(keyfn);
return function(elements, item) {
var pos = bisect.left(elements, keyfn(item));
if(keyfn(elements[pos])===keyfn(item))
elements.splice(pos, 1);
return elements;
};
}
It's somewhat inefficient to maintain all these arrays, so you might test if it has an impact on your application. JS is pretty fast so it probably doesn't matter unless you have a lot of data.
Unfortunately there is no other way to compute the minimum for a bin other than to keep an array of all the items in it. (If you tried to keep track of just the lowest item, or lowest N items, what would you do when they are removed?)
Using these arrays inside the group reduce-add function:
(p, v) => {
++p.count;
p.rowsByDate = rbdAdd(p.rowsByDate, v);
p.absGain += v.close - v.open;
// ...
p.percentageGain = p.rowsByDate.length ? (p.absGain / p.rowsByDate[0].open) * 100 : 0;
return p;
},
In the reduce-remove function it's
p.rowsByDate = rbdRemove(p.rowsByDate, v);
and the same percentageGain change.
Here is a demo in a notebook: https://jsfiddle.net/gordonwoodhull/08bzcd4y/17/
I only see slight changes in the Y positions of the bubbles; the changes are more apparent in the values printed in the tooltip.

What is the largest number of people that were in the city at the same period?

a problem has been discussed in the class today says :
n paris are given (ai,bi), each pair stands for a human and ai,bi represent his entrance date and exit date from the city for 2019.
the question, what was the largest number of people were in the city at the same period.
I tried to cast the dates to [1,365] (Integers), and insert them entrance to one AVL and the exit dates to another and save pointers from both of them traversing one tree and updating the maximum if needed.
I beileve this soultion is a naive one since it takes O(n^2).
The data-structers that we learend are:
Array,Linked-List,Queue,Stack,Heap,BST,AVL,Heap,Hash-Table,SkipList and Graph.
You can use array for this logic.
Since number of days in the year is fixed, create an array to keep count of number of people in the city in that particular day.
count[365] = {0}; //Reset the counter, all entries should be zero.
maxCount = 0;
maxDay = -1;
for(day from 0 to (365-1))
{
for(i from 0 to (n-1) person)
{
if(day >= ai && day <= bi) //Update this check based on whether ai and bi are inclusive or not.
{
count[day] = count[day]+1;
if(count[day] > maxCount) //Keep track of maxCount, if required update it.
{
maxCount = count[day];
maxDay = day;
}
}
}
}
Output maxDay, maxCount;
The time complexity of the above logic is O(365*n) => O(n).

Finding Cheapest Combination of Tickets

I'm writing a web application to help commuters determine the cheapest combination of tickets to purchase for their daily commute based on how many trips they take in a month.
I've done a bunch of reading on this, but I'm still not quite sure how I would implement this. I think it could be similar to the knapsack problem or some other questions on here, but I still find the answers quite confusing.
There are three ticket types:
Single Trip: $6.50
10 Trips: $49.80
Monthly Pass: $149.40
I want to be able to compute the combination of tickets that has the lowest total price which covers a given number of monthly trips for a commuter.
Example:
For 12 trips in a month, the cheapest combination is one 10 Trip pass and two Single passes for a total of $62.80.
Is anyone able to point me in the right direction (with pseudocode or otherwise) on how I would implement this? Thanks in advance! :)
Explanation
As sascha suggested in their comment, I was actually able to implement this fairly simply.
With n as the total number of trips:
The number of ten trip tickets to use is (n % 10)
The number of single tickets to use is (n / 10) using integer division
If the total cost of these tickets is greater than the cost of a monthly ticket, use a monthly ticket.
The Code
for (i = 0; i < Math.floor(totalNumTrips / 10); i++) {
ticketsToBuy.push(TicketType.TenTrip);
cost += tenTripPrice;
}
for (i = 0; i < totalNumTrips % 10; i++) {
ticketsToBuy.push(TicketType.Single);
cost += singlePrice;
}
if (monthlyPrice < cost) {
return {
tickets: [TicketType.Monthly],
cost: monthlyPrice,
};
}
return {
tickets: ticketsToBuy,
cost: cost,
};
Hopefully this is helpful to someone!

Finding a majority of unorderable items

I have this one problem with finding solution to this task.
You have N students and N courses. Student can attend only one course
and one course can be attended by many students. Two students are
classmates if they are attending same course. How to find out if there
are N/2 classmates in N students with this?
conditions: You can take two students and ask if they are classmates
and only answer you can get is "yes" or "no". And you need to do this
in O(N*log(N)).
I need just some idea how to make it, pseudo code will be fine. I guess it will divide the list of students like merge sort, which gives me the logarithmic part of complexity. Any ideas will be great.
First, pair off each student (1&2, 3&4, 5&6... etc), and you check and see which pairs are in the same class. The first student of the pair gets "promoted". If there an "oddball" student, they are in their own class, so they get promoted as well. If a single class contains >=50% of the students, then >=50% of the promoted students are also in this class. If no students are promoted, then if a single class contains >=50% of the students then either the first or the second student must be in the class, so simply promote both of them. This leads to the case where >=50% of the promotions are in the large class. This always takes ⌊N/2⌋ comparisons.
Now when we examine the promoted students, then if a class contains >=50% of the students, then >=50% of the promoted students are in this class. Therefore, we can simply recurse, until we reach a stop condition: there are less than three promoted students. At each step we promote <=50% of the students (plus one sometimes), so this step occurs at most ⌈log(N,2)⌉ times.
If there are less than three promoted students, then we know that if >=50% of the original students are in the class, then at least one of these remaining students is in that class. Therefore, we can simply compare each and every original student against these promoted students, which will reveal either (A) the class with >=50% of the students, or (B) that no class has >=50% of the students. This takes at most (N-1) comparisons, but only occurs once. Note that there is the possibility where all the original students match with one of the two remaining students evenly, and this detects that both classes have =50% of the students.
So the complexity is N/2 *~ log(N,2) + N-1. However, the *~ signifies that we don't iterate over all N/2 students at each of the log(N,2) iterations, only decreasing fractions N/2, N/4, N/8..., which sum to N. So the total complexity is N/2 + N/2 + N-1 = 2N-1, and when we remove the constants we get O(N). (I feel like I may have made a math error in this paragraph. If someone spots it, let me know)
See it in action here: http://coliru.stacked-crooked.com/a/144075406b7566c2 (The comparison counts may be slightly over the estimate due to simplifications I made in the implementation)
Key here is that if >50% of the students are in a class, then >=50% of the arbitrary pairs are in that class, assuming the oddball student matches himself. One trick is that if exactly 50% match, it's possible that they alternate in the original order perfectly and thus nobody gets promoted. Luckily, the only cases is the alternating, so by promoting the first and second students, then even in that edge case, >=50% of the promotions are in the large class.
It's complicated to prove that >=50% of the promotions are in the large class, and I'm not even certain I can articulate why this is. Confusingly, it also doesn't hold prettily for any other fractions. If the target is >=30% of the comparisons, it's entirely possible that none of the promoted students are in the target class(s). So >=50% is the magic number, it isn't arbitrary at all.
If one can know the number of students for each course then it should suffice to know if there is a course with a number of students >= N/2. In this case you have a complexity of O(N) in the worst case.
If it is not possible to know the number of students for each course then you could use an altered quicksort. In each cycle you pick a random student and split the other students in classmates and non-classmates. If the number of classmates is >= N/2 you stop because you have the answer, else you analyze the non-classmates partition. If the number of students in that partition is < N/2 you stop because it is not possible to have a quantity of classmates >= N/2, else you pick another student from the non-classmates partition and repeat everything using only the non-classmates elements.
What we take from the quicksort algorithm is just the way we partition the students. The above algorithm has nothing to do with sorting. In pseudo-code it would look something like this (array indexing starts from 1 for the sake of clarity):
Student[] students = all_students;
int startIndex = 1;
int endIndex = N; // number of students
int i;
while(startIndex <= N/2){
endIndex = N; // this index must point to the last position in each cycle
students.swap(startIndex, start index + random_int % (endIndex-startIndex));
for(i = startIndex + 1; i < endIndex;){
if(students[startIndex].isClassmatesWith(students[i])){
i++;
}else{
students.swap(i,endIndex);
endIndex--;
}
}
if(i-startIndex >= N/2){
return true;
}
startIndex = i;
}
return false;
The situation of the partition before the algorithm starts would be as simple as:
| all_students_that_must_be_analyzed |
during the first run the set of students would be partitioned this way:
| classmates | to_be_analyzed | not_classmates |
and during each run after it, the set of students would be partitioned as follows:
| to_ignore | classmates | to_be_analyzed | not_classmates |
In the end of each run the set of students would be partitioned in the following way:
| to_ignore | classmates | not_classmates |
At this moment we need to check if the classmates partition has more than N/2 elements. If it has, then we have a positive result, if not we need to check if the not_classmates partition has >= N/2 elements. If it has, then we need to proceed with another run, otherwise we have a negative result.
Regarding complexity
Thinking more in depth on the complexity of the above algorithm, there are two main factors that affect it, which are:
The number of students in each course (it's not necessary to know this number for the algorithm to work).
The average number of classmates found in each iteration of the algorithm.
An important part of the algorithm is the random choice of the student to be analyzed.
The worst case scenario would be when each course has 1 student. In this case (for obvious reasons I would say) the complexity would be O(N^2). If the number of students for the courses varies then this case won't happen.
An example of the worst case scenario would be when we have, let's say, 10 students, 10 courses, and 1 student for each course. We would check 10 students the first time, 9 students the second time, 8 students the third time, and so on. This brings a O(N^2) complexity.
The best case scenario would be when the first student you choose is in a course with a number of students >= N/2. In this case the complexity would be O(N) because it would stop in the first run.
An example of the best case scenario would be when we have 10 students, 5 (or more) of which are classmates, and in the first run we pick one of these 5 students. In this case we would check only 1 time for the classmates, find 5 classmates, and return true.
The average case scenario is the most interesting part (and more close to a real-world scenario). In this case there are some probabilistic calculations to make.
First of all, the chances of a student from a particular course to be picked are [number_of_students_in_the_course] / N. This means that, in the first runs it's more probable to pick a student with many classmates.
That being said, let's consider the case where the average number of classmates found in each iteration is a number smaller that N/2 (as is the length of each partition in the average case for quicksort). Let's say that the average amount of classmates found in each iteration is 10% (number taken for ease of calculations) of the remaining M students (that are not classmates of the previously picked students). In this case we would have these values of M for each iteration:
M1 = N - 0.1*N = 0.9*N
M2 = M1 - 0.1*M1 = 0.9*M1 = 0.9*0.9*N = 0.81*N
M3 = M2 - 0.1*M2 = 0.9*M2 = 0.9*0.81*N = 0.729*N and I would round it to 0.73*N for ease of calculations
M4 = 0.9*M3 = 0.9*0.73*N = 0.657*N ~= 0.66*N
M5 = 0.9*M4 = 0.9*0.66*N = 0.594*N ~= 0.6*N
M6 = 0.9*M5 = 0.9*0.6*N = 0.54*N
M7 = 0.9*M6 = 0.9*0.54*N = 0.486*N ~= 0.49*N
The algorithm stops because we have 49% of remaining students and we can't have more than N/2 classmates among them.
Obviously, in the case of a smaller percentage of average classmates the number of iterations will be greater but, combined with the first fact (the students in courses with many students have a higher probability to get picked in the early iterations), the complexity will tend towards O(N), the number of iterations (in the outer cycle in the pseudo-code) will be (more or less) constant and will not depend on N.
To better explain this scenario let's work with greater (but more realistic) numbers and more than 1 distribution. Let's say that we have 100 students (number taken for the sake of simplicity in calculations) and that these students are distributed among the courses in one of the following (hypothetical) ways (the numbers are sorted just for explanation purposes, they are not necessary for the algorithm to work):
50, 30, 10, 5, 1, 1, 1, 1, 1
35, 27, 25, 10, 5, 1, 1, 1
11, 9, 9, 8, 7, 7, 5, 5, 5, 5, 5, 5, 5, 5, 5, 3, 1
The numbers given are also (in this particular case) the probabilities that a student in a course (not a particular student, just a student of that course) is picked in the first run. The 1st case is when we have a course with half the students. The 2nd case is when we don't have a course with half the students but more than 1 course with many students. The 3rd case is when we have a similar distribution among the courses.
In the 1st case we would have a 50% probability that a student of the 1st course gets picked in the first run, 30% probability that a student of the 2nd course gets picked, 10% probability that a student of the 3rd course gets picked, 5% probability that a student of the 4th course gets picked, 1% that a student from the 5th course gets picked, and so on for the 6th, 7th, 8th, and 9th course. The probabilities are higher for a student of the 1st case to get picked early, and in the case a student from this course does not get picked in the first run the probabilities it gets picked in the second run only increase. For example, let's suppose that in the 1st run a student from the second course is picked. 30% of the students would be "removed" (as in "not considered anymore") and not be analyzed in the 2nd run. In the 2nd run we would have 70 students remaining. The probability to pick a student from the 1st course in the second run would be 5/7, more than 70%. Let's suppose that - out of bad luck - in the 2nd run a student from the 3rd course gets picked. In the 3rd run we would have 60 students left and the probability that a student from the first course gets picked in the 3rd run would be 5/6 (more than 80%). I would say that we can consider our bad luck to be over in the 3rd run, a student from the 1st course gets picked, and the method returns true :)
For the 2nd and 3rd case I would follow the probabilities for each run, just for the sake of simplicity of calculations.
In the 2nd case we would have a student from the 1st course picked in the 1st run. Being that the number of classmates is not <= N/2 the algorithm would go on with the 2nd run. In the end of the 2nd run we would have "removed" from the student set 35+27=62 students. In the 3rd run we would have 38 students left, and being that 38 < (N/2) = 50 the computation stops and returns false.
The same happens in the 3rd case (in which we "remove" an average of 10% of the remaining students in each run), but with more steps.
Final considerations
In any case, the complexity of the algorithm in the worst case scenario is O(N^2). The average case scenario is heavily based on probabilities and tends to pick early the students from courses with many attendees. This behaviour tends to bring the complexity down to O(N), complexity that we also have in the best case scenario.
Test of the algorithm
In order to test the theoretical complexity of the algorithm I wrote the following code in C#:
public class Course
{
public int ID { get; set; }
public Course() : this(0) { }
public Course(int id)
{
ID = id;
}
public override bool Equals(object obj)
{
return (obj is Course) && this.Equals((Course)obj);
}
public bool Equals(Course other)
{
return ID == other.ID;
}
}
public class Student
{
public int ID { get; set; }
public Course Class { get; set; }
public Student(int id, Course course)
{
ID = id;
Class = course;
}
public Student(int id) : this(id, null) { }
public Student() : this(0) { }
public bool IsClassmatesWith(Student other)
{
return Class == other.Class;
}
public override bool Equals(object obj)
{
return (obj is Student) && this.Equals((Student)obj);
}
public bool Equals(Student other)
{
return ID == other.ID && Class == other.Class;
}
}
class Program
{
static int[] Sizes { get; set; }
static List<Student> Students { get; set; }
static List<Course> Courses { get; set; }
static void Initialize()
{
Sizes = new int[] { 2, 10, 100, 1000, 10000, 100000, 1000000 };
Students = new List<Student>();
Courses = new List<Course>();
}
static void PopulateCoursesList(int size)
{
for (int i = 1; i <= size; i++)
{
Courses.Add(new Course(i));
}
}
static void PopulateStudentsList(int size)
{
Random ran = new Random();
for (int i = 1; i <= size; i++)
{
Students.Add(new Student(i, Courses[ran.Next(Courses.Count)]));
}
}
static void Swap<T>(List<T> list, int i, int j)
{
if (i < list.Count && j < list.Count)
{
T temp = list[i];
list[i] = list[j];
list[j] = temp;
}
}
static bool AreHalfOfStudentsClassmates()
{
int startIndex = 0;
int endIndex;
int i;
int numberOfStudentsToConsider = (Students.Count + 1) / 2;
Random ran = new Random();
while (startIndex <= numberOfStudentsToConsider)
{
endIndex = Students.Count - 1;
Swap(Students, startIndex, startIndex + ran.Next(endIndex + 1 - startIndex));
for (i = startIndex + 1; i <= endIndex; )
{
if (Students[startIndex].IsClassmatesWith(Students[i]))
{
i++;
}
else
{
Swap(Students, i, endIndex);
endIndex--;
}
}
if (i - startIndex + 1 >= numberOfStudentsToConsider)
{
return true;
}
startIndex = i;
}
return false;
}
static void Main(string[] args)
{
Initialize();
int studentsSize, coursesSize;
Stopwatch stopwatch = new Stopwatch();
TimeSpan duration;
bool result;
for (int i = 0; i < Sizes.Length; i++)
{
for (int j = 0; j < Sizes.Length; j++)
{
Courses.Clear();
Students.Clear();
studentsSize = Sizes[j];
coursesSize = Sizes[i];
PopulateCoursesList(coursesSize);
PopulateStudentsList(studentsSize);
Console.WriteLine("Test for {0} students and {1} courses.", studentsSize, coursesSize);
stopwatch.Start();
result = AreHalfOfStudentsClassmates();
stopwatch.Stop();
duration = stopwatch.Elapsed;
var studentsGrouping = Students.GroupBy(s => s.Class);
var classWithMoreThanHalfOfTheStudents = studentsGrouping.FirstOrDefault(g => g.Count() >= (studentsSize + 1) / 2);
Console.WriteLine(result ? "At least half of the students are classmates." : "Less than half of the students are classmates");
if ((result && classWithMoreThanHalfOfTheStudents == null)
|| (!result && classWithMoreThanHalfOfTheStudents != null))
{
Console.WriteLine("There is something wrong with the result");
}
Console.WriteLine("Test duration: {0}", duration);
Console.WriteLine();
}
}
Console.ReadKey();
}
}
The execution time matched the expectations of the average case scenario. Feel free to play with the code, you just need to copy and paste it and it should work.
I will post some of my ideas..
First of all, I think that we need to do something like mergesort, to make that logarithmical part... I thought, that at the lowest level, where we have just 2 students to compare, we just ask and got an answer. But that doesnt solve anything. In this case, we will just have N/2 pairs of students and knowledge either they are classmates or not so ever. And this doesnt help..
Next idea was little bit better. I didnt divide that set to minimum level, but i stopped when i had sets of 4 students. so I had N/4 little sets where I compared everyone to each other. And if I found, that at least two of them are classmates, that was good. If not, and all of them was from different class, I completly forgot that group of 4. When I applyed this to each group, I started to joining them to groups of 8 just by comparing those, who were already flagged as classmates. (thanks to transitivity). And again... if there were at least 4 classmates, in group of 8, I was happy and if not, I forgot about that group. This ought to be repeated until i have two sets of students and make one comparsion on students from both sets to got final answer. BUT problem is, that in there can be n/2-1 classmates in one half and in another half just one student matching with them.. and this agorithm doesnt work with this idea.

Algorithm to share/settle expenses among a group

I am looking forward for an algorithm for the below problem.
Problem: There will be a set of people who owe each other some money or none. Now, I need an algorithm (the best and neat) to settle expense among this group.
Person AmtSpent
------ ---------
A 400
B 1000
C 100
Total 1500
Now, expense per person is 1500/3 = 500. Meaning B to give A 100. B to give C 400. I know, I can start with the least spent amount and work forward.
Can some one point me the best one if you have.
To sum up,
Find the total expense, and expense per head.
Find the amount each owe or outstanding (-ve denote outstanding).
Start with the least +ve amount. Allocate it to the -ve amount.
Keep repeating step 3, until you run out of -ve amount.
s. Move to next bigger +ve number. Keep repeating 3 & 4 until there are +ve numbers.
Or is there any better way to do?
The best way to get back to zero state (minimum number of transactions) was covered in this question here.
I have created an Android app which solves this problem. You can input expenses during the trip, it even recommends you "who should pay next". At the end it calculates "who should send how much to whom". My algorithm calculates minimum required number of transactions and you can setup "transaction tolerance" which can reduce transactions even further (you don't care about $1 transactions) Try it out, it's called Settle Up:
https://market.android.com/details?id=cz.destil.settleup
Description of my algorithm:
I have basic algorithm which solves the problem with n-1 transactions, but it's not optimal. It works like this: From payments, I compute balance for each member. Balance is what he paid minus what he should pay. I sort members according to balance increasingly. Then I always take the poorest and richest and transaction is made. At least one of them ends up with zero balance and is excluded from further calculations. With this, number of transactions cannot be worse than n-1. It also minimizes amount of money in transactions. But it's not optimal, because it doesn't detect subgroups which can settle up internally.
Finding subgroups which can settle up internally is hard. I solve it by generating all combinations of members and checking if sum of balances in subgroup equals zero. I start with 2-pairs, then 3-pairs ... (n-1)pairs. Implementations of combination generators are available. When I find a subgroup, I calculate transactions in the subgroup using basic algorithm described above. For every found subgroup, one transaction is spared.
The solution is optimal, but complexity increases to O(n!). This looks terrible but the trick is there will be just small number of members in reality. I have tested it on Nexus One (1 Ghz procesor) and the results are: until 10 members: <100 ms, 15 members: 1 s, 18 members: 8 s, 20 members: 55 s. So until 18 members the execution time is fine. Workaround for >15 members can be to use just the basic algorithm (it's fast and correct, but not optimal).
Source code:
Source code is available inside a report about algorithm written in Czech. Source code is at the end and it's in English:
https://web.archive.org/web/20190214205754/http://www.settleup.info/files/master-thesis-david-vavra.pdf
You have described it already. Sum all the expenses (1500 in your case), divide by number of people sharing the expense (500). For each individual, deduct the contributions that person made from the individual share (for person A, deduct 400 from 500). The result is the net that person "owes" to the central pool. If the number is negative for any person, the central pool "owes" the person.
Because you have already described the solution, I don't know what you are asking.
Maybe you are trying to resolve the problem without the central pool, the "bank"?
I also don't know what you mean by "start with the least spent amount and work forward."
Javascript solution to the accepted algorithm:
const payments = {
John: 400,
Jane: 1000,
Bob: 100,
Dave: 900,
};
function splitPayments(payments) {
const people = Object.keys(payments);
const valuesPaid = Object.values(payments);
const sum = valuesPaid.reduce((acc, curr) => curr + acc);
const mean = sum / people.length;
const sortedPeople = people.sort((personA, personB) => payments[personA] - payments[personB]);
const sortedValuesPaid = sortedPeople.map((person) => payments[person] - mean);
let i = 0;
let j = sortedPeople.length - 1;
let debt;
while (i < j) {
debt = Math.min(-(sortedValuesPaid[i]), sortedValuesPaid[j]);
sortedValuesPaid[i] += debt;
sortedValuesPaid[j] -= debt;
console.log(`${sortedPeople[i]} owes ${sortedPeople[j]} $${debt}`);
if (sortedValuesPaid[i] === 0) {
i++;
}
if (sortedValuesPaid[j] === 0) {
j--;
}
}
}
splitPayments(payments);
/*
C owes B $400
C owes D $100
A owes D $200
*/
I have recently written a blog post describing an approach to solve the settlement of expenses between members of a group where potentially everybody owes everybody else, such that the number of payments needed to settle the debts is the least possible. It uses a linear programming formulation. I also show an example using a tiny R package that implements the solution.
I had to do this after a trip with my friends, here's a python3 version:
import numpy as np
import pandas as pd
# setup inputs
people = ["Athos", "Porthos", "Aramis"] # friends names
totals = [300, 150, 90] # total spent per friend
# compute matrix
total_spent = np.array(totals).reshape(-1,1)
share = total_spent / len(totals)
mat = (share.T - share).clip(min=0)
# create a readable dataframe
column_labels = [f"to_{person}" for person in people]
index_labels = [f"{person}_owes" for person in people]
df = pd.DataFrame(data=mat, columns=column_labels, index=index_labels)
df.round(2)
Returns this dataframe:
to_Athos
to_Porthos
to_Aramis
Athos_owes
0
0
0
Porthos_owes
50
0
0
Aramis_owes
70
20
0
Read it like: "Porthos owes $50 to Athos" ....
This isn't the optimized version, this is the simple version, but it's simple code and may work in many situations.
I'd like to make a suggestion to change the core parameters, from a UX-standpoint if you don't mind terribly.
Whether its services or products being expensed amongst a group, sometimes these things can be shared. For example, an appetizer, or private/semi-private sessions at a conference.
For things like an appetizer party tray, it's sort of implied that everyone has access but not necessarily that everyone had it. To charge each person to split the expense when say, only 30% of the people partook can cause contention when it comes to splitting the bill. Other groups of people might not care at all. So from an algorithm standpoint, you need to first decide which of these three choices will be used, probably per-expense:
Universally split
Split by those who partook, evenly
Split by proportion per-partaker
I personally prefer the second one in-general because it has the utility to handle whole-expense-ownership for expenses only used by one person, some of the people, and the whole group too. It also remedies the ethical question of proportional differences with a blanket generalization of, if you partook, you're paying an even split regardless of how much you actually personally had. As a social element, I would consider someone who had a "small sample" of something just to try it and then decided not to have anymore as a justification to remove that person from the people splitting the expense.
So, small-sampling != partaking ;)
Then you take each expense and iterate through the group of who partook in what, and atomically handle each of those items, and at the end provide a total per-person.
So in the end, you take your list of expenses and iterate through them with each person. At the end of the individual expense check, you take the people who partook and apply an even split of that expense to each person, and update each person's current split of the bill.
Pardon the pseudo-code:
list_of_expenses[] = getExpenseList()
list_of_agents_to_charge[] = getParticipantList()
for each expense in list_of_expenses
list_of_partakers[] = getPartakerList(expense)
for each partaker in list_of_partakers
addChargeToAgent(expense.price / list_of_partakers.size, list_of_agents_to_charge[partaker])
Then just iterate through your list_of_agents_to_charge[] and report each total to each agent.
You can add support for a tip by simply treating the tip like an additional expense to your list of expenses.
Straightforward, as you do in your text:
Returns expenses to be payed by everybody in the original array.
Negativ values: this person gets some back
Just hand whatever you owe to the next in line and then drop out. If you get some, just wait for the second round. When done, reverse the whole thing. After these two round everybody has payed the same amount.
procedure SettleDepth(Expenses: array of double);
var
i: Integer;
s: double;
begin
//Sum all amounts and divide by number of people
// O(n)
s := 0.0;
for i := Low(Expenses) to High(Expenses) do
s := s + Expenses[i];
s := s / (High(Expenses) - Low(Expenses));
// Inplace Change to owed amount
// and hand on what you owe
// drop out if your even
for i := High(Expenses) downto Low(Expenses)+1 do begin
Expenses[i] := s - Expenses[i];
if (Expenses[i] > 0) then begin
Expenses[i-1] := Expenses[i-1] + Expenses[i];
Expenses.Delete(i);
end else if (Expenses[i] = 0) then begin
Expenses.Delete(i);
end;
end;
Expenses[Low(Expenses)] := s - Expenses[Low(Expenses)];
if (Expenses[Low(Expenses)] = 0) then begin
Expenses.Delete(Low(Expenses));
end;
// hand on what you owe
for i := Low(Expenses) to High(Expenses)-1 do begin
if (Expenses[i] > 0) then begin
Expenses[i+1] := Expenses[i+1] + Expenses[i];
end;
end;
end;
The idea (similar to what is asked but with a twist/using a bit of ledger concept) is to use Pool Account where for each bill, members either pay to the Pool or get from the Pool.
e.g.
in below attached image, the Costco expenses are paid by Mr P and needs $93.76 from Pool and other members pay $46.88 to the pool.
There are obviously better ways to do it. But that would require running a NP time complexity algorithm which could really show down your application. Anyways, this is how I implemented the solution in java for my android application using Priority Queues:
class calculateTransactions {
public static void calculateBalances(debtors,creditors) {
// add members who are owed money to debtors priority queue
// add members who owe money to others to creditors priority queue
}
public static void calculateTransactions() {
results.clear(); // remove previously calculated transactions before calculating again
PriorityQueue<Balance> debtors = new PriorityQueue<>(members.size(),new BalanceComparator()); // debtors are members of the group who are owed money, balance comparator defined for max priority queue
PriorityQueue<Balance> creditors = new PriorityQueue<>(members.size(),new BalanceComparator()); // creditors are members who have to pay money to the group
calculateBalances(debtors,creditors);
/*Algorithm: Pick the largest element from debtors and the largest from creditors. Ex: If debtors = {4,3} and creditors={2,7}, pick 4 as the largest debtor and 7 as the largest creditor.
* Now, do a transaction between them. The debtor with a balance of 4 receives $4 from the creditor with a balance of 7 and hence, the debtor is eliminated from further
* transactions. Repeat the same thing until and unless there are no creditors and debtors.
*
* The priority queues help us find the largest creditor and debtor in constant time. However, adding/removing a member takes O(log n) time to perform it.
* Optimisation: This algorithm produces correct results but the no of transactions is not minimum. To minimize it, we could use the subset sum algorithm which is a NP problem.
* The use of a NP solution could really slow down the app! */
while(!creditors.isEmpty() && !debtors.isEmpty()) {
Balance rich = creditors.peek(); // get the largest creditor
Balance poor = debtors.peek(); // get the largest debtor
if(rich == null || poor == null) {
return;
}
String richName = rich.name;
BigDecimal richBalance = rich.balance;
creditors.remove(rich); // remove the creditor from the queue
String poorName = poor.name;
BigDecimal poorBalance = poor.balance;
debtors.remove(poor); // remove the debtor from the queue
BigDecimal min = richBalance.min(poorBalance);
// calculate the amount to be send from creditor to debtor
richBalance = richBalance.subtract(min);
poorBalance = poorBalance.subtract(min);
HashMap<String,Object> values = new HashMap<>(); // record the transaction details in a HashMap
values.put("sender",richName);
values.put("recipient",poorName);
values.put("amount",currency.charAt(5) + min.toString());
results.add(values);
// Consider a member as settled if he has an outstanding balance between 0.00 and 0.49 else add him to the queue again
int compare = 1;
if(poorBalance.compareTo(new BigDecimal("0.49")) == compare) {
// if the debtor is not yet settled(has a balance between 0.49 and inf) add him to the priority queue again so that he is available for further transactions to settle up his debts
debtors.add(new Balance(poorBalance,poorName));
}
if(richBalance.compareTo(new BigDecimal("0.49")) == compare) {
// if the creditor is not yet settled(has a balance between 0.49 and inf) add him to the priority queue again so that he is available for further transactions
creditors.add(new Balance(richBalance,richName));
}
}
}
}
I've created a React App that implements Bin-packing approach to split trip expenses among friends with least number of transactions.
Check out the TypeScript file SplitPaymentCalculator.ts implementing the same.
You can find the working app's link on the homepage of the repo.

Resources