Definição do problema

O objetivo é minimizar o custo para transportar as mercadorias das fontes de origem para os diversos destinos (portos).

$\\$

**Conteineres Movimentados**
Porto X Porto Y Porto Z Soma
Fonte A 105
Fonte B 170
Fonte C 130
Soma 155 120 130 405

$\\$

**Custos transportes**
X Y Z
A 7 12 14
B 12 10 13
C 7 9 11

Função objetivo

Z = ( 7x[0, 0] + 12x[0, 1] + 14x[0, 2] + 12x[1, 0] + 10x[1, 1] + 13x[1, 2] + 7x[2, 0] + 9x[2, 1] + 11*x[2, 2] )

Variáveis de restrição

capacidade:

$ x[0, 0] + x[0, 1] + x[0, 2] = 105\\ x[1, 0] + x[1, 1] + x[1, 2] = 170\\ x[2, 0] + x[2, 1] + x[2, 2] = 130\\ $

absorção:

$ x[0, 0] + x[1, 0] + x[2, 0] = 155\\ x[0, 1] + x[1, 1] + x[2, 1] = 120\\ x[0, 2] + x[1, 2] + x[2, 2] = 130\\ $


In [1]:
from collections import OrderedDict
from IPython.display import display
import pandas as pd
import numpy as np
import io

In [2]:
def Z(x):
    _x = x.copy().fillna(0).as_matrix()
    return (
        7*_x[0, 0] + 12*_x[0, 1] + 14*_x[0, 2] + 
        12*_x[1, 0] + 10*_x[1, 1] + 13*_x[1, 2] + 
        7*_x[2, 0] + 9*_x[2, 1] + 11*_x[2, 2]
    )

In [3]:
def calc_n_used_cells(df: pd.DataFrame, verbose=False) -> int:
    """
    """
    n, m = df.shape
    cells = df.values.reshape(1, n*m)
    return cells[cells>0].shape[0]
    

def fix_table(df: pd.DataFrame, verbose=False) -> pd.DataFrame:
    """
    """
    n, m = df.shape
    n_used_cells = calc_n_used_cells(df)
    r = n_used_cells - (n+m-1)
    _used = []
    
    if n <=2:
        return df

    if r < 0:        
        for i in range(n):
            _line = df.iloc[i, :]
            _l_cell_filled = _line[_line.notnull()].shape[0]
            
            if not _l_cell_filled == 1:
                continue
            
            for j in range(m):
                # just basic variable
                if np.isnan(df.iloc[i, j]):
                    continue
                    
                _col = df.iloc[:, j]
                _c_cell_filled = _col[_col.notnull()].shape[0]
                
                if _c_cell_filled == 1:
                    for ii in range(n):
                        _id_ii = '%s,%s' % (ii, j)
                        
                        if _id_ii in _used:
                            continue
                            
                        if np.isnan(df.iloc[ii, j]):
                            df.iloc[ii, j] = 10**-10
                            _used.append(_id_ii)
                            r += 1
                            if r >= 0:
                                break
                if r >= 0:
                    break        
    return df

In [4]:
def calc_basic_variables(df, df_cost, verbose=False):
    basic_variables = OrderedDict()
    n, m = df.shape
    # c_ij
    for i in range(n):
        for j in range(m):
            _id = 'c_%s,%s' % (i, j)

            if not np.isnan(df.iloc[i, j]):
                basic_variables[_id] = df_cost.iloc[i, j]
    return basic_variables

In [5]:
def calc_multipliers(df, basic_variables, verbose=False):
    multipliers = OrderedDict()
    n, m = df.shape
    # u_i, v_j
    for i in range(n):
        for j in range(m):
            _id_u = 'u_%s' % i
            _id_v = 'v_%s' % j
            _id_c = 'c_%s,%s' % (i, j)

            if not _id_c in basic_variables:
                continue

            if i == 0 and j == 0:
                multipliers[_id_u] = 0

            if ((
                not _id_u in multipliers and 
                not _id_v in multipliers
            ) or (
                _id_u in multipliers and 
                 _id_v in multipliers
            )):
                continue

            if not _id_u in multipliers:
                multipliers[_id_u] = (
                    basic_variables[_id_c] - multipliers[_id_v]
                )
            else:
                multipliers[_id_v] = (
                    basic_variables[_id_c] - multipliers[_id_u]
                )
    return multipliers

In [6]:
def calc_no_basic_variables(
    df_cost, basic_variables, multipliers, verbose=False
):
    no_basic_variables = OrderedDict()
    optimo = True
    n, m = df_cost.shape
    
    # P_{ij}
    for i in range(n):
        for j in range(m):
            _id_u = 'u_%s' % i
            _id_v = 'v_%s' % j
            _id_c = 'c_%s,%s' % (i, j)  # cost
            _id_p = 'p_%s,%s' % (i, j)  # quantity

            if _id_c in basic_variables:
                continue

            no_basic_variables[_id_p] = (
                df_cost.iloc[i, j] - 
                multipliers[_id_u] - 
                multipliers[_id_v]
            )

            optimo = (
                optimo and no_basic_variables[_id_p] >= 0
            )
    return optimo, no_basic_variables

In [7]:
def get_minor_no_basic(no_basic_variables, verbose=False):
    φ_id = None
    for k, v in no_basic_variables.items():
        if φ_id is None:
            φ_id = k
            continue

        if v < no_basic_variables[φ_id]:
            φ_id = k
    return φ_id

In [8]:
def get_candidate_nodes(df, ii, jj, verbose=False):
    n, m = df.shape
    _c_used = {}
    for i in range(n):
        if i == ii:
            continue
        if not np.isnan(df.iloc[i, jj]):
            _c_used['%s,%s' % (i, jj)] = True

    _r_used = {}
    for j in range(m):
        if j == jj:
            continue
        if not np.isnan(df.iloc[ii, j]):
            _r_used['%s,%s' % (ii, j)] = True
    return _r_used, _c_used

In [9]:
def rearange_table(df, _r_used, _c_used, ii, jj, verbose=False):
    _used = []

    df2 = df.copy()

    run = True
    for kc in _c_used:
        ic, jc = map(int, kc.split(','))

        for kr in _r_used:
            ir, jr = map(int, kr.split(','))
            
            if verbose:
                print(ic, jr, df.iloc[ic, jr])
                
            if not np.isnan(df.iloc[ic, jr]):
                _used.append((
                    (ic, jr, +1, df.iloc[ic, jr]), 
                    (ic, jc, -1, df.iloc[ic, jc]), 
                    (ir, jr, -1, df.iloc[ir, jr]), 
                ))

                φ, φ_id = (
                    (df.iloc[ic, jc], (ic, jc))
                    if df.iloc[ic, jc] < df.iloc[ir, jr] else 
                    (df.iloc[ir, jr], (ir, jr))
                )

                for ki, kj, s, v in _used[0]:
                    print(ki, kj, s)
                    df2.iloc[ki, kj] = v + (φ*s)

                df2.iloc[ii, jj] = φ
                df2.iloc[φ_id[0], φ_id[1]] = np.nan
                run = False
                break

        if not run:
            break
    if verbose:
        print('\nφ = ', φ, '\n')
        print('\nSelected:', _used, φ)
        display(df2)
        
    return df2

In [10]:
# cost matrix
values_text = '''
0,1,2
0,7,12,14
1,12,10,13
2,7,9,11
'''
df_cost = pd.read_csv(io.StringIO(values_text))

print('COSTS')
df_cost


COSTS
Out[10]:
0 1 2
0 7 12 14
1 12 10 13
2 7 9 11

In [11]:
# restriction
capacity = [105, 170, 130]
absortion = [155, 120, 130]

Northwest Corner


In [12]:
# initial basic solution with northwest corner
values_text = '''
0,1,2
0,,,
1,,,
2,,,
'''
df_initial = pd.read_csv(io.StringIO(values_text))

_capacity = list(capacity)
_absortion = list(absortion)

# iter by row
print(
    '#ID'.ljust(5, ' '), 
    'CAPACITY'.ljust(15, ' '), 
    'ABSORTION'.ljust(15, ' ')
)

ii = 1

print(
    ('#%s' % ii).ljust(5, ' '), 
    str(capacity).ljust(15, ' '), 
    str(absortion).ljust(15, ' ')
)

i = 0
while i < df_initial.shape[0]:
    # iter by column
    j = 0
    while j < df_initial.shape[1]:
        ii += 1
        r = _capacity[i] - _absortion[j]
        
        if r > 0:
            df_initial.iloc[i, j] = _absortion[j]
            _absortion[j] = 0
            _capacity[i] = r
        elif r < 0:
            df_initial.iloc[i+0, j] = _capacity[i]
            _capacity[i] = 0
            _absortion[j] = abs(r)
        else:
            df_initial.iloc[i+0, j] = _capacity[i]
            _capacity[i] = 0
            _absortion[j] = 0
            
        print(
            ('#%s' % ii).ljust(5, ' '), 
            str(_capacity).ljust(15, ' '), 
            str(_absortion).ljust(15, ' ')
        )
        
        if _capacity[i] == 0:
            break
        j += 1
    i += 1

df_initial = df_initial.replace(0, np.nan)
df_initial


#ID   CAPACITY        ABSORTION      
#1    [105, 170, 130] [155, 120, 130]
#2    [0, 170, 130]   [50, 120, 130] 
#3    [0, 120, 130]   [0, 120, 130]  
#4    [0, 0, 130]     [0, 0, 130]    
#5    [0, 0, 130]     [0, 0, 130]    
#6    [0, 0, 130]     [0, 0, 130]    
#7    [0, 0, 0]       [0, 0, 0]      
Out[12]:
0 1 2
0 105 NaN NaN
1 50 120 NaN
2 NaN NaN 130

In [13]:
df = df_initial.copy()
# calculate the cost
z = Z(df)
zs = [z]

# check the result
assert z == 105*7+ 50*12 + 120*10 + 130*11
print('Z =', z)

# check
print('\ninitial table')
df = df_initial.copy()
display(df)

it_n = 0
optimo = False

while not optimo:
    it_n += 1
    print('\n' + '#'*80)
    print('# Iteraction #:', it_n)
    print('#'*80 + '\n')
    df = fix_table(df)
    # df.iloc[1, 2] = 10**-10
    print('>>', 'table fixed')
    display(df)

    n, m = df.shape

    '''
    ## Calculation of multipliers

    ### basic variables

    $u_i + v_j = c_{ij}$

    where

    $u_0 = 0$

    '''

    basic_variables = calc_basic_variables(df, df_cost, verbose=True)

    multipliers = calc_multipliers(df, basic_variables, verbose=True)

    '''
    ### No basic variable

    $P_{ij} = c_{ij} - u_i - v_j$

    '''
    try:
        optimo, no_basic_variables = calc_no_basic_variables(
            df_cost, basic_variables, multipliers, verbose=True
        )
    except:
        break
    
    if optimo:
        break

    φ_id = get_minor_no_basic(no_basic_variables, verbose=True)

    ii, jj = map(int, φ_id.split('_')[1].split(','))
    print('>>', 'ii', 'jj')
    print(ii, jj)

    _r_used, _c_used = get_candidate_nodes(df, ii, jj, verbose=True)
    
    print('>>', '_c_used', '_r_used')
    print(_c_used)
    print(_r_used)

    df = rearange_table(df, _r_used, _c_used, ii, jj, verbose=True)
    zs.append(Z(df))
print('\n\n' + '#'*80)
print('# RESULTS')
print('#'*80)

for i, v in enumerate(zs):
    print('>> %s:' % i, v)
    
print('\n>> final answer:')
display(df.fillna(''))


Z = 3965.0

initial table
0 1 2
0 105 NaN NaN
1 50 120 NaN
2 NaN NaN 130
################################################################################
# Iteraction #: 1
################################################################################

>> table fixed
0 1 2
0 105 NaN 1.000000e-10
1 50 120 NaN
2 NaN NaN 1.300000e+02
>> ii jj
1 2
>> _c_used _r_used
{'0,2': True, '2,2': True}
{'1,1': True, '1,0': True}
0 1 nan
0 0 105.0
0 0 1
0 2 -1
1 0 -1

φ =  1e-10 


Selected: [((0, 0, 1, 105.0), (0, 2, -1, 1e-10), (1, 0, -1, 50.0))] 1e-10
0 1 2
0 105 NaN NaN
1 50 120 1.000000e-10
2 NaN NaN 1.300000e+02
################################################################################
# Iteraction #: 2
################################################################################

>> table fixed
0 1 2
0 105 NaN NaN
1 50 120 1.000000e-10
2 NaN NaN 1.300000e+02
>> ii jj
2 0
>> _c_used _r_used
{'1,0': True, '0,0': True}
{'2,2': True}
1 2 1e-10
1 2 1
1 0 -1
2 2 -1

φ =  49.9999999999 


Selected: [((1, 2, 1, 1e-10), (1, 0, -1, 49.999999999899998), (2, 2, -1, 130.0))] 49.9999999999
0 1 2
0 105 NaN NaN
1 NaN 120 50
2 50 NaN 80
################################################################################
# Iteraction #: 3
################################################################################

>> table fixed
0 1 2
0 105 NaN NaN
1 NaN 120 50
2 50 NaN 80

################################################################################
# RESULTS
################################################################################
>> 0: 3965.0
>> 1: 3965.0
>> 2: 3815.0

>> final answer:
0 1 2
0 105
1 120 50
2 50 80