23. lokakuu 2011, 15:07

Korintäyttö ja ahne menetelmä

Mietiskelen tässä korintäyttöä (bin packing) ja sitä, miten ahne menetelmä on joko ylipolynomiaalinen tai sitten epäoptimaalinen. Luultavasti se on vain liian epätehokas algoritmi.

Korintäyttö

Olkoon meillä joukko lukuja väliltä (0,1]. Kysymys kuuluu, kuinka moneen (yhden suuruiseen) koriin nämä luvut voidaan pienimmillään asettaa.

Tämä ongelma on todistetusti NP-täydellinen, eli aika vaikea tehtävä. Ahneella ratkaisumenetelmällä ongelmaan saadaan hyvin simppeli ratkaisu, mutta se joko tuottaa epäoptimaalisia ratkaisuja tai sitten sen aikavaativuus on eksponentiaalinen. Jotenkin kuulostaisi siltä, että se on hitaampi kuin miltä näyttää.

Ahne algoritmi

Otetaan kasasta lukuja suuruusjärjestyksessä pois, isoimmasta alkaen. Pannaan se simppelisti ensimmäiseen koriin b_i, johon se mahtuu. Jos ei mahdu mihinkään entisistä koreista, niin otetaan uusi, tyhjä kori. Kun alkuperäinen kasa on tyhjentynyt, niin katsotaan korien lukumäärä. Toki totuusarvo-ongelmana (riittääkö nkoria?) meille riittää lopettaa tarkastelu siihen paikkaan, jos kori numero n+1pitäisi ottaa käyttöön. Huonoin tapaus johtaa kaikkien tarkasteluun.

Ääritapauksessa oletetaan, että kaikki alkiot ovat lukuja 0.51, (tai yleisemmin kaikki alkiot lukuja 0.5 < x \le 1, eli yhteen koriin ei mitenkään mahdu yhtä alkiota enempää. Olkoon näitä lukuja nkappaletta. Algoritmi ottaa suurimman, eli alkion 0.51kasasta ja valitsee ensimmäisen vapaan korin b_0sille. Toista alkiota valittaessa havaitaan, ettei alkio mahdu ensimmäiseen koriin, koska 0.51 \times 2 = 1.02 > 1” />. Valitaan kori <img class=siten. Induktiolla voitaisiin havaita, että tällä tavalla nkappaletta lukuja 0.51voidaan täyttää vain nkoriin, ja että siihen menee \sum_{k=1}^{n} kaskelta, koska vapaita koreja aina aloitetaan tarkastelemaan alusta. Tämähän olisi O(n^2).

Ja jos luvut ovat niin pieniä, että ne kaikki mahtuvat yhteen koriin, niin algoritmi ajaa lineaarisessa ajassa O(n). Melkein uskaltaisin väittää, että yleinen aikavaativuus on O(nm), missä non lukujen lukumäärä ja mon lopullinen korien lukumäärä.

Eli tuskinpa on aikavaativuudesta kiinni. Millä ihmeen tavalla tämä ei ole optimaalinen? Vai teinkö virheellisen oikaisun aikavaativuustarkastelussa? Kenties kumpikaan käsittelemistäni tapauksista ei oikeasti ole ajankäytöllisesti pahin tapaus.

Ei optimaalinen

No niin. Sitten uteliaisuus voitti ja menin katsomaan wikipediasta. Esittelemäni algoritmi on nopeampi kuin O(n^2), mutta ei todellakaan aina optimaalinen. Esittelemäni lajiteltu syöte; keksitään vastaesimerkki. Mutta jopa on vaikea keksiä! Wikipedian artikkelissa esitetään ihan fiksu esitys, että menetelmä ei ole optimaalinen, koska useampien korien ei anneta olla puolityhjiä samanaikaisesti. Helpompi olisi kai löytää vastaesimerkki, jolla epäoptimaalisuus todettaisiin.

Pythonillahan on helppo kirjoittaa tämä ahne algoritmi:

def binpack(items):
  bins = []
  for x in sorted(items):
    for b in bins:
      if sum(b) + x < 1:
        b.append(x)
        break
    else:
      bins.append([x])
  return bins

Harmi vain, että liukulukuaritmetiikka antaa hankaluuksia.
Jos rajoitun kokonaislukuakselille 1..1000, (voidaan ajatella siis liukulukuina 0.001…1.000) niin jännää kyllä, en kykene edes python-ohjelmani avulla löytämään vastaesimerkkiä. Wikipediakin sanoo, että lajitellussa syötteessä tämä ahne approksimointi (nimeltään first fit decreasing order) on erityisen tehokas.

Testiohjelmani hakee vastaesimerkkejä ensin ajamalla approksimoinnin ja sitten optimaalisen. Koska luonnollisesti optimaalinen malli on ylipolynomaalinen, niin kovin isoilla syötteillä ei enää kykene etsimään.

Tässä on esitettynä kokonaisluvuille toimiva algoritmi sekä testiohjelmat:

def binpack_greedy(items):
  bins = []
  for x in items:
    for b in bins:
      if sum(b) + x <= 1000:
        b.append(x)
        break
    else:
      bins.append([x])
  return bins

def binpack_greedy_decr(items):
  "Very powerful variation of the first fit algorithm."
  return binpack_greedy(sorted(items, reverse=True))

def binpack_optimal(items):          
  """Return the optimal number of bins needed. We'll use nonsorted greedy
  algorithm with all possible permutations so the complexity is O(n!)"""
  import itertools

  best_solution = []
  best_size = len(items) + 1 

  for perm in itertools.permutations(items):
    solution = binpack_greedy(perm)
    size = len(solution)
    if size < best_size:
      best_solution = solution
      best_size = size

  return best_solution

##### tests
import random
def randomlist(size):
  l = []
  while size > 0:
    n = random.randint(1, 1000)
    l.append(n)
    size -= 1
  return l

def try_to_find_out(times, number = 5):
  "let's try to find a counterexample"
  for _ in range(times):
    foo = randomlist(number)
    greedy = len(binpack_greedy_decr(foo))
    optimal = len(binpack_optimal(foo))
    if greedy != optimal:
      print "input", sorted(foo, reverse=True)
      print "optimal", binpack_optimal(foo)
      print "greedy ", binpack_greedy_decr(foo)
      break           

Ja mitään tuloksia en ole löytänyt. Kunnes sitten kymmentä minuuttia myöhemmin.

Vastaesimerkki

Vaihdoin syötteet takaisin liukuluvuiksi ja ajelin epätoivoissani sarjoja sisään. Python-tulkki antoi kuin antoikin sopivat syötteet. Ellen väärin ajatellut asiaa, niin tulos vaikuttaa seuraavan ihan aidoista lähtökohdista, ei esimerkiksi liukulukulaskennan epätarkkuuksista (voi syntyä virheellisiä summauksia tjsp):

>>> try_to_find_out(100, 6)
input [0.44604504320199823,
       0.39084470840484253,
       0.36302114260833085,
       0.29242927571305433,
       0.24068549251130467,
       0.21981144650723328]
optimal [[0.39084470840484253,      # kaksi koria
          0.21981144650723328,
          0.36302114260833085],
         [0.44604504320199823,
          0.29242927571305433,
          0.24068549251130467]]
greedy [[0.44604504320199823,       # kolme koria
         0.39084470840484253],
        [0.36302114260833085,
         0.29242927571305433,
         0.24068549251130467],
        [0.21981144650723328]]
>>> ce                              # counter example, sama kuin yo. `input`
[0.44604504320199823,
 0.39084470840484253,
 0.36302114260833085,
 0.29242927571305433,
 0.24068549251130467,
 0.21981144650723328]
>>> sum(ce)                         # katsellaan korien summat
1.9528371089467642
>>> map(sum, binpack_greedy_decr(ce))  # ahne
[0.83688975160684076,
 0.89613591083268984,
 0.21981144650723328]
>>> map(sum, binpack_optimal(ce))   # optimaalinen
[0.97915981142635722,
 0.97367729752040666]

Jooh. Huomenna on aiheesta tentti. Toivottavasti tämä korvasi nyt pänttäilyn riittävän hyvin.

Tageja: , ,

---
---

---

Aiheen vierestä