Numerical Optimization IV: Combinatorial optimization

Eugeniy E. Mikhailov and Greg Bentsen

Logistics and Agenda

Midterm 2


Agenda

Today

Optimization in multiple dimensions

Simulated annealing a.k.a. Metropolis–Hastings algorithm

P(accept)=min(1,e−ΔE/T) \boxed{P(\mathrm{accept})=\min\left(1,e^{-\Delta E/T}\right)}

with

ΔE=Enew−Eold \Delta E=E_{\mathrm{new}}-E_{\mathrm{old}}

Uphill moves are especially probable at high temperature TT, but occur less frequently at low temperature.

Gradually lower TT: explore first, settle later. Good news: we can find global minimum, bad news it might take infinite amount of time.


Question: what is the good starting temperature and final temperature?

Combinatorial optimization problem statement

We still want to optimize (minimize) our multidimensional energy or cost or merit function EE

Find x→=x1,x2,x3⋯\vec{x}=x_1, x_2, x_3 \cdots that minimizes E(x→)E(\vec{x}) subject to g(x→)=0g(\vec{x})=0, h(x→)≤0h(\vec{x}) \le 0

The only difference is that values of the x→\vec{x} are discrete, i.e. any component of the x→\vec{x} can take a countable set of different values.

In this case, we cannot run our golden search algorithm or anything else which assumes continuous space for the x→\vec{x}.

Well, actually we can but constructing a proper constraining function will be a nightmare.

Instead, we have to find a method to search through discrete sets of all possible input values, i.e. try all possible combinations of x→\vec{x} components.

Hence, the name combinatorial optimization.

Example: Backpack problem

Suppose you have a backpack with the given size (volume) and a set of objects with given volumes and monetary (or sentimental) values.

Our job is to find a subset of items that still fits in the backpack and has the maximum combined value.

For simplicity, we will assume that every item occurs only once.

Our job is to maximize E(x→)=∑valueixi=values→⋅x→\begin{aligned} E(\vec{x}) = \sum \text{value}_i x_i = \overrightarrow{\text{values}} \cdot \vec{x} \end{aligned} Subject to the following constraints ∑volumeixi=volumes→⋅x→≤BackpackSize\begin{aligned} \sum \text{volume}_i x_i = \overrightarrow{\text{volumes}} \cdot \vec{x} \le \text{BackpackSize} \end{aligned}

Where xi=(0x_i=(0 or 1)1), i.e. it reflects whether we take this object or not.

Backpack illustration (generated by ChatGPT)
Backpack illustration (generated by ChatGPT)

Brute force backpack optimization: binary encoding

Recall that the x→\vec{x} component is either 0 or 1.

So, x→\vec{x} is a combination of zeros and ones x→=[0,1,0,1,⋯,1,1,0,1,1]\vec{x}=[0,1,0,1,\cdots,1,1,0,1,1].

How would we generate all possible combinations of x→\vec{x} components?

Notice that if there are NN objects then the number of all possible combinations is 2N2^N. Thus the time of the optimization grows exponentially with the number NN of items to chose, but we will find the global optimum.

Backpack optimization with binary encoding: test run

#!/usr/bin/env python3
import numpy as np

def backpack_binary(max_capacity, values, volumes):
    """ Solve backpack problem using binary coding approach."""
    n = len(values)
    max_value = 0
    best_combination = np.zeros(n, dtype=int)

# Try all possible combinations using binary representation

# From 0 to 2^n - 1 (all possible binary numbers with n bits)

for i in range(2**n):
        selected_items = np.zeros(n, dtype=int)
        
        code = bin(i)[2:].zfill(n) # string representing bits
        for i,s in enumerate(code):
            selected_items[i] = 1 if s=="1" else 0
        total_volume = np.sum(volumes * selected_items)
        total_value = np.sum(values * selected_items)

        if total_volume <= max_capacity:
            if total_value > max_value:
                max_value = total_value
                best_combination = selected_items.copy()
    return max_value, best_combination


def print_solution(values, volumes, max_capacity, max_value, selected_items):
    print(f"Backpack Capacity: {max_capacity}")
    print(f"Maximum Value: {max_value}")
    print(f"Selected Items: {selected_items}")
    print(f"{values=}")
    print(f"{volumes=}")
    print(f"Total number of objects: {len(values)}")


if __name__ == "__main__":
    print("====== Example 1 ===========")
    values1 = np.array([12, 10, 20, 15], dtype=int)
    volumes1 = np.array([2, 1, 3, 2], dtype=int)
    max_capacity1 = 5
    
    max_val1, selected1 = backpack_binary(max_capacity1, values1, volumes1)
    print_solution(values1, volumes1, max_capacity1, max_val1, selected1)
    
    print("====== Example 2 ===========")
    N=20
    values2 = (100*np.random.uniform(size=(N))).astype(int)
    volumes2 = (80*np.random.uniform(size=(N))).astype(int)
    max_capacity2 = 50
    
    import time
    t0 = time.time()
    max_val2, selected2 = backpack_binary(max_capacity2, values2, volumes2)
    print_solution(values2, volumes2, max_capacity2, max_val2, selected2)
    print(f"Execution time: {time.time() - t0}")

For realization of this algorithm, have a look at the backpack_binaray.py