Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Csúszóablakos technika

Egy sorozat (lista) különféle szempontok szerint kiválasztott folytonos részsorozatait csúszóablakos módszerrel állíthatjuk elő, egymásba ágyazott ismétlésekkel. Mivel a funkcionális nyelvekben nincs ciklus, az ismétlést rekurzióval valósítjuk meg, a részeredményeket egy, esetleg több akkumulátorban gyűjtjük. Mivel a gyűjtéshez plusz paraméter(ek)re van szükség, rendszerint segédfüggvényeket is definiálunk.

A példafeladat egy számlista maximális összegű folytonos részlistáinak előállítása. Például az [1, 2, 3, 4, -10, 4, 3, 2, 1] lista maximális összegű folytonos részlistái: [1, 2, 3, 4], [1, 2, 3, 4, -10, 4, 3, 2, 1] és [4, 3, 2, 1], összegük 10. A függvény visszatérési értéke olyan pár legyen, amelynek első eleme a részlisták összege, második eleme ezen maximális összegű részlisták listája; a fenti példában:

{10, [[1, 2, 3, 4], [1, 2, 3, 4, -10, 4, 3, 2, 1], [4, 3, 2, 1]]}

A reszlistak/1 függvény specifikációja:

  @spec reszlistak(xs::[integer()]) :: {max::integer(), rss::[[integer()]]}
  # rss az xs max összegű részlistáinak listája

Két megközelítést nézünk meg: az elsőben előállítjuk az összes folytonos részlistát, és a maximumot a legvégén keressük meg; a másodikban már a részlisták gyűjtése közben csak a maximális összegűeket tartjuk meg.

1. Maximumkeresés a legvégén

11. A lista elejétől kezdődő összes folytonos részlista

Az ablak bal széle a lista eleje, a jobb széle elemenként halad előre. Az ss akkumulátor fordított sorrendben tartalmazza az eddig bevett elemeket (így az új elemet olcsón elé lehet fűzni), a zss akkumulátor gyűjti az eddigi részlistákat, helyes sorrendre fordítva:

defmodule Reszlistak11 do
  def reszlistak([x|xs]), do: reszlistak(xs, [x], [[x]])
  def reszlistak([]), do: []

  def reszlistak([y|ys], ss, zss) do
    ss_uj = [y|ss]
    reszlistak(ys, ss_uj, [Enum.reverse(ss_uj) | zss])
  end
  def reszlistak([], _ss, zss), do: zss
end
Reszlistak11.reszlistak([1,2,3,4,5]) |> Enum.reverse()
[[1], [1, 2], [1, 2, 3], [1, 2, 3, 4], [1, 2, 3, 4, 5]]

12. A lista összes folytonos részlistája

Az ablak bal szélét is léptetni kell: a lista minden szuffixumára (a lista egyre rövidülő farkára) előállítjuk az elejétől kezdődő részlistákat:

defmodule Reszlistak12 do
  def reszlistak(xxs), do: reszlistak(xxs, [])

  def reszlistak([_x|xs]=xxs, zss) do
    reszlistak(xs, Reszlistak11.reszlistak(xxs) ++ zss)
  end

  def reszlistak([], zss), do: zss
end
Reszlistak12.reszlistak([1,2,3,4,5]) |> Enum.reverse()
[
  [1],
  [1, 2],
  [1, 2, 3],
  [1, 2, 3, 4],
  [1, 2, 3, 4, 5],
  [2],
  [2, 3],
  [2, 3, 4],
  [2, 3, 4, 5],
  [3],
  [3, 4],
  [3, 4, 5],
  [4],
  [4, 5],
  [5]
]

12x. Az összes folytonos részlista és az összegük

Kitérőként lássuk a részösszegeket is:

defmodule Reszlistak12x do
  def reszlistak(xs), do: reszlistak(Reszlistak12.reszlistak(xs), [])

  def reszlistak([xs|xss], zss), do: reszlistak(xss, [{Enum.sum(xs), xs} | zss])
  def reszlistak([], zss), do: zss
end
Reszlistak12x.reszlistak([1,2,-3,-4,5]) |> Enum.reverse()
[
  {5, [5]},
  {1, [-4, 5]},
  {-4, [-4]},
  {-2, [-3, -4, 5]},
  {-7, [-3, -4]},
  {-3, [-3]},
  {0, [2, -3, -4, 5]},
  {-5, [2, -3, -4]},
  {-1, [2, -3]},
  {2, [2]},
  {1, [1, 2, -3, -4, 5]},
  {-4, [1, 2, -3, -4]},
  {0, [1, 2, -3]},
  {3, [1, 2]},
  {1, [1]}
]

13. A maximális összegű folytonos részlisták

Az összes részlista közül a maximális összegűeket egy újabb bejárással választjuk ki. A max akkumulátor az eddigi legnagyobb összeg, a zss az ilyen összegű részlisták listája. Ha egy részlista összege nagyobb az eddigi maximumnál, új maximumot és új gyűjtést kezdünk; ha egyenlő vele, hozzávesszük; ha kisebb, eldobjuk:

defmodule Reszlistak13 do
  @spec reszlistak(xs::[integer()]) :: {max::integer(), rss::[[integer()]]}
  # rss az xs max összegű részlistáinak listája
  def reszlistak([_|_]=xs) do
    [rs|rss] = Reszlistak12.reszlistak(xs)
    reszlistak(rss, Enum.sum(rs), [rs])
  end
  def reszlistak([]), do: []

  @spec reszlistak(xs::[integer()], max::integer(), zss::[[integer()]]) :: rss::[[integer()]]
  # rss az xs max összegű részlistáinak listája; a részlistákat zss-ben gyűjtjük
  def reszlistak([xs|xss], max, zss) do
    sum = Enum.sum(xs)
    cond do
      sum > max -> reszlistak(xss, sum, [xs])
      sum == max -> reszlistak(xss, max, [xs | zss])
      true -> reszlistak(xss, max, zss)
    end
  end
  def reszlistak([], max, zss), do: {max, zss}
end

A cond kifejezés sorban kiértékeli a feltételeket, és az első igaz feltételhez tartozó kifejezés értékét adja; a true ág a „minden más” eset.

Reszlistak13.reszlistak([1,2,13,4,5]) |> IO.inspect()
Reszlistak13.reszlistak([1,2,13,4,-10,4,13,2,1]) |> IO.inspect()
Reszlistak13.reszlistak([-13,-13,-13]) |> IO.inspect()
Reszlistak13.reszlistak([0,0,0]) |> IO.inspect()
Reszlistak13.reszlistak([-13]) |> IO.inspect()
Reszlistak13.reszlistak([]) |> IO.inspect()
{25, [[1, 2, 13, 4, 5]]}
{30, [[1, 2, 13, 4, -10, 4, 13, 2, 1]]}
{-13, [[-13], [-13], [-13]]}
{0, [[0], [0, 0], [0, 0, 0], [0], [0, 0], [0]]}
{-13, [[-13]]}
[]

2. Maximumkiválasztás a részlisták gyűjtésekor

21. A lista elejétől kezdődő folytonos részlisták és összegük

defmodule Reszlistak21 do
  def reszlistak([x|xs]), do: reszlistak(xs, [x], [{x, [x]}])
  def reszlistak([]), do: []

  def reszlistak([y|ys], ss, zss) do
    ss_uj = [y|ss]
    reszlistak(ys, ss_uj, [{Enum.sum(ss_uj), Enum.reverse(ss_uj)} | zss])
  end
  def reszlistak([], _ss, zss), do: zss
end
Reszlistak21.reszlistak([1,2,3,4,5]) |> Enum.reverse() |> IO.inspect()
Reszlistak21.reszlistak([1,2,3,-3,-2,5]) |> Enum.reverse() |> IO.inspect()
[
  {1, [1]},
  {3, [1, 2]},
  {6, [1, 2, 3]},
  {10, [1, 2, 3, 4]},
  {15, [1, 2, 3, 4, 5]}
]
[
  {1, [1]},
  {3, [1, 2]},
  {6, [1, 2, 3]},
  {3, [1, 2, 3, -3]},
  {1, [1, 2, 3, -3, -2]},
  {6, [1, 2, 3, -3, -2, 5]}
]

22a. A lista elejétől kezdődő, maximális összegű folytonos részlisták

Most már gyűjtés közben csak a maximális összegű részlistákat tartjuk meg:

defmodule Reszlistak22a do
  def reszlistak([x|xs]), do: reszlistak(xs, [x], x, [[x]])
  def reszlistak([]), do: []

  def reszlistak([y|ys], ss, max, zss) do
    ss_uj = [y|ss]
    ss_uj_rev = Enum.reverse(ss_uj) # |> IO.inspect(label: "ss_uj_rev")
    sum = Enum.sum(ss_uj_rev) # |> IO.inspect(label: "sum")
    cond do
      sum > max -> reszlistak(ys, ss_uj, sum, [ss_uj_rev])
      sum == max -> reszlistak(ys, ss_uj, max, [ss_uj_rev | zss])
      true -> reszlistak(ys, ss_uj, max, zss)
    end
  end
  def reszlistak([], _ss, max, zss), do: {max, Enum.reverse(zss)}
end
Reszlistak22a.reszlistak([1,2,3,-3,-2,5]) |> IO.inspect()
Reszlistak22a.reszlistak([6,-6,1,2,3,-3,-2,5]) |> IO.inspect()
{6, [[1, 2, 3], [1, 2, 3, -3, -2, 5]]}
{6, [[6], [6, -6, 1, 2, 3], [6, -6, 1, 2, 3, -3, -2, 5]]}

22b. Ugyanez Enum.take-kel

Ez a változat a teljes listából indul, és a részlistákat az Enum.take/2-vel egyre rövidebb prefixumként állítja elő:

defmodule Reszlistak22b do
  def reszlistak(xs), do: reszlistak(xs, length(xs)-1, Enum.sum(xs), [xs])

  def reszlistak(_xs, 0, max, zss), do: {max, zss}
  def reszlistak(xs, len, max, zss) do
    ss = Enum.take(xs, len)
    sum = Enum.sum(ss)
    cond do
      sum > max -> reszlistak(xs, len-1, sum, [ss])
      sum == max -> reszlistak(xs, len-1, max, [ss | zss])
      true -> reszlistak(xs, len-1, max, zss)
    end
  end
end
Reszlistak22b.reszlistak([1,2,3,-3,-2,5]) |> IO.inspect()
Reszlistak22b.reszlistak([6,-6,1,2,3,-3,-2,5]) |> IO.inspect()
{6, [[1, 2, 3], [1, 2, 3, -3, -2, 5]]}
{6, [[6], [6, -6, 1, 2, 3], [6, -6, 1, 2, 3, -3, -2, 5]]}

23. A maximális összegű folytonos részlisták

A lista minden szuffixumára meghívjuk a 22a vagy a 22b változatot, és az eredményekből csak a maximális összegűeket tartjuk meg. Az elejétől kezdődő maximális részlistákat előállító függvényt paraméterként (mxrls) adjuk át, így a két változat ugyanazzal a kerettel használható:

defmodule Reszlistak23 do
  @type mxrls() :: ([integer()] -> [[integer()]])
  @spec reszlistak(f::mxrls(), xs::[integer()]) :: {max::integer(), rss::[[integer()]]}
  # rss az xs max összegű részlistáinak listája
  # f egy számlista elejétől kezdődő, folytonos, max. összegű részlistákat adja eredményül
  def reszlistak(mxrls, [_x|xs]=xxs), do: reszlistak(mxrls, xs, mxrls.(xxs))
  def reszlistak(_mxrls, []), do: {}

  def reszlistak(mxrls, [_y|ys]=yys, {maxsum, zss}) do
    {max, mss} = mxrls.(yys)
    cond do
      max > maxsum -> reszlistak(mxrls, ys, {max, mss})
      max == maxsum -> reszlistak(mxrls, ys, {maxsum, zss ++ mss}) # hatékonyság vs. sorrend!
      true -> reszlistak(mxrls, ys, {maxsum, zss})
    end
  end
  def reszlistak(_mxrls, [], maxlists), do: maxlists
end

A zss ++ mss megőrzi a részlisták sorrendjét, de a ++ az első listát lemásolja; a mss ++ zss olcsóbb lenne, de felcserélné a sorrendet.

(&Reszlistak22a.reszlistak/1) |> Reszlistak23.reszlistak([1,2,3,4,5]) |> IO.inspect()
(&Reszlistak22a.reszlistak/1) |> Reszlistak23.reszlistak([1,2,3,4,-10,4,3,2,1]) |> IO.inspect()
(&Reszlistak22b.reszlistak/1) |> Reszlistak23.reszlistak([1,2,3,4,5]) |> IO.inspect()
(&Reszlistak22b.reszlistak/1) |> Reszlistak23.reszlistak([1,2,3,4,-10,4,3,2,1]) |> IO.inspect()
{15, [[1, 2, 3, 4, 5]]}
{10, [[1, 2, 3, 4], [1, 2, 3, 4, -10, 4, 3, 2, 1], [4, 3, 2, 1]]}
{15, [[1, 2, 3, 4, 5]]}
{10, [[1, 2, 3, 4], [1, 2, 3, 4, -10, 4, 3, 2, 1], [4, 3, 2, 1]]}

Futási idők összehasonlítása

A futási idők mérésére véletlenszerű számok listáját használjuk. Egy 10 hosszúságú, a -5..5 tartományba eső számokat tartalmazó sorozatot például így állíthatunk elő (az eredmény minden futtatáskor más):

for _ <- 1..10, do: Enum.random(-5..5)
[-5, 4, -2, -2, -5, 0, 2, 1, 4, -3]

A mérés egy 1000 elemű véletlen listán:

xs = for _ <- 1..1000, do: Enum.random(-5..5)
Benchee.run(
  %{
    "utolag keres"  =>
      fn -> Reszlistak13.reszlistak(xs) end,
    "menet kozben, sajat fv"  =>
      fn -> Reszlistak23.reszlistak(&Reszlistak22a.reszlistak/1, xs) end,
    "menet kozben, Enum.take"  =>
      fn -> Reszlistak23.reszlistak(&Reszlistak22b.reszlistak/1, xs) end
  }# , profile_after: true
)
:ok

Az eredmény (Elixir 1.20.2, Erlang 29.0.6, AMD Ryzen AI 9 HX 370):

Name                              ips        average  deviation         median         99th %
menet kozben, sajat fv           2.26      442.21 ms     ±2.22%      438.00 ms      465.67 ms
menet kozben, Enum.take          1.42      702.56 ms     ±1.30%      702.25 ms      720.61 ms
utolag keres                     0.33     3016.53 ms    ±44.34%     3169.76 ms     4270.77 ms

Comparison: 
menet kozben, sajat fv           2.26
menet kozben, Enum.take          1.42 - 1.59x slower +260.35 ms
utolag keres                     0.33 - 6.82x slower +2574.31 ms

A menet közbeni maximumkiválasztás több mint hatszor gyorsabb, mert nem kell az összes (1000 elemű listánál félmilliónál több) részlistát egyszerre tárolni és utólag újra bejárni.

Forrás: dp26a-fp3ea.pdf (4. dia), dp26a-fp3ea-reszlistak-kihagy_bevesz_rek.livemd