How to dynamically compute nurse walking distance based on assignments in a nurse-patient scheduler model in Pyomo? - constraints

I am writing a Nurse-Patient assignment algorithm in Pyomo, but I am having trouble dynamically computing the walking distance between assigned patient beds for each nurse. The relevant sets and variables are below with a small example. The variables are filled in with the values that I would want at the end of solving.
#Sets
model.PatientIDs = {0, 1, 2}
model.NurseIDs = {a, b}
#Indexed parameter (indexed by (patient1, patient2))
# Stores the distance between each patient bed
model.Bed_Distances = {(0, 0) : 0, (0, 1) : 6.4, (0, 2) : 7.2, (1, 0) : 6.4, (1, 1) : 0, (1, 2) : 1.9, (2, 1) : 1.9), (2, 0) : 7.2}
# Indexed variable (indexed by (patient, nurse))
model.Assignments = {(0, a): 1, (0, b): 0, (1, a) : 0, (1, b) : 1, (2, a) : 1, (2, b) : 0}
# Indexed variable (indexed by nurse)
# Keeps track of the distance between assigned patient beds for each nurse
model.Nurse_Distances = {a : 7.2, b : 0}
I'm having trouble computing the nurse distances dynamically as the model is being solved (since it is dependent on the patients assigned to each nurse). Since the model.ASSIGNMENTS decision variable is represented with binary 0 or 1, I've been using this rule to compute the nurse distances:
def nurse_distance(self, nurse):
return model.NURSE_DISTANCE[nurse] == sum([model.ASSIGNMENTS[p1, nurse] * model.ASSIGNMENTS[p2, nurse] * model.BED_DISTANCES[p1, p2] for p1 in model.PATIENTS for p2 in model.PATIENTS])
model.DISTANCE_CONSTRAINT = pe.Constraint(model.NURSES, rule = nurse_distance)
After adding this constraint, my model runs forever, or when I run it with only 2 or 3 patients it finishes relatively quickly and gives me an error that looks like this:
ERROR: Error parsing NEOS solution file NEOS log: Job 11992161 dispatched password: iGYfJtMj ---------- Begin Solver Output ----------packages/pyomo/opt/plugins/sol.py", line 87, in _load raise ValueError("no Options line found") ValueError: no Options line found
Is there a better way to keep track of a variable that is dependent on the value of another variable in Pyomo (nurse bed distances is dependent on the assignments made between nurses and patients)? If not, is there a more computationally efficient way to compute it within the constraint I've written without using "if then" statements?

You are creating a non-linear model by multiplying your decision variables together, which is probably not desired...
Because you have an "and" condition here (that the same nurse is assigned to p1 and p2) you almost certainly need another binary "helper" variable to track whether both are true, then use that. Of course, you will need a linking constraint between the individual assignments and the paired assignment. See the example below... Note that "Todd" gets off easy here because he only has 1 patient.
Code:
# Nurse Patrol
import pyomo.environ as pyo
# data
PatientIDs = [0, 1, 2]
NurseIDs = ['Todd', 'Cindy']
Bed_Distances = {(0, 0) : 0, (0, 1) : 6.4, (0, 2) : 7.2, (1, 0) : 6.4, (1, 1) : 0, (1, 2) : 1.9, (2, 1) : 1.9, (2, 0) : 7.2}
patient_pairings = [(p1, p2) for p1 in PatientIDs for p2 in PatientIDs if p1 != p2]
# model
m = pyo.ConcreteModel('Nurse Patrol')
# put the stuff above into model parameters. You can use them "raw" but this will help you T/S the model
# SETS
m.N = pyo.Set(initialize=NurseIDs)
m.P = pyo.Set(initialize=PatientIDs)
m.PP = pyo.Set(initialize=patient_pairings)
# PARAMS
m.distance = pyo.Param(m.P, m.P, initialize=Bed_Distances)
# VARIABLES
m.assign = pyo.Var(m.N, m.P, domain=pyo.Binary)
m.pair_assigned = pyo.Var(m.N, m.PP, domain=pyo.Binary) # true if BOTH are assigned to nurse
# OBJ: Minimize the total of all nurses' travels
m.obj = pyo.Objective(expr=sum(m.pair_assigned[n, p1, p2]*m.distance[p1, p2]
for (p1, p2) in m.PP
for n in m.N))
# CONSTRAINTS
# Cover all patients
def covered(m, patient):
return sum(m.assign[n, patient] for n in m.N) >= 1
m.C1 = pyo.Constraint(m.P, rule=covered)
# link the assignment to the paired assignment
def paired(m, n, p1, p2):
return m.pair_assigned[n, p1, p2] >= m.assign[n, p1] + m.assign[n, p2] - 1
m.C2 = pyo.Constraint(m.N, m.PP, rule=paired)
result = pyo.SolverFactory('glpk').solve(m)
print(result)
m.assign.display()
for n in m.N:
tot_dist = sum(m.pair_assigned[n, p1, p2]*m.distance[p1, p2] for (p1, p2) in m.PP)
print(f'nurse {n} distance is: {pyo.value(tot_dist)}')
Yields:
Problem:
- Name: unknown
Lower bound: 3.8
Upper bound: 3.8
Number of objectives: 1
Number of constraints: 16
Number of variables: 19
Number of nonzeros: 43
Sense: minimize
Solver:
- Status: ok
Termination condition: optimal
Statistics:
Branch and bound:
Number of bounded subproblems: 3
Number of created subproblems: 3
Error rc: 0
Time: 0.007218837738037109
Solution:
- number of solutions: 0
number of solutions displayed: 0
assign : Size=6, Index=assign_index
Key : Lower : Value : Upper : Fixed : Stale : Domain
('Cindy', 0) : 0 : 0.0 : 1 : False : False : Binary
('Cindy', 1) : 0 : 1.0 : 1 : False : False : Binary
('Cindy', 2) : 0 : 1.0 : 1 : False : False : Binary
('Todd', 0) : 0 : 1.0 : 1 : False : False : Binary
('Todd', 1) : 0 : 0.0 : 1 : False : False : Binary
('Todd', 2) : 0 : 0.0 : 1 : False : False : Binary
nurse Todd distance is: 0.0
nurse Cindy distance is: 3.8
[Finished in 558ms]

Related

How to allocate a product using R

I want to solve the following problem using R, and I am struggling to find a good way to do so.
I have a sales forecasts for two products (Product 1 & Product 2), of which there are 2 variations each (A & B).
dat_forecast <- data.frame(
product = c(1,1,2,2),
variation = c("A", "B", "A", "B"),
forecast_sales = c(612,238,741,455),
ratio = c(0.72,0.28,0.6195652,0.3804348)
)
and I have data frame containing the current units in stock:
dat_stock <- data.frame(
product = c(1,1,2,2),
variation = c("A", "B", "A", "B"),
current_stock = c(400,268,341,155),
ratio = c(0.5988024,0.4011976,0.6875,0.3125)
)
Assume we wanted to produce another 100 units of Product 1 and another 200 units of Product 2. The task is to allocate the produced units of product to the different variations (A & B) in such a way, that the ratio of units in stock (highlighted in green) gets as close as possible to the ratio in the original forecast (highlighted in blue).
dat_to_be_produced <- data.frame(
product = c(1,2),
units = c(100,200)
)
What is the easiest way to solve this problem? Please note, in case for Product 1, there cannot be a precise solution as there is already more stock of Product 1 - Variation B than forecast (238 forecast, 268 in stock), so obviously one wouldn't allocate any more units to variation B in this case.
Any help as to how to solve this in R would be greatly appreciated.
In R you can use optimize() to find the minimum or max of a function.
Here is a function that use optimize to allocate N units of product to A and B, given the initial stocks and the forecast. It uses optimize() to find the best number of units to allocate to "A", so that the resulting ratio is the closest to the forecasted one.
allocate_units <- function(forecast,
stocks,
units) {
get_error <- function(x) {
target_ratio <- forecast[1]/sum(forecast)
updated_stocks <- c(stocks[1]+x,stocks[2]+(units-x))
ratio <- updated_stocks[1]/sum(updated_stocks)
abs(target_ratio-ratio)
}
opt_As <- optimize(f = get_error,
maximum = F,
interval = c(0,units))
As <- round(opt_As$minimum)
Bs <- units - As
c(A=As,B=Bs)
}
for example:
allocate_units(forecast = c(A=612,B=238),
stocks = c(A=400,B=268),
units = 100)
A = 100; B = 0
allocate_units(forecast = c(A=741,B=455),
stocks = c(A=341,B=155),
units = 200)
A = 90; B = 110
The equations you describe can be translated into a Linear Program (LP), most likely you will need to make it a Mixed Integer Linear Program (MILP) because the variables for how many of something to make should be integers. So there are a handful of online references to search/review on that, including on this site.
So, as a starting place, I'd set out to tackle one of the products at a time. You'll clearly need a couple variables:
QuantityA: an integer for how many of type-A to produce
QuantityB: ... type-B
and some logical constraints such as:
QuantityA + QuantityB == 100
Those are the key variables you want to determine, obviously. You will also need a variable for the "error" or the difference between the target ratio and the best you can do. Your optimization should have as the objective function to min(error).
The error part here is a little tricky and you'll have to think through it. Perhaps you can hit 1 ratio exactly, but the other would be 0.1 off, or such. It seems a logical starting point to minimize the maximum error across both target ratios. Or:
min(error) = min(error_A, error_B)
or...
min(|tgtA - resultA|, |tgtB - resultB|)
absolute values are non-linear so you will need 4 equations to constrain the error to be greater than the +/- of these two quantities.... that is 4 constraints on the error variable in your program.
Something like:
error >= tgtA - resultA
error >= resultA - tgtA
.... same for B
and you can express resultA and resultB as functions of your variables and the fixed inputs.
Give it a whirl...
EDIT: Below is a full solution in python using the pyomo package to express the problem and the separately available (free) cbc solver:
CODE
# restock
import pyomo.environ as pyo
### DATA
current_stock = { (1, 'A') : 400,
(1, 'B') : 268,
(2, 'A') : 341,
(2, 'B') : 155
}
production_size = { 1: 100,
2: 200 }
# we could "do a little math" and compute these, but I'll just plug in...
target_ratios = { (1, 'A') : 0.72,
(1, 'B') : 0.28,
(2, 'A') : 0.6195652,
(2, 'B') : 0.3804348}
products = list({k[0] for k in current_stock.keys()}) # Sets are appropriate, but pyomo gives warning w/ set initializations
variants = list({k[1] for k in current_stock.keys()})
### MODEL
m = pyo.ConcreteModel()
# SETS
m.P = pyo.Set(initialize=products)
m.V = pyo.Set(initialize=variants)
# PARAMETERS
m.stock = pyo.Param(m.P, m.V, initialize=current_stock)
m.target = pyo.Param(m.P, m.V, initialize=target_ratios)
m.produce = pyo.Param(m.P, initialize=production_size)
# VARIABLES
m.make = pyo.Var(m.P, m.V, domain=pyo.NonNegativeIntegers)
m.error = pyo.Var(m.P, domain=pyo.NonNegativeReals)
# OBJECTIVE
# minimize the sum of the two errors. They are independent, so this works fine
m.obj = pyo.Objective(expr = sum(m.error[p] for p in m.P))
# CONSTRAINTS
# Make exactly the production run...
#m.Constraint(m.P)
def production_run(m, p):
return sum(m.make[p, v] for v in m.V) == m.produce[p]
# constrain the "positive" and "negative" error for each product line.
# e >= |ratio - target| translates into 2 constraints to linearize
# e >= ratio - target
# e >= target - ratio
#m.Constraint(m.P, m.V)
def pos_error(m, p, v):
return m.error[p] >= (m.make[p, v] + m.stock[p, v]) / (sum(m.stock[p, vv] for vv in m.V) + m.produce[p]) - m.target[p, v]
#m.Constraint(m.P, m.V)
def neg_error(m, p, v):
return m.error[p] >= -(m.make[p, v] + m.stock[p, v]) / (sum(m.stock[p, vv] for vv in m.V) + m.produce[p]) + m.target[p, v]
m.pprint() # to QA everything...
# solve
solver = pyo.SolverFactory('cbc')
result = solver.solve(m)
print(result) # must check status = optimal
print('production plan')
for idx in sorted(m.make.index_set()):
print(f'make {m.make[idx].value:3.0f} of {idx}')
OUTPUT
7 Set Declarations
P : Size=1, Index=None, Ordered=Insertion
Key : Dimen : Domain : Size : Members
None : 1 : Any : 2 : {1, 2}
V : Size=1, Index=None, Ordered=Insertion
Key : Dimen : Domain : Size : Members
None : 1 : Any : 2 : {'A', 'B'}
make_index : Size=1, Index=None, Ordered=True
Key : Dimen : Domain : Size : Members
None : 2 : P*V : 4 : {(1, 'A'), (1, 'B'), (2, 'A'), (2, 'B')}
neg_error_index : Size=1, Index=None, Ordered=True
Key : Dimen : Domain : Size : Members
None : 2 : P*V : 4 : {(1, 'A'), (1, 'B'), (2, 'A'), (2, 'B')}
pos_error_index : Size=1, Index=None, Ordered=True
Key : Dimen : Domain : Size : Members
None : 2 : P*V : 4 : {(1, 'A'), (1, 'B'), (2, 'A'), (2, 'B')}
stock_index : Size=1, Index=None, Ordered=True
Key : Dimen : Domain : Size : Members
None : 2 : P*V : 4 : {(1, 'A'), (1, 'B'), (2, 'A'), (2, 'B')}
target_index : Size=1, Index=None, Ordered=True
Key : Dimen : Domain : Size : Members
None : 2 : P*V : 4 : {(1, 'A'), (1, 'B'), (2, 'A'), (2, 'B')}
3 Param Declarations
produce : Size=2, Index=P, Domain=Any, Default=None, Mutable=False
Key : Value
1 : 100
2 : 200
stock : Size=4, Index=stock_index, Domain=Any, Default=None, Mutable=False
Key : Value
(1, 'A') : 400
(1, 'B') : 268
(2, 'A') : 341
(2, 'B') : 155
target : Size=4, Index=target_index, Domain=Any, Default=None, Mutable=False
Key : Value
(1, 'A') : 0.72
(1, 'B') : 0.28
(2, 'A') : 0.6195652
(2, 'B') : 0.3804348
2 Var Declarations
error : Size=2, Index=P
Key : Lower : Value : Upper : Fixed : Stale : Domain
1 : 0 : None : None : False : True : NonNegativeReals
2 : 0 : None : None : False : True : NonNegativeReals
make : Size=4, Index=make_index
Key : Lower : Value : Upper : Fixed : Stale : Domain
(1, 'A') : 0 : None : None : False : True : NonNegativeIntegers
(1, 'B') : 0 : None : None : False : True : NonNegativeIntegers
(2, 'A') : 0 : None : None : False : True : NonNegativeIntegers
(2, 'B') : 0 : None : None : False : True : NonNegativeIntegers
1 Objective Declarations
obj : Size=1, Index=None, Active=True
Key : Active : Sense : Expression
None : True : minimize : error[1] + error[2]
3 Constraint Declarations
neg_error : Size=4, Index=neg_error_index, Active=True
Key : Lower : Body : Upper : Active
(1, 'A') : -Inf : - (make[1,A] + 400)/768 + 0.72 - error[1] : 0.0 : True
(1, 'B') : -Inf : - (make[1,B] + 268)/768 + 0.28 - error[1] : 0.0 : True
(2, 'A') : -Inf : - (make[2,A] + 341)/696 + 0.6195652 - error[2] : 0.0 : True
(2, 'B') : -Inf : - (make[2,B] + 155)/696 + 0.3804348 - error[2] : 0.0 : True
pos_error : Size=4, Index=pos_error_index, Active=True
Key : Lower : Body : Upper : Active
(1, 'A') : -Inf : (make[1,A] + 400)/768 - 0.72 - error[1] : 0.0 : True
(1, 'B') : -Inf : (make[1,B] + 268)/768 - 0.28 - error[1] : 0.0 : True
(2, 'A') : -Inf : (make[2,A] + 341)/696 - 0.6195652 - error[2] : 0.0 : True
(2, 'B') : -Inf : (make[2,B] + 155)/696 - 0.3804348 - error[2] : 0.0 : True
production_run : Size=2, Index=P, Active=True
Key : Lower : Body : Upper : Active
1 : 100.0 : make[1,A] + make[1,B] : 100.0 : True
2 : 200.0 : make[2,A] + make[2,B] : 200.0 : True
16 Declarations: P V stock_index stock target_index target produce make_index make error obj production_run pos_error_index pos_error neg_error_index neg_error
Problem:
- Name: unknown
Lower bound: 0.06927066
Upper bound: 0.06927066
Number of objectives: 1
Number of constraints: 2
Number of variables: 2
Number of binary variables: 0
Number of integer variables: 4
Number of nonzeros: 1
Sense: minimize
Solver:
- Status: ok
User time: -1.0
System time: 0.0
Wallclock time: 0.01
Termination condition: optimal
Termination message: Model was solved to optimality (subject to tolerances), and an optimal solution is available.
Statistics:
Branch and bound:
Number of bounded subproblems: 0
Number of created subproblems: 0
Black box:
Number of iterations: 0
Error rc: 0
Time: 0.14908695220947266
Solution:
- number of solutions: 0
number of solutions displayed: 0
production plan
make 100 of (1, 'A')
make 0 of (1, 'B')
make 90 of (2, 'A')
make 110 of (2, 'B')
[Finished in 534ms]

Is it possible to find a few common multiples of a list of numbers, without them having to be integers?

I don't even know if something like this is possible, but:
Let us say we have three numbers:
A = 6
B = 7.5
C = 24
I would like to find a few evenly spaced common multiples of these numbers between 0 and 2.
So the requirement is: one_of_these_numbers / common_multiple = an_integer (or almost an integer with a particular tolerance)
For example, a good result would be [0.1 , 0.5 , 1 , 1.5]
I have no idea if this is possible, because one can not iterate through a range of floats, but is there a smart way to do it?
I am using python, but a solution could be represented in any language of your preference.
Thank you for your help!
While I was writing my question, I actually came up with an idea for the solution.
To find common divisors using code, we have to work with integers.
My solution is to multiply all numbers by a factor = 1, 10, 100, ...
so that we can act as if they are integers, find their integer common divisors, and then redivide them by the factor to get a result.
Better explained in code:
a = 6
b = 7.5
c = 24
# Find a few possible divisors between 0 and 2 so that all numbers are divisible
by div.
# We define a function that finds all divisors in a range of numbers, supposing
all numbers are integers.
def find_common_divisors(numbers, range_start, range_end):
results = []
for i in range(range_start + 1, range_end + 1):
if all([e % i == 0 for e in numbers]):
results.append(i)
return results
def main():
nums = [a, b, c]
range_start = 0
range_end = 2
factor = 1
results = [1]
while factor < 11:
nums_i = [e * factor for e in nums]
range_end_i = range_end * factor
results += [e / factor for e in find_common_divisors(nums_i, range_start, range_end_i)]
factor *= 10
print(sorted(set(results)))
if __name__ == '__main__':
main()
For these particular numbers, I get the output:
[0.1, 0.3, 0.5, 1, 1.5]
If we need more results, we can adjust while factor < 11: to a higher number than 11 like 101.
I am curious to see if I made any mistake in my code.
Happy to hear some feedback.
Thank you!

Is there a function f(n) that returns the n:th combination in an ordered list of combinations without repetition?

Combinations without repetitions look like this, when the number of elements to choose from (n) is 5 and elements chosen (r) is 3:
0 1 2
0 1 3
0 1 4
0 2 3
0 2 4
0 3 4
1 2 3
1 2 4
1 3 4
2 3 4
As n and r grows the amount of combinations gets large pretty quickly. For (n,r) = (200,4) the number of combinations is 64684950.
It is easy to iterate the list with r nested for-loops, where the initial iterating value of each for loop is greater than the current iterating value of the for loop in which it is nested, as in this jsfiddle example:
https://dotnetfiddle.net/wHWK5o
What I would like is a function that calculates only one combination based on its index. Something like this:
tuple combination(i,n,r) {
return [combination with index i, when the number of elements to choose from is n and elements chosen is r]
Does anyone know if this is doable?
You would first need to impose some sort of ordering on the set of all combinations available for a given n and r, such that a linear index makes sense. I suggest we agree to keep our combinations in increasing order (or, at least, the indices of the individual elements), as in your example. How then can we go from a linear index to a combination?
Let us first build some intuition for the problem. Suppose we have n = 5 (e.g. the set {0, 1, 2, 3, 4}) and r = 3. How many unique combinations are there in this case? The answer is of course 5-choose-3, which evaluates to 10. Since we will sort our combinations in increasing order, consider for a minute how many combinations remain once we have exhausted all those starting with 0. This must be 4-choose-3, or 4 in total. In such a case, if we are looking for the combination at index 7 initially, this implies we must subtract 10 - 4 = 6 and search for the combination at index 1 in the set {1, 2, 3, 4}. This process continues until we find a new index that is smaller than this offset.
Once this process concludes, we know the first digit. Then we only need to determine the remaining r - 1 digits! The algorithm thus takes shape as follows (in Python, but this should not be too difficult to translate),
from math import factorial
def choose(n, k):
return factorial(n) // (factorial(k) * factorial(n - k))
def combination_at_idx(idx, elems, r):
if len(elems) == r:
# We are looking for r elements in a list of size r - thus, we need
# each element.
return elems
if len(elems) == 0 or len(elems) < r:
return []
combinations = choose(len(elems), r) # total number of combinations
remains = choose(len(elems) - 1, r) # combinations after selection
offset = combinations - remains
if idx >= offset: # combination does not start with first element
return combination_at_idx(idx - offset, elems[1:], r)
# We now know the first element of the combination, but *not* yet the next
# r - 1 elements. These need to be computed as well, again recursively.
return [elems[0]] + combination_at_idx(idx, elems[1:], r - 1)
Test-driving this with your initial input,
N = 5
R = 3
for idx in range(choose(N, R)):
print(idx, combination_at_idx(idx, list(range(N)), R))
I find,
0 [0, 1, 2]
1 [0, 1, 3]
2 [0, 1, 4]
3 [0, 2, 3]
4 [0, 2, 4]
5 [0, 3, 4]
6 [1, 2, 3]
7 [1, 2, 4]
8 [1, 3, 4]
9 [2, 3, 4]
Where the linear index is zero-based.
Start with the first element of the result. The value of that element depends on the number of combinations you can get with smaller elements. For each such smaller first element, the number of combinations with first element k is n − k − 1 choose r − 1, with potentially some of-by-one corrections. So you would sum over a bunch of binomial coefficients. Wolfram Alpha can help you compute such a sum, but the result still has a binomial coefficient in it. Solving for the largest k such that the sum doesn't exceed your given index i is a computation you can't do with something as simple as e.g. a square root. You need a loop to test possible values, e.g. like this:
def first_naive(i, n, r):
"""Find first element and index of first combination with that first element.
Returns a tuple of value and index.
Example: first_naive(8, 5, 3) returns (1, 6) because the combination with
index 8 is [1, 3, 4] so it starts with 1, and because the first combination
that starts with 1 is [1, 2, 3] which has index 6.
"""
s1 = 0
for k in range(n):
s2 = s1 + choose(n - k - 1, r - 1)
if i < s2:
return k, s1
s1 = s2
You can reduce the O(n) loop iterations to O(log n) steps using bisection, which is particularly relevant for large n. In that case I find it easier to think about numbering items from the end of your list. In the case of n = 5 and r = 3 you get choose(2, 2)=1 combinations starting with 2, choose(3,2)=3 combinations starting with 1 and choose(4,2)=6 combinations starting with 0. So in the general choose(n,r) binomial coefficient you increase the n with each step, and keep the r. Taking into account that sum(choose(k,r) for k in range(r,n+1)) can be simplified to choose(n+1,r+1), you can eventually come up with bisection conditions like the following:
def first_bisect(i, n, r):
nCr = choose(n, r)
k1 = r - 1
s1 = nCr
k2 = n
s2 = 0
while k2 - k1 > 1:
k3 = (k1 + k2) // 2
s3 = nCr - choose(k3, r)
if s3 <= i:
k2, s2 = k3, s3
else:
k1, s1 = k3, s3
return n - k2, s2
Once you know the first element to be k, you also know the index of the first combination with that same first element (also returned from my function above). You can use the difference between that first index and your actual index as input to a recursive call. The recursive call would be for r − 1 elements chosen from n − k − 1. And you'd add k + 1 to each element from the recursive call, since the top level returns values starting at 0 while the next element has to be greater than k in order to avoid duplication.
def combination(i, n, r):
"""Compute combination with a given index.
Equivalent to list(itertools.combinations(range(n), r))[i].
Each combination is represented as a tuple of ascending elements, and
combinations are ordered lexicograplically.
Args:
i: zero-based index of the combination
n: number of possible values, will be taken from range(n)
r: number of elements in result list
"""
if r == 0:
return []
k, ik = first_bisect(i, n, r)
return tuple([k] + [j + k + 1 for j in combination(i - ik, n - k - 1, r - 1)])
I've got a complete working example, including an implementation of choose, more detailed doc strings and tests for some basic assumptions.

How to approach this type of problem in permutation and combination?

Altitudes
Alice and Bob took a journey to the mountains. They have been climbing
up and down for N days and came home extremely tired.
Alice only remembers that they started their journey at an altitude of
H1 meters and they finished their wandering at an alitude of H2
meters. Bob only remembers that every day they changed their altitude
by A, B, or C meters. If their altitude on the ith day was x,
then their altitude on day i + 1 can be x + A, x + B, or x + C.
Now, Bob wonders in how many ways they could complete their journey.
Two journeys are considered different if and only if there exist a day
when the altitude that Alice and Bob covered that day during the first
journey differs from the altitude Alice and Bob covered that day during
the second journey.
Bob asks Alice to tell her the number of ways to complete the journey.
Bob needs your help to solve this problem.
Input format
The first and only line contains 6 integers N, H1, H2, A, B, C that
represents the number of days Alice and Bob have been wandering,
altitude on which they started their journey, altitude on which they
finished their journey, and three possible altitude changes,
respectively.
Output format
Print the answer modulo 10**9 + 7.
Constraints
1 <= N <= 10**5
-10**9 <= H1, H2 <= 10**9
-10**9 <= A, B, C <= 10**9
Sample Input
2 0 0 1 0 -1
Sample Output
3
Explanation
There are only 3 possible journeys-- (0, 0), (1, -1), (-1, 1).
Note
This problem comes originally from a hackerearth competition, now closed. The explanation for the sample input and output has been corrected.
Here is my solution in Python 3.
The question can be simplified from its 6 input parameters to only 4 parameters. There is no need for the beginning and ending altitudes--the difference of the two is enough. Also, we can change the daily altitude changes A, B, and C and get the same answer if we make a corresponding change to the total altitude change. For example, if we add 1 to each of A, B, and C, we could add N to the altitude change: 1 additional meter each day over N days means N additional meters total. We can "normalize" our daily altitude changes by sorting them so A is the smallest, then subtract A from each of the altitude changes and subtract N * A from the total altitude change. This means we now need to add a bunch of 0's and two other values (let's call them D and E). D is not larger than E.
We now have an easier problem: take N values, each of which is 0, D, or E, so they sum to a particular total (let's say H). This is the same at using up to N numbers equaling D or E, with the rest zeros.
We can use mathematics, in particular Bezout's identity, to see if this is possible. Some more mathematics can find all the ways of doing this. Once we know how many 0's, D's, and E's, we can use multinomial coefficients to find how many ways these values can be rearranged. Total all these up and we have the answer.
This code finds the total number of ways to complete the journey, and takes it modulo 10**9 + 7 only at the very end. This is possible since Python uses large integers. The largest result I found in my testing is for the input values 100000 0 100000 0 1 2 which results in a number with 47,710 digits before taking the modulus. This takes a little over 8 seconds on my machine.
This code is a little longer than necessary, since I made some of the routines more general than necessary for this problem. I did this so I can use them in other problems. I used many comments for clarity.
# Combinatorial routines -----------------------------------------------
def comb(n, k):
"""Compute the number of ways to choose k elements out of a pile of
n, ignoring the order of the elements. This is also called
combinations, or the binomial coefficient of n over k.
"""
if k < 0 or k > n:
return 0
result = 1
for i in range(min(k, n - k)):
result = result * (n - i) // (i + 1)
return result
def multcoeff(*args):
"""Return the multinomial coefficient
(n1 + n2 + ...)! / n1! / n2! / ..."""
if not args: # no parameters
return 1
# Find and store the index of the largest parameter so we can skip
# it (for efficiency)
skipndx = args.index(max(args))
newargs = args[:skipndx] + args[skipndx + 1:]
result = 1
num = args[skipndx] + 1 # a factor in the numerator
for n in newargs:
for den in range(1, n + 1): # a factor in the denominator
result = result * num // den
num += 1
return result
def new_multcoeff(prev_multcoeff, x, y, z, ag, bg):
"""Given a multinomial coefficient prev_multcoeff =
multcoeff(x-bg, y+ag, z+(bg-ag)), calculate multcoeff(x, y, z)).
NOTES: 1. This uses bg multiplications and bg divisions,
faster than doing multcoeff from scratch.
"""
result = prev_multcoeff
for d in range(1, ag + 1):
result *= y + d
for d in range(1, bg - ag + 1):
result *= z + d
for d in range(bg):
result //= x - d
return result
# Number theory routines -----------------------------------------------
def bezout(a, b):
"""For integers a and b, find an integral solution to
a*x + b*y = gcd(a, b).
RETURNS: (x, y, gcd)
NOTES: 1. This routine uses the convergents of the continued
fraction expansion of b / a, so it will be slightly
faster if a <= b, i.e. the parameters are sorted.
2. This routine ensures the gcd is nonnegative.
3. If a and/or b is zero, the corresponding x or y
will also be zero.
4. This routine is named after Bezout's identity, which
guarantees the existences of the solution x, y.
"""
if not a:
return (0, (b > 0) - (b < 0), abs(b)) # 2nd is sign(b)
p1, p = 0, 1 # numerators of the two previous convergents
q1, q = 1, 0 # denominators of the two previous convergents
negate_y = True # flag if negate y=q (True) or x=p (False)
quotient, remainder = divmod(b, a)
while remainder:
b, a = a, remainder
p, p1 = p * quotient + p1, p
q, q1 = q * quotient + q1, q
negate_y = not negate_y
quotient, remainder = divmod(b, a)
if a < 0:
p, q, a = -p, -q, -a # ensure the gcd is nonnegative
return (p, -q, a) if negate_y else (-p, q, a)
def byzantine_bball(a, b, s):
"""For nonnegative integers a, b, s, return information about
integer solutions x, y to a*x + b*y = s. This is
equivalent to finding a multiset containing only a and b that
sums to s. The name comes from getting a given basketball score
given scores for shots and free throws in a hypothetical game of
"byzantine basketball."
RETURNS: None if there is no solution, or an 8-tuple containing
x the smallest possible nonnegative integer value of
x.
y the value of y corresponding to the smallest
possible integral value of x. If this is negative,
there is no solution for nonnegative x, y.
g the greatest common divisor (gcd) of a, b.
u the found solution to a*u + b*v = g
v " "
ag a // g, or zero if g=0
bg b // g, or zero if g=0
sg s // g, or zero if g=0
NOTES: 1. If a and b are not both zero and one solution x, y is
returned, then all integer solutions are given by
x + t * bg, y - t * ag for any integer t.
2. This routine is slightly optimized for a <= b. In that
case, the solution returned also has the smallest sum
x + y among positive integer solutions.
"""
# Handle edge cases of zero parameter(s).
if 0 == a == b: # the only score possible from 0, 0 is 0
return (0, 0, 0, 0, 0, 0, 0, 0) if s == 0 else None
if a == 0:
sb = s // b
return (0, sb, b, 0, 1, 0, 1, sb) if s % b == 0 else None
if b == 0:
sa = s // a
return (sa, 0, a, 1, 0, 1, 0, sa) if s % a == 0 else None
# Find if the score is possible, ignoring the signs of x and y.
u, v, g = bezout(a, b)
if s % g:
return None # only multiples of the gcd are possible scores
# Find one way to get the score, ignoring the signs of x and y.
ag, bg, sg = a // g, b // g, s // g # we now have ag*u + bg*v = 1
x, y = sg * u, sg * v # we now have a*x + b*y = s
# Find the solution where x is nonnegative and as small as possible.
t = x // bg # Python rounds toward minus infinity--what we want
x, y = x - t * bg, y + t * ag
# Return the information
return (x, y, g, u, v, ag, bg, sg)
# Routines for this puzzle ---------------------------------------------
def altitude_reduced(n, h, d, e):
"""Return the number of distinct n-tuples containing only the
values 0, d, and e that sum to h. Assume that all these
numbers are integers and that 0 <= d <= e.
"""
# Handle some impossible special cases
if n < 0 or h < 0:
return 0
# Handle some other simple cases with zero values
if n == 0:
return 0 if h else 1
if 0 == d == e: # all step values are zero
return 0 if h else 1
if 0 == d or d == e: # e is the only non-zero step value
# If possible, return # of tuples with proper # of e's, the rest 0's
return 0 if h % e else comb(n, h // e)
# Handle the main case 0 < d < e
# --Try to get the solution with the fewest possible non-zero days:
# x d's and y e's and the rest zeros: all solutions are given by
# x + t * bg, y - t * ag
solutions_info = byzantine_bball(d, e, h)
if not solutions_info:
return 0 # no way at all to get h from d, e
x, y, _, _, _, ag, bg, _ = solutions_info
# --Loop over all solutions with nonnegative x, y, small enough x + y
result = 0
while y >= 0 and x + y <= n: # at most n non-zero days
# Find multcoeff(x, y, n - x - y), in a faster way
if result == 0: # 1st time through loop: no prev coeff available
amultcoeff = multcoeff(x, y, n - x - y)
else: # use previous multinomial coefficient
amultcoeff = new_multcoeff(amultcoeff, x, y, n - x - y, ag, bg)
result += amultcoeff
x, y = x + bg, y - ag # x+y increases by bg-ag >= 0
return result
def altitudes(input_str=None):
# Get the input
if input_str is None:
input_str = input('Numbers N H1 H2 A B C? ')
# input_str = '100000 0 100000 0 1 2' # replace with prev line for input
n, h1, h2, a, b, c = map(int, input_str.strip().split())
# Reduce the number of parameters by normalizing the values
h_diff = h2 - h1 # net altitude change
a, b, c = sorted((a, b, c)) # a is now the smallest
h, d, e = h_diff - n * a, b - a, c - a # reduce a to zero
# Solve the reduced problem
print(altitude_reduced(n, h, d, e) % (10**9 + 7))
if __name__ == '__main__':
altitudes()
Here are some of my test routines for the main problem. These are suitable for pytest.
# Testing, some with pytest ---------------------------------------------------
import itertools # for testing
import collections # for testing
def brute(n, h, d, e):
"""Do alt_reduced with brute force."""
return sum(1 for v in itertools.product({0, d, e}, repeat=n)
if sum(v) == h)
def brute_count(n, d, e):
"""Count achieved heights with brute force."""
if n < 0:
return collections.Counter()
return collections.Counter(
sum(v) for v in itertools.product({0, d, e}, repeat=n)
)
def test_impossible():
assert altitude_reduced(0, 6, 1, 2) == 0
assert altitude_reduced(-1, 6, 1, 2) == 0
assert altitude_reduced(3, -1, 1, 2) == 0
def test_simple():
assert altitude_reduced(1, 0, 0, 0) == 1
assert altitude_reduced(1, 1, 0, 0) == 0
assert altitude_reduced(1, -1, 0, 0) == 0
assert altitude_reduced(1, 1, 0, 1) == 1
assert altitude_reduced(1, 1, 1, 1) == 1
assert altitude_reduced(1, 2, 0, 1) == 0
assert altitude_reduced(1, 2, 1, 1) == 0
assert altitude_reduced(2, 4, 0, 3) == 0
assert altitude_reduced(2, 4, 3, 3) == 0
assert altitude_reduced(2, 4, 0, 2) == 1
assert altitude_reduced(2, 4, 2, 2) == 1
assert altitude_reduced(3, 4, 0, 2) == 3
assert altitude_reduced(3, 4, 2, 2) == 3
assert altitude_reduced(4, 4, 0, 2) == 6
assert altitude_reduced(4, 4, 2, 2) == 6
assert altitude_reduced(2, 6, 0, 2) == 0
assert altitude_reduced(2, 6, 2, 2) == 0
def test_main():
N = 12
maxcnt = 0
for n in range(-1, N):
for d in range(N): # must have 0 <= d
for e in range(d, N): # must have d <= e
counts = brute_count(n, d, e)
for h, cnt in counts.items():
if cnt == 25653:
print(n, h, d, e, cnt)
maxcnt = max(maxcnt, cnt)
assert cnt == altitude_reduced(n, h, d, e)
print(maxcnt) # got 25653 for N = 12, (n, h, d, e) = (11, 11, 1, 2) etc.

Dijkstra's algorithm with adjacency matrix

I'm trying to implement the following code from here but it won't work correctly.
What I want is the shortest path distances from a source to all nodes and also the predecessors. Also, I want the input of the graph to be an adjacency matrix which contains all of the edge weights.
I'm trying to make it work in just one function so I have to rewrite it. If I'm right the original code calls other functions (from graph.jl for example).
I don't quite understand how to rewrite the for loop which calls the adj() function.
Also, I'm not sure if the input is correct in the way the code is for now.
function dijkstra(graph, source)
node_size = size(graph, 1)
dist = ones(Float64, node_size) * Inf
dist[source] = 0.0
Q = Set{Int64}() # visited nodes
T = Set{Int64}(1:node_size) # unvisited nodes
pred = ones(Int64, node_size) * -1
while condition(T)
# node selection
untraversed_nodes = [(d, k) for (k, d) in enumerate(dist) if k in T]
if minimum(untraversed_nodes)[1] == Inf
break # Break if remaining nodes are disconnected
end
node_ind = untraversed_nodes[argmin(untraversed_nodes)][2]
push!(Q, node_ind)
delete!(T, node_ind)
# distance update
curr_node = graph.nodes[node_ind]
for (neigh, edge) in adj(graph, curr_node)
t_ind = neigh.index
weight = edge.cost
if dist[t_ind] > dist[node_ind] + weight
dist[t_ind] = dist[node_ind] + weight
pred[t_ind] = node_ind
end
end
end
return dist, pred
end
So if I'm trying it with the following matrix
A = [0 2 1 4 5 1; 1 0 4 2 3 4; 2 1 0 1 2 4; 3 5 2 0 3 3; 2 4 3 4 0 1; 3 4 7 3 1 0]
and source 2 i would like to get the distances in a vector dist and the predeccessors in anothe vectore pred.
Right now I'm getting
ERROR: type Array has no field nodes
Stacktrace: [1] getproperty(::Any, ::Symbol) at .\sysimg.jl:18
I guess I have to rewrite it a bit more.
I m thankful for any help.
Assuming that graph[i,j] is a length of path from i to j (your graph is directed looking at your data), and it is a Matrix with non-negative entries, where 0 indicates no edge from i to j, a minimal rewrite of your code should be something like:
function dijkstra(graph, source)
#assert size(graph, 1) == size(graph, 2)
node_size = size(graph, 1)
dist = fill(Inf, node_size)
dist[source] = 0.0
T = Set{Int}(1:node_size) # unvisited nodes
pred = fill(-1, node_size)
while !isempty(T)
min_val, min_idx = minimum((dist[v], v) for v in T)
if isinf(min_val)
break # Break if remaining nodes are disconnected
end
delete!(T, min_idx)
# distance update
for nei in 1:node_size
if graph[min_idx, nei] > 0 && nei in T
possible_dist = dist[min_idx] + graph[min_idx, nei]
if possible_dist < dist[nei]
dist[nei] = possible_dist
pred[nei] = min_idx
end
end
end
end
return dist, pred
end
(I have not tested it extensively, so please report if you find any bugs)

Resources