Why does ortools set constraint rigidly? - python

I went through google ortools scheduling tutorial and created slightly different constraint.
Say, that we have a df, that indicates the following:
Each nurse can work only on the preassigned shift. According to the table, nurse 0 must work either on shift 0 or 1, the nurse 2 must work only on the shift 2 etc.
Furthermore, I added the following constraint: "If nurse_0 takes shift_0, then she must take shif_1". It's shown by column "is_child" - shift_1 is child_shift for shift_0 for nurse_0.
df_dict = {
"slot_number":[1,1,2,2,3],
"asset_name":['0','1','0','1','2'],
"is_child":["No","No","Yes",'No','No']
}
df = pd.DataFrame.from_dict(df_dict)
Create variables and a model:
num_nurses = 3
num_shifts = 3
all_nurses = range(num_nurses)
all_shifts = range(num_shifts)
model = cp_model.CpModel()
shifts = {}
for n in all_nurses:
for s in all_shifts:
shifts[(n, s)] = model.NewBoolVar('shift_n%is%i' % (n, s))
One nurse can take only one shift:
for s in all_shifts:
model.Add(sum(shifts[(n, s)] for n in all_nurses) == 1)
My constraint:
# whether the slot (key) have any children in other slots
children = {0: [1], 1: [] ,2: []}
# what nurses are considered the children (values) in which slot (key)
nurse_child_sched = {0:[], 1:[0], 2:[]}
# In this case if nurse 0 take slot 0, then she must take slot 1 too.
for s in all_shifts:
for n in all_nurses:
if (children[s]):
for child in children[s]:
if n in nurse_child_sched[child]:
model.Add((shifts[(n,s)] + shifts[(n,child)])==2)
print(f"{s}-parent_slot and {child}-child_slot were connected for nurse {n}")
The code to create schedules and show solutions:
class NursesPartialSolutionPrinter(cp_model.CpSolverSolutionCallback):
"""Print intermediate solutions."""
def __init__(self, shifts, num_nurses, num_shifts, sols):
cp_model.CpSolverSolutionCallback.__init__(self)
self._shifts = shifts
self._num_nurses = num_nurses
self._num_shifts = num_shifts
self._solutions = set(sols)
self._solution_count = 0
def on_solution_callback(self):
if self._solution_count in self._solutions:
print('Solution %i' % self._solution_count)
for n in range(self._num_nurses):
is_working = False
for s in range(self._num_shifts):
if self.Value(self._shifts[(n, s)]):
is_working = True
print(' Nurse %i works shift %i' % (n, s))
if not is_working:
print(' Nurse {} does not work'.format(n))
print()
self._solution_count += 1
def solution_count(self):
return self._solution_count
solver = cp_model.CpSolver()
solver.parameters.linearization_level = 0
a_few_solutions = range(5)
solution_printer = NursesPartialSolutionPrinter(shifts, num_nurses, num_shifts,a_few_solutions)
solver.SearchForAllSolutions(model, solution_printer)
And, finally, I have the following result:
Solution 0
Nurse 0 works shift 0
Nurse 0 works shift 1
Nurse 1 works shift 2
Nurse 2 does not work
Solution 1
Nurse 0 works shift 0
Nurse 0 works shift 1
Nurse 1 does not work
Nurse 2 works shift 2
Solution 2
Nurse 0 works shift 0
Nurse 0 works shift 1
Nurse 0 works shift 2
Nurse 1 does not work
Nurse 2 does not work
However, according to my logic, there must be also at least the solution below. I realize, that my constraint says the model to put nurse_0 on shift_0, but I need to just put the relation between two slots in case if nurse_0 is putted on first shift and no constraints otherwise. Thanks in advance.
Solution x
Nurse 1 works shift 0
Nurse 0 works shift 1
Nurse 0 works shift 2
Nurse 1 does not work
Nurse 2 does not work

Side note:
One nurse can take only one shift:
for s in all_shifts:
model.Add(sum(shifts[(n, s)] for n in all_nurses) == 1)
Here you have written "each shift is assigned to exactly one nurse"
If you wan to express "one nurse can take only one shift" you should write:
for n in all_nurses:
model.Add(sum(shifts[(n, s)] for s in all_shifts) == 1)
note: but in this case since nurse exactly work one shift, child shift is impossible so I guess your code is OK and not the comment...

Related

Debugging the solution to a possible Bipartition

I came across this problem
We want to split a group of n people (labeled from 1 to n)
into two groups of any size. Each person may dislike some other people,
and they should not go into the same group.
Given the integer n and the array dislikes where dislikes[i] = [ai, bi]
indicates that the person labeled ai does not like the person labeled bi,
return true if it is possible to split everyone into two groups in this way.
Example 1:
Input: n = 4, dislikes = [[1,2],[1,3],[2,4]]
Output: true
Explanation: group1 [1,4] and group2 [2,3].
Example 2:
Input: n = 3, dislikes = [[1,2],[1,3],[2,3]]
Output: false
Example 3:
Input: n = 5, dislikes = [[1,2],[2,3],[3,4],[4,5],[1,5]]
Output: false
Below is my approach to the solution:
create two lists, group1 and group2 and initialise group1 with 1
generate all the numbers from 2 to n in a variable called num
check if num is enemy with group1 elements, if yes, then check if num is enemy with group2 elements, if yes as well, return False
else put num in its respective group and goto step 2 with the next value
return True
below is the code implementation
class Solution(object):
def possibleBipartition(self, n, dislikes):
"""
:type n: int
:type dislikes: List[List[int]]
:rtype: bool
"""
group1 = [1]
group2 = []
for num in range(2, n+1):
put_to_group_1 = 1
for _n in group1:
if [_n, num] in dislikes or [num, _n] in dislikes:
put_to_group_1 = 0
break
put_to_group_2 = 1
for _n in group2:
if[_n, num] in dislikes or [num, _n] in dislikes:
put_to_group_2 = 0
break
if put_to_group_1 == 0 and put_to_group_2 == 0:
return False
if put_to_group_1 == 1:
group1.append(num)
else:
group2.append(num)
return True
However for the following input I am getting False, but the expected output isTrue.
50
[[21,47],[4,41],[2,41],[36,42],[32,45],[26,28],[32,44],[5,41],[29,44],[10,46],[1,6],[7,42],[46,49],[17,46],[32,35],[11,48],[37,48],[37,43],[8,41],[16,22],[41,43],[11,27],[22,44],[22,28],[18,37],[5,11],[18,46],[22,48],[1,17],[2,32],[21,37],[7,22],[23,41],[30,39],[6,41],[10,22],[36,41],[22,25],[1,12],[2,11],[45,46],[2,22],[1,38],[47,50],[11,15],[2,37],[1,43],[30,45],[4,32],[28,37],[1,21],[23,37],[5,37],[29,40],[6,42],[3,11],[40,42],[26,49],[41,50],[13,41],[20,47],[15,26],[47,49],[5,30],[4,42],[10,30],[6,29],[20,42],[4,37],[28,42],[1,16],[8,32],[16,29],[31,47],[15,47],[1,5],[7,37],[14,47],[30,48],[1,10],[26,43],[15,46],[42,45],[18,42],[25,42],[38,41],[32,39],[6,30],[29,33],[34,37],[26,38],[3,22],[18,47],[42,48],[22,49],[26,34],[22,36],[29,36],[11,25],[41,44],[6,46],[13,22],[11,16],[10,37],[42,43],[12,32],[1,48],[26,40],[22,50],[17,26],[4,22],[11,14],[26,39],[7,11],[23,26],[1,20],[32,33],[30,33],[1,25],[2,30],[2,46],[26,45],[47,48],[5,29],[3,37],[22,34],[20,22],[9,47],[1,4],[36,46],[30,49],[1,9],[3,26],[25,41],[14,29],[1,35],[23,42],[21,32],[24,46],[3,32],[9,42],[33,37],[7,30],[29,45],[27,30],[1,7],[33,42],[17,47],[12,47],[19,41],[3,42],[24,26],[20,29],[11,23],[22,40],[9,37],[31,32],[23,46],[11,38],[27,29],[17,37],[23,30],[14,42],[28,30],[29,31],[1,8],[1,36],[42,50],[21,41],[11,18],[39,41],[32,34],[6,37],[30,38],[21,46],[16,37],[22,24],[17,32],[23,29],[3,30],[8,30],[41,48],[1,39],[8,47],[30,44],[9,46],[22,45],[7,26],[35,42],[1,27],[17,30],[20,46],[18,29],[3,29],[4,30],[3,46]]
Can anyone tell me where I might be going wrong with the implementation?
Consider a scenario:
Let's assume that in the dislikes array, we have [1,6],[2,6] among other elements (so 6 hates 1 and 2).
Person 1 doesn't hate anybody else
After placing everybody into groups, let's say 2 gets placed in group 2.
While placing 6, you can't put it in either group, since it conflicts with 1 in group 1 and 2 in group 2.
6 could have been placed in group 1 if you didn't start with the assumption of placing 1 in group 1 (ideally 1 could have been placed in group 2 without conflict).
Long story short, don't start with person 1 in group 1. Take the first element in the dislikes array, put either of them in either group, and then continue with the algorithm.

PYTHON - "Love for Mathematics"

I just finished a challenge on Dcoder ("Love for Mathematics") using Python. I failed two test-cases, but got one right. I used somewhat of a lower level of Python for the same as I haven't explored more yet, so I'm sorry if it looks a bit too basic.The Challenge reads:
Students of Dcoder school love Mathematics. They love to read a variety of Mathematics books. To make sure they remain happy, their Mathematics teacher decided to get more books for them.
A student would become happy if there are at least X Mathematics books in the class and not more than Y books because they know "All work and no play makes Jack a dull boy".The teacher wants to buy a minimum number of books to make the maximum number of students happy.
The Input
The first line of input contains an integer N indicating the number of students in the class. This is followed up by N lines where every line contains two integers X and Y respectively.
#Sample Input
5
3 6
1 6
7 11
2 15
5 8
The Output
Output two space-separated integers that denote the minimum number of mathematics books required and the maximum number of happy students.
Explanation: The teacher could buy 5 books and keep student 1, 2, 4 and 5 happy.
#Sample Output
5 4
Constraints:
1 <= N <= 10000
1 <= X, Y <= 10^9
My code:
n = int(input())
l = []
mi = []
ma = []
for i in range(n):
x, y = input().split()
mi.append(int(x))
ma.append(int(y))
if i == 0:
h=ma[0]
else:
if ma[i]>h:
h=ma[i]
for i in range(h):
c = 0
for j in range(len(mi)):
if ma[j]>=i and mi[j]<=i:
c+=1
l.append(c)
great = max(l)
for i in range(1,len(l)+1):
if l[i]==great:
print(i,l[i])
break
My Approach:
I first assigned the two minimum and maximum variables to two different lists - one containing the minimum values, and the other, the maximum. Then I created a loop that processes all numbers from 0 to the maximum possible value of the list containing maximum values and increasing the count for each no. by 1 every time it lies within the favorable range of students.
In this specific case, I got that count list to be (for the above given input):
[1,2,3,3,4,4,3,3,2 ...] and so on. So I could finalize that 4 would be the maximum no. of students and that the first index of 4 in the list would be the minimum no. of textbooks required.
But only 1 test-case worked and two failed. I would really appreciate it if anyone could help me out here.
Thank You.
This problem is alike minimum platform problem.
In that, you need to sort the min and max maths books array in ascending order respectively. Try to understand the problem from the above link (platform problem) then this will be a piece of cake.
Here is your solution:
n = int(input())
min_books = []
max_books = []
for i in range(n):
x, y = input().split()
min_books.append(int(x))
max_books.append(int(y))
min_books.sort()
max_books.sort()
happy_st_result = 1
happy_st = 1
books_needed = min_books[0]
i = 1
j = 0
while (i < n and j < n):
if (min_books[i] <= max_books[j]):
happy_st+= 1
i+= 1
elif (min_books[i] > max_books[j]):
happy_st-= 1
j+= 1
if happy_st > happy_st_result:
happy_st_result = happy_st
books_needed = min_books[i-1]
print(books_needed, happy_st_result)
Try this, and let me know if you need any clarification.
#Vinay Gupta's logic and explanation is correct. If you think on those lines, the answer should become immediately clear to you.
I have implemented the same logic in my code below, except using fewer lines and cool in-built python functions.
# python 3.7.1
import itertools
d = {}
for _ in range(int(input())):
x, y = map(int, input().strip().split())
d.setdefault(x, [0, 0])[0] += 1
d.setdefault(y, [0, 0])[1] += 1
a = list(sorted(d.items(), key=lambda x: x[0]))
vals = list(itertools.accumulate(list(map(lambda x: x[1][0] - x[1][1], a))))
print(a[vals.index(max(vals))][0], max(vals))
The above answer got accepted in Dcoder too.

Condionally and Randomly update Pandas values?

I want to build a scheduling app in python using pandas.
The following DataFrame is initialised where 0 denotes if a person is busy and 1 if a person is available.
import pandas as pd
df = pd.DataFrame({'01.01.': [1,1,0], '02.01.': [0,1,1], '03.01.': [1,0,1]}, index=['Person A', 'Person B', 'Person C'])
>>> df
01.01. 02.01. 03.01.
Person A 1 0 1
Person B 1 1 0
Person C 0 1 1
I now want to randomly schedule n number of people per day if they are available. In other words, for every day, if people are available (1), randomly set n number of people to scheduled (2).
I tried something as follows:
# Required number of people across time / columns
required_number = [0, 1, 2]
# Iterate through time / columns
for col in range(len(df.columns)):
# Current number of scheduled people
current_number = (df.iloc[:, [col]].values==2).sum()
# Iterate through indices / rows / people
for ind in range(len(df.index)):
# Check if they are available (1) and
# if the required number of people has not been met yet
if (df.iloc[ind, col]==1 and
current_number<required_number[col]):
# Change "free" / 1 person to "scheduled" / 2
df.iloc[ind, col] = 2
# Increment scheduled people by one
current_number += 1
>>> df
01.01. 02.01. 03.01.
Person A 1 0 2
Person B 1 2 0
Person C 0 1 2
This works as intended but – because I'm simply looping, I have no way of adding randomness (ie. that Person A / B / C) are randomly selected so long as they are available. Is there a way of directly doing so in pandas?
Thanks. BBQuercus
You can randomly choose proper indices in a series and then change values corresponding to the chosen indices:
for i in range(len(df.columns)):
if sum(df.iloc[:,i] == 1) >= required_number[i]:
column = df.iloc[:,i].reset_index(drop=True)
#We are going to store indices in a list
a = [j for j in column.index if column[j] == 1]
random_indexes = np.random.choice(a, required_number[i], replace = False)
df.iloc[:,i] = [column[j] if j not in random_indexes else 2 for j in column.index]
Now df is the wanted result.

python Constraints - constraining the amount

I have a constraint problem that I'm trying to solve with python-constraint
So let's say I have 3 locations: loc1,...loc3
Also, I have 7 devices: device1,...device7
Max amount of devices in each location: loc1:3, loc2:4, loc3:2
(for example maximum of 3 devices in loc1 and so on...)
And some constraints about the locations and the devices:
loc1: device1, device3, device7,
loc2: device1, device3, device4, device5, device6, device7
loc3: device2, device4, device5, device6
(meaning for example only device1, device3 and device7 can be in loc1.)
I'm trying to get a set of possible options for devices in locations.
from constraint import *
problem = Problem()
for key in locations_devices_dict:
problem.addVariable(key,locations_devices_dict[key])
# problem.addVariable("loc1", ['device1', 'device3', 'device7'])
problem.addConstraint(AllDifferentConstraint())
and I'm stuck on how to do the constrains. I've tried:
problem.addConstraint(MaxSumConstraint(3), 'loc1')
but it doesn't work, MaxSumConstraint does not sum what I need.
All devices must be placed somewhere
possible solution:
loc1: device1, device3
loc2: device4, device6, device7
loc3: device2, device5
Anyone has an idea?
(another python package/not to use any package, is also good idea if someone has any suggestions...)
This is simple assignment-like model:
So we have a binary variable indicating if device d is assigned to location L. The linear constraints are just:
assign each device to one location
each location has a maximum number of devices
make sure to use only allowed assignments (modeled above by allowed(L,d))
This problem can be handled by any constraint solver.
Enumerating all possible solutions is a bit dangerous. For large instances there are just way too many. Even for this small problem we already have 25 solutions:
For large problems this number will be astronomically large.
Using the Python constraint package this can look like:
from constraint import *
D = 7 # number of devices
L = 3 # number of locations
maxdev = [3,4,2]
allowed = [[1,3,7],[1,3,4,5,6,7],[2,4,5,6]]
problem = Problem()
problem.addVariables(["x_L%d_d%d" %(loc+1,d+1) for loc in range(L) for d in range(D) if d+1 in allowed[loc]],[0,1])
for loc in range(L):
problem.addConstraint(MaxSumConstraint(maxdev[loc]),["x_L%d_d%d" %(loc+1,d+1) for d in range(D) if d+1 in allowed[loc]])
for d in range(D):
problem.addConstraint(ExactSumConstraint(1),["x_L%d_d%d" %(loc+1,d+1) for loc in range(L) if d+1 in allowed[loc]])
S = problem.getSolutions()
n = len(S)
n
For large problems you may want to use dicts to speed things up.
edit: I wrote this answer before I saw #ErwinKalvelagen's code. So I did not check his solution...
So I used #ErwinKalvelagen approach and created a matrix that represented the probelm.
for each (i,j), x[i,j]=1 if device i can go to location j, 0 otherwise.
Then, I used addConstraint(MaxSumConstraint(maxAmount[i]), row) for each row - this is the constraint that represent the maximum devices in each location.
and addConstraint(ExactSumConstraint(1), col) for each column - this is the constraint that each device can be placed only in one location.
next, I took all x[i,j]=0 (device i can not be in location j) and for each t(i,j) addConstraint(lambda var, val=0: var == val, (t,))
This problem is similar to the sudoku problem, and I used this example for help
The matrix for my example above is:
(devices:) 1 2 3 4 5 6 7
loc1: 1 0 1 0 0 0 1
loc2: 1 0 1 1 1 1 1
loc3: 0 1 0 1 1 1 0
My code:
problem = Problem()
rows = range(locations_amount)
cols = range(devices_amount)
matrix = [(row, col) for row in rows for col in cols]
problem.addVariables(matrix, range(0, 2)) #each cell can get 0 or 1
rowSet = [zip([el] * len(cols), cols) for el in rows]
colSet = [zip(rows, [el] * len(rows)) for el in cols]
rowsConstrains = getRowConstrains() # list that has the maximum amount in each location(3,4,2)
#from my example: loc1:3, loc2:4, loc3:2
for i,row in enumerate(rowSet):
problem.addConstraint(MaxSumConstraint(rowsConstrains[i]), row)
for col in colSet:
problem.addConstraint(ExactSumConstraint(1), col)
s = getLocationsSet() # set that has all the tuples that x[i,j] = 1
for i, loc in enumerate(locations_list):
for j, iot in enumerate(devices_list):
t=(i,j)
if t in s:
continue
problem.addConstraint(lambda var, val=0: var == val, (t,)) # the value in these cells must be 0
solver = problem.getSolution()
example for a solution:
(devices:) 1 2 3 4 5 6 7
loc1: 1 0 1 0 0 0 1
loc2: 0 0 0 1 1 1 0
loc3: 0 1 0 0 0 0 0

How to compute the number of edges to change to make the undirected graph a single component?

I was trying to solve this problem with the following code. But the answers aren't accurate for all inputs.
Problem Statement
There are N cities and N one-way bridges in Byteland. There is exactly one incoming and one outgoing
bridge for each city.
Byteland wants to be "elligible" to host the World Cup. A country is "elligible" for the World
Cup, if and only if you can travel from any city to any other city of that country. You are asked to perform
the minimum number of steps to help Byteland become an elligible World Cup host.
For each step you can swap the destinations of two bridges.
For example: We have two bridges A -> B and
C -> D (there is a bridge from A to B and from C to D ). If we swap their destinations, we will have two
bridges A -> D and C -> B .
Input Format
First line contains one integer, T denoting the number of test cases.
For each test there will be two lines:
First line contains one integer, N.
Second line contains N integers, where the X th integer denotes city where bridge goes from city X .
Output Format
Just print the answer to the problem, one line per case.
Sample Input
2
4
3 1 2 4
3
2 3 1
Sample Output
1
0
My Code
for i in range(input()):
n = input()
bridges = {}
connection = [int(y) for y in raw_input().split(' ')]
for j in range(n):
bridges[j+1] = connection[j]
#print bridges
count = 0
swapped = True
for k in range(1, n+1):
if swapped:
swapped = False
for j in range(1, n+1):
if bridges[bridges[j]] == j:
bridges[j], bridges[1 if (j+1)%n == 0 else (j+1)%n] = bridges[1 if (j+1)%n == 0 else (j+1)%n], bridges[j]
swapped = True
#print bridges
count += 1
print count
You have to compute the connected components, then the number of swap is equal to the number of components minus one.
for i in range(int(input())):
N = int(input())
bridges = {index + 1: int(connection) for index, connection in enumerate(input().split())}
visited = set()
component_count = 0
towns = list(bridges.keys())
while towns:
current = towns[0]
component = set()
while current not in visited:
towns.remove(current)
visited.add(current)
current = bridges[current]
component_count += 1
print(component_count - 1)
The code does the following:
start at the first town not visited yet (outer loop), and visit as much town as possible followinng the bridges connections (inner loop)
each time the inner loop breaks, it means that we met a town we already met which makes a component in the graph.
Break the loop and increment the number of component. Start again with a new twon that is not visited
This gives the correct result because we are guaranteed that components are cycles, you can break the cycle (swap a connection) to connect a component to another without breaking the component.

Categories