Advent of code 2020 elixir 解法回顧 (上)

Advent of code 2020 elixir 解法回顧 (上)node

網絡上有不少有趣的編程題庫,其中 Advent of code 近幾年收到愈來愈多人的關注。緣由是題目頗有趣,結合聖誕節主題,在聖誕節前的25天天天一題。另外不限制編程語言,只須要輸入正確答案便可。每作出一題還會獲得一顆聖誕樹上的小星星,有成就感。今年我使用 elixir 來解題,轉眼間已經作了過半的題目,因而寫一篇文章來回顧一下。若是你也想嘗試解題,建議作完再看。正則表達式

第一天

第一部分是從一個由數字組成的列表中,找到兩個數,它們的和等於2020,返回它們的積。 爲了讓查詢快一些,我用了一個 MapSet (查詢複雜度 O(1)) 來替代 List (查詢複雜度O(n)) 存儲這些數字,而後遍歷 List,在 MapSet 中尋找可以和當前的數相加等於 2020 的數。編程

Enum.reduce_while(list, mapset, fn x, acc ->
  if MapSet.member?(acc, 2020 - x) do
    {:halt, x * (2020 - x)}
  else
    {:cont, MapSet.put(acc, x)}
  end
end)

第二部分是把兩個數變成了三個數。能夠三次遍歷 List,而後使用 raise 語句來拋出異常從在找到知足條件的數時打破循環。因爲題目規定不能夠重複使用同一個位置的數,因此須要記錄每一個數的 index。 也可使用三次嵌套的 reduce_while 來尋找答案,好處是不須要 raiseindex 了,缺點是代碼看起來很醜。網絡

Enum.reduce_while(list, list, fn x, list ->
  case Enum.reduce_while(tl(list), tl(list), fn y, list ->
         case Enum.reduce_while(tl(list), tl(list), fn z, list ->
                if x + y + z == 2020 do
                  {:halt, x * y * z}
                else
                  {:cont, tl(list)}
                end
              end) do
           [] ->
             {:cont, tl(list)}

           a ->
             {:halt, a}
         end
       end) do
    [] ->
      {:cont, tl(list)}

    a ->
      {:halt, a}
  end
end)

次日

第一部分和第二部分都是根據既定的規則來統計合法的 「密碼」 的數量,不一樣之處是使用的規則不一樣。第一部分的規則是字符串裏某個字母出現的次數,例如 1-2 z 表示字符串裏只能由 1 到 2 個 z 。比較麻煩的地方可能就是把規則解碼出來,若是你熟悉正則表達式會比較快。而後根據規則檢查每一個 "password"。編程語言

validate = fn {min, max, [letter], pass} ->
  String.to_charlist(pass)
  |> Enum.count(fn x -> x == letter end)
  |> (fn x -> if x >= min and x <= max, do: 1, else: 0 end).()
end

第二部分的規則是「密碼」的某兩個位置裏,必須有且只有一個位置是給定的字母。例如 1-2 z 表示「密碼」的第一位和第二位有且只有一位是 z。能夠用 erlang 的字符串匹配來檢查某一位的值,而後使用異或邏輯來描述 「有且只有」。函數

xor = fn a, b ->
  if not (a and b) and (a or b) do
    1
  else
    0
  end
end

toboggan_corporate = fn {left, right, [code], pass} ->
  s1 = left - 1
  s2 = right - 1

  match?(<<_::bytes-size(s1), ^code, _::binary>>, pass)
  |> xor.(match?(<<_::bytes-size(s2), ^code, _::binary>>, pass))
end

第三天

第三天是頗有畫面感的一道題,甚至有人把它作成了動畫。在一個二維的地圖上,從左上角出發,按照必定的規則跳躍行進,統計一路上會遇到多少顆樹(用 # 表示),而後返回全部的規則裏會遇到的樹的數量的乘積。第一部分和第二部分惟一的區別只是行動的規則不一樣。動畫

slops1 = [
  {3, 1}
]

slops2 = [
  {1, 1},
  {3, 1},
  {5, 1},
  {7, 1},
  {1, 2}
]

對於向下和向右,須要用不一樣的方式去處理,向右的時候,若是到達了地圖邊緣,像不少老遊戲的處理方式同樣,又從地圖的左邊出現(也能夠認爲是把地圖複製了一份到右邊)。設計

check_tree = fn s, p ->
  p = rem(p, String.length(s))
  String.at(s, p) == "#"
end

for {slops, label} <- Enum.with_index([slops1, slops2]) do
  Enum.reduce(slops, 1, fn {right, down}, acc ->
    s =
      data
      |> Enum.take_every(down)
      |> tl()
      |> Enum.reduce({0, 0}, fn x, {count, p} ->
        p = p + right

        if check_tree.(x, p) do
          {count + 1, p}
        else
          {count, p}
        end
      end)
      |> elem(0)

    acc * s
  end)
  |> IO.inspect(label: "Day3 Part#{label}")
end

第四天

這一題是很常見的業務邏輯:數據格式校驗。首先我把數據解析成了 map 格式便於處理,第一部分能夠用 match? 來完成,map 結構裏的元素是不保證順序的,因此在匹配的時候也不會要求順序一致。說到這個,elixir 裏有一個可能會被坑到地方,就是若是你寫了兩個 function clauses,都用 map 去匹配,即便第二個 clause 永遠不會匹配到,elixir compiler 也是不會提示的。code

第一部分的解法:排序

data
|> Enum.count(&match?(%{ecl: _, pid: _, eyr: _, hcl: _, byr: _, iyr: _, hgt: _}, &1))

第二部分就比較複雜了,仍是那句話,若是你很熟悉正則表達式,就能夠很快地解答出來,若是是像我同樣只會用最基礎的正則,那可能就麻煩一點。但好處是正則作不到的功能,咱們也能夠作到。

rules = [
  {:byr,
   rcheck.(~r/^[0-9]{4}$/)
   |> fand.(range.(1920, 2002))},
  {:iyr,
   rcheck.(~r/^[0-9]{4}$/)
   |> fand.(range.(2010, 2020))},
  {:eyr,
   rcheck.(~r/^[0-9]{4}$/)
   |> fand.(range.(2020, 2030))},
  {:hgt,
   rcapture.(~r/^([0-9]{2})in$/)
   |> fmap.(range.(59, 76))
   |> ffor.(
     rcapture.(~r/^([0-9]{3})cm$/)
     |> fmap.(range.(150, 193))
   )},
  {:hcl, rcheck.(~r/^#[0-9a-f]{6}$/)},
  {:ecl, fn x -> x in ~w(amb blu brn gry grn hzl oth) end},
  {:pid, rcheck.(~r/^[0-9]{9}$/)}
]

這裏的 fand, fmap, ffor 是對匿名函數進行 and ,map,or 的操做,這樣就能夠用匿名函數來表示規則的一部分,而後用邏輯運算把不一樣的規則方便地結合在一塊兒。

fand = fn f1, f2 ->
  fn x ->
    f1.(x) and f2.(x)
  end
end

ffor = fn f1, f2 ->
  fn x ->
    f1.(x) or f2.(x)
  end
end

fmap = fn f1, f2 ->
  fn x ->
    case f1.(x) do
      false -> false
      a -> f2.(a)
    end
  end
end

第五天

這道題一開始我規規矩矩地按照題目的描述去二分,計算座位 ID。而看到別人的解法以後,個人心裏是崩潰的。由於所謂的座位ID,其實就等於FFFBBBFRRR 按照 F -> 0, B -> 1, L -> 0, R -> 1 轉換爲二進制後的值。

第二部分能夠按照題目的描述去寫代碼,「找到一個位置,它不在列表裏,但它的前一位和後一位都在列表裏」.

for id <- 1..(heighest - 1) do
  if not MapSet.member?(set, id) do
    if Enum.all?([id - 1, id + 1], fn x ->
         MapSet.member?(set, x)
       end) do
      IO.inspect(id, label: "Day5 Part2")
    end
  end
end

第六天

問題是「在多個羣組裏面進行投票,統計符合規則的選項的數量」,第一部分的規則是:「全部被選擇的選項(不重複)」,第二部分的規則是:「全部人都選擇了的選項」. 能夠這樣描述這兩種規則:

uniq_in_group = fn g ->
  g
  |> List.flatten()
  |> Enum.into(MapSet.new())
  |> MapSet.size()
end

common = fn x, y ->
  Enum.filter(x, fn a -> a in y end)
end

common_in_group = fn [h | t] ->
  t
  |> Enum.reduce(h, common)
  |> length()
end

MapSet 是公認的「找不一樣」的利器。而「找相同」能夠用 reduce 依此 filter 出那些 「我有你也有」 的項。

第七天

我把這個問題稱爲「千層包」,很像是編譯器展開宏的流程。首先使用一個嵌套的 map 結構去存儲每一個 「bag」 的 「定義」, 而後使用另一個 map 去存儲展開後的結構。重複地去展開bag,即用 bag 的「定義」去替換 bag 自己,直到不能再展開爲止。

expand = fn rules, contains ->
  Enum.reduce_while(Stream.cycle([1]), contains, fn _, acc ->
    new_acc =
      Enum.reduce(acc, %{}, fn {bag, n}, acc1 ->
        case rules[bag] do
          m when map_size(m) == 0 ->
            %{bag => n}

          nil ->
            %{bag => n}

          contains1 ->
            for {bag1, n1} <- contains1, into: %{} do
              {bag1, n * n1}
            end
            |> Map.merge(%{"opened #{bag}" => n})
        end
        |> Map.merge(acc1, fn _k, v1, v2 -> v1 + v2 end)
      end)

    if new_acc == acc do
      {:halt, new_acc}
    else
      {:cont, new_acc}
    end
  end)
end

這裏有一個陷阱,被展開的 bag 自身也須要被記入總數。因此在展開後的 map 裏我加入了 "opened #{bag}" 這樣的 key,以方便計數。這裏的 Stream.cycle([1])實際上是爲了以簡單的方式結合 reduce_while 創造一個循環體。第一部分中,須要統計 「shiny gold」 ,只需把它的定義置空,讓其再也不繼續展開便可。

第八天

若是你曾經嘗試實現一個簡單的虛擬機,那麼這一題你必定會感受不陌生。本質上是根據「紙帶」傳來的命令去對 state 進行更新。在這一題裏,咱們能夠這樣設計state 包含的內容,分別是 當前的 index,當前的值,和執行過的命令的 indexes。提及這個 jmp 命令呀,就像這道題目裏面說的同樣,有可能引發無限循環。因此在有一些特殊的虛擬機中,是不存在 jmp 命令的,好比 Bitcoin Script 的虛擬機。

exe = fn
  {"nop", _}, i, a -> {i + 1, a}
  {"acc", v}, i, a -> {i + 1, a + v}
  {"jmp", v}, i, a -> {i + v, a}
end

run = fn d ->
  Enum.reduce_while(Stream.cycle([0]), {0, 0, MapSet.new()}, fn _, {i, a, history} ->
    cond do
      MapSet.member?(history, i) ->
        {:halt, a}

      i == map_size(d) ->
        {:halt, {:terminate, a}}

      true ->
        {i1, a1} = exe.(d[i], i, a)
        {:cont, {i1, a1, MapSet.put(history, i)}}
    end
  end)
end

第二部分,能夠像題目中描述的那樣,嘗試替換 nopjmp 命令,看看能不能運行出符合要求的結果。

第九天

第一部分,找出列表中不符合 「一個數字列表,除了開頭的 pre 個數,以後的數都知足:等於其以前的 pre 個數中的某兩個數的和」 的數。這裏的問題有一點點像第一天的問題,不過因爲 pre 的值很小(25),這裏使用 List 去遍歷就能夠了。

one_valid = fn ins, x ->
  Enum.any?(ins, fn y -> (x - y) in ins and 2 * y != x end)
end

valid = fn data, pre ->
  {pres, data} = Enum.split(data, pre)

  Enum.reduce_while(data, pres, fn x, acc ->
    if one_valid.(acc, x) do
      {:cont, tl(acc) ++ [x]}
    else
      {:halt, x}
    end
  end)
end

第二部分,問題是 「找出一段在列表裏連續的數,它們的和等於指定的數」。咱們能夠先假設符合條件的子數列的長度是 1,2,3...以此類推。在計算子數列中數字之和的時候,也能夠用到上一步的結果,例如一個從 index i 開始的,長度爲 n 的子數列,它的元素和等於從 i 開始的,長度爲 n-1 的子數列的元素和,加上 index i 處的值。這裏我用 {sum_of_value, start_index, end_index} 來表示一個子數列。一個細節:子數列的長度每增長n,子數列的數量就會減小一個,換句話說,上一步裏最後一個子數列不具有繼續擴張的能力。

sets = fn
  idata, last_sets ->
    last_sets
    |> Enum.drop(-1)
    |> Enum.map(fn {v, s, e} ->
      {v + idata[e + 1], s, e + 1}
    end)
end

init_sets =
  Enum.map(data |> Enum.with_index(), fn {v, i} ->
    {v, i, i}
  end)

第十天

第一個問題主要是 「找到間距爲3和間距爲1的相鄰數字組合」,首先把數列從小到大排序,而後計算出相鄰數字的間距。

distance = fn list ->
  Enum.reduce(list, {[0 | list], []}, fn x, {[h | t], r} -> {t, [x - h | r]} end)
  |> elem(1)
  |> Enum.reverse()
end

第二個問題,「從最低層到頂層有多少種路徑」, 首先在腦海中想象一下這些路徑的的圖,是一個樹狀的結構,須要統計它的葉子節點的數量。看到樹,就想到了抽象語法樹,從而想到宏的展開,進而想到了第七天的問題,直接把第七天的代碼複製過來稍做修改就能夠解決。這裏的細節是如何把被節點構形成 {node, children} 的結構,每一個 child 必須是在集合中,且知足比 node 的值小1到3 .

before = fn x -> (x - 3)..(x - 1) end

attach_before = fn x, set ->
  {
    x,
    before.(x)
    |> Enum.filter(fn y ->
      MapSet.member?(set, y) and y >= 0
    end)
    |> Enum.map(fn x -> {x, 1} end)
    |> Enum.into(%{})
  }
end
相關文章
相關標籤/搜索