From c3a114537a54dc6daa7508fac1ccd0cc1f7ca47c Mon Sep 17 00:00:00 2001 From: Gard Spreemann Date: Mon, 12 Dec 2022 11:41:13 +0100 Subject: Day 12 --- 12/Cargo.toml | 15 +++++ 12/input.txt | 41 +++++++++++++ 12/src/part-1.rs | 120 ++++++++++++++++++++++++++++++++++++++ 12/src/part-2.rs | 120 ++++++++++++++++++++++++++++++++++++++ 12/src/tensor.rs | 171 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 12/test.txt | 5 ++ 6 files changed, 472 insertions(+) create mode 100644 12/Cargo.toml create mode 100644 12/input.txt create mode 100644 12/src/part-1.rs create mode 100644 12/src/part-2.rs create mode 100644 12/src/tensor.rs create mode 100644 12/test.txt diff --git a/12/Cargo.toml b/12/Cargo.toml new file mode 100644 index 0000000..04aaa89 --- /dev/null +++ b/12/Cargo.toml @@ -0,0 +1,15 @@ +[package] +name = "day-12" +version = "0.1.0" +authors = ["Gard Spreemann "] +edition = "2021" + +[[bin]] +name = "part-1" +path = "src/part-1.rs" + +[[bin]] +name = "part-2" +path = "src/part-2.rs" + +[dependencies] diff --git a/12/input.txt b/12/input.txt new file mode 100644 index 0000000..c2ec0e6 --- /dev/null +++ b/12/input.txt @@ -0,0 +1,41 @@ +abccccccccccccccccccccaaaaaaaacccccccccccccaacaaaaacccccccccccccccccccccaaaaaacccccaaaaaccccccccccccccccccccaaaccccccccccccccccccccccccccccccccccccccccccccccccccccccccccaaaa +abcccccccccccccccccccaaaaaaaaacccccccccccccaaaaaaaaccccccccccccccaacccccaaaaaaaaaaaaaaaaccccccccccccccccccccaaaccccccccccccccccccccccccaccaccccccccccccccccccccccccccccaaaaaa +abccccccccccccccccccaaaaaaaaaacccccaacccccccaaaaaccccccccccccccaaaaaacccaaaaaaaaaaaaaaaaccccccccccccccccccaacaaaaacccccccccccccccccccccaaaaccccccccccccccccccccccccccccaaaaaa +abccccccccccaaacaaacaaacaaacccccacaaaccccccccaaaaacccccccccccccaaaaaacaaaaaaaaaaaaaaaaaaccccccccccccccccccaaaaaaaaccccccccccccccccccccaaaaaccccccccaaaccccaaaccccccccccaaacaa +abccccccccaaaaaccaaaaaccaaaccccaaaaaaaacccccaaacaaccccccccccccccaaaaccaaaaaaaaccccaaaaaacccccccccccccccccccaaaaaccccccccccccccccccccccaaaaaacccccccaaaacccaaaccccccccccccccaa +abccccccccaaaaaaccaaaaaaaaaaaccaaaaaaaaccccccaacccccccccccccccccaaaacaaaacaaaacccccaaacccccccaacaaccccccccccaaaaacccaaccccccccccccccccaaaaaaccccccccaaaaaaaacccccccccccccccaa +abccccccccaaaaaaaaaaaaaaaaaaacccaaaaaaccccccccccccccccccccccccccaccaccccccaaaaaccccccccccccccaaaaacccccccccaaacaacaaaaaaccccccccccccccccaacccccccckkkkkkaaaaccccccccccccccccc +abccccccccaaaaacaaaaaccaaaaaaaacaaaaaccccccccccccccccccccccccccccccccccccccaaaccccccccccccccccaaaaacccccccccaaccccaaaaaaccccccccccccccccccccccccckkkkkkklaaccccccccaacccccccc +abccccccccaaaaacaacaaacaaaaaaaacaaaaaacccccccccccccccccccccaacccccccccccccccaaaccccccccccccccaaaaaacccccccccccccccaaaaaacccccccccccccccccccccccckkkkkkkklllccccccccccaaaacccc +abcaaccccccccccccccaaaccaaaaaaaccccaaccccccccccccccccccaaccaacccccccccccccccccccaacccccccccccaaaacccccccccccccccccaaaaacccccccaaaacccccccccccccckkkoppppllllllccccccccaaccccc +abcaacccccccccccccccccccaaaaaccccccccccccccccccccccccccaaaaaacccccccccccccccccaaaaaacccccccccccaaccccccccccccccccccaaaacccccccaaaaacccccccccccckkkooppppplllllllllccccdaccccc +abaaaccccccaaacccccccccaaaaaaccccccccaaaccccccccccccccccaaaaaaacccccccccccccccaaaaaacccccccccccccccccccccccccccccccccccccccccaaaaaaccccccccccccjkoooopuppplllllllmmmddddacccc +abaaaaaccccaaaaacccccccccccaaaaacccccaaaaccccccccccccccccaaaaaaccccccccccccccccaaaacccccccccccccccccaaaccccccccccccccccccccccaaaaaacccccccccccjjjooouuuuppppppqqmmmmmdddacccc +abaaaaacccaaaaaacccaacaaacccaaaaaacccaaaacccccccccccccccaaaaaccccccccccaaccccccaaaaccccccaacccccacccaaccccccccccaacaaccccccccaaaaaacccaaaccccjjjjoouuuuuuppppqqqqqmmmdddacccc +abaaccacccaaaaaacccaaaaaacccaaaaaacccaaacccccccccccccccaaaaaaccccccccaaaaaaccccaccacccccaaaacccaaaaaaaccccccccccaaaaaccccccccccaacacccaaccccjjjjooouuuxuuupppqqqqqmmmdddccccc +abaaaccccccaaaaacccaaaaaacccaaaaaccccccccccccccccccccccccccaaccccccccaaaaaacccccccccccccaaaaccccaaaaaaaaccccccccaaaaaaccccccccccccaaaaaaacjjjjjoooouuxxxuuvvvvvvqqqmmdddccccc +abaaaccccccaacaacccaaaaaaacccaaaaacccccccccccccccaaacccccccccccccccccaaaaaccccaaacccccccaaaaccccaaaaaaaaacccccccaaaaaaccccccccccccaaaaaacjjjjjoooouuuxxxuuvvvvvvqqqmmdddccccc +abccccccccccccccccaaaaaaaacccaaaaacccccccccccccccaaaccccccccccccccccccaaaaacccaaacacccccccccccccaaaaaaaaacccccccaaaaaccccccaacccccaaaaaaajjjnoooottuuxxxxvyyyvvvqqmmmdddccccc +abccccccccccccccccaaaaaaaacccccccccccccccccaaaaaaaaaccaaccccccccccccccaaaaacaacaaaaaccccccccccccaaaaaaaaccccccccccaaacccccaaaaccccaaaaaajjjnnnntttttxxxxyyyyyvvvqqmmmdddccccc +abccccccaaaccccccccccaaacccaaccccccccccccccaaaaaaaaaaaaaaaccccccccaaacccccccaaaaaaaacccccccccccaaaaaaaaccccccccccccaacccccaaaaacacaaaaaaiiinnntttxxxxxxxyyyyyvvqqqmmdddcccccc +SbccccccaaaaaccccccccaaccccaaaccccccccccccccaaaaaaaaaaaaaacccccccaaaaaaccccccaaaaaccccccccacccaaaccaaacccccccccaaacaaccaaaaaaaaaaaaaaaaaiiinnntttxxxEzzzzyyyvvqqqmmmeeecccccc +abcccccaaaaaacccccccccccaaaaaaaaccccccccccccaaaaaaaaaaaaaccccccccaaaaaacccccccaaaaacccccccaacaaaccccaaacccccccccaaaaaccaaaaaaaaaacaccaaaiiinnntttxxxxxyyyyyvvvqqqnnneeecccccc +abaacccaaaaaacccccccccccaaaaaaaaccccaaaccccaaaaaaaaaaaaaaacccccccaaaaaccccccccaacaaaaaccccaaaaacccccccccccccccccaaaaaaaccaaaaaacccccccaaiiinnnttttxxxxyyyyyyvvvrrnnneeecccccc +abaaaaaaaaaaaccccccccccccaaaaaaccccaaaacccaaaaaaaaaaaaacaaccccccccaaaaacccccccaaccccaaaacccaaaaaacaacaaccccccccaaaaaaaaccaaaaaaccccccccciiiinnnttttxxwyywyyyyvvrrrnneeecccccc +abaaaaacaacaaccccccccccccaaaaaaccccaaaacccaaacaaaacaaaccccccccccccaacaaccccaacccccaaaaaacaaaaaaaacaaaaacccccaaaaaaaaaaaccaaaaaacccccccccciiiinnnttttwwyywwywwwvrrrnneeecccccc +abaaaacccccccccccccccccccaaaaaacccccaaacccccccaaacccacaaccccccccccccccccccaaccccccaaaaaccaaaaaaaaaaaaaccccccaaaaaaaaacccaaaaaaaacccccccccciiinnnnntswwywwwwwwwwrrrnnneecccccc +abaaaaaccccccccccccccccccaacaaacccccccccccccccaacaaacaaacccccccccccccccaaaaacaaccccaaaaaccccaacccaaaaaacccccaaacaaaaaccccacccccccaaacccccciiiiinnmsswwwwwswwwwrrrrnnneecccccc +abaaaaaaccaaaccccccccccccccccccccccccccccccccccccaaaaaaaccccccccaaaccccaaaaaaaaccccaaccaccccaacccccaaaaccaaaaaaaaaaccccccccccccccaaaaacccccciihmmmsswwwwssrrrrrrrrnnneecccccc +abaaccaacaaaaccccccccccccccccccccccccccccccccccccaaaaaacccccccccaaaaaccccaaaaacccccccccccccccccccccacccccaaaaaaaaacccccccaaccaacaaaaacccccccchhhmmssswwsssrrrrrrrnnneeecccccc +abaacccccaaaacccccccccccccccccccccccccccccccccccccaaaaaaaacccccaaaaaacccaaaaacccccccccccccccccccccccccccccaaaaaaaccccccccaaaaaacaaaaacccccccchhhmmssssssslllllllnnnnfeecccccc +abccccccccaaacccccccccccccccaaccccccccccccccaaaccaaaaaaaaacccccaaaaaacccaacaaaccccccccccccccccccccccccaacccaaaaaaccccccccaaaaacccaaaaaccccccchhhmmmssssslllllllllnnfffeaacccc +abcccccccccccccccccccccccacaaaccccccccccccccaaaaaaaaaaaaaaccaaccaaaaaccccccaaccaacccccccccccccccccccccaaaccaaaaaaacccccccaaaaaaccaaccccccccccchhhmmmmsmllllllllllfffffaaacccc +abccccccccccccccccccccccaaaaaaaaccccccccccccaaaaaaacaaacaaaaaaccaaaaccccccccccaaaacccccccccccccccccaaaaaaaaaaacaaaccccccaaaaaaaacccccccccccccchhhmmmmmmlllggfffffffffaacccccc +abccccccccccccccccccccccaaaaaaaaccccccccccccaaacccccaaacaaaaacccccccccccaaacccaaaacccccccccccccccccaaaaaaaaaacccccccccccaaaaaaaaccccccccccccccchhhmmmmmlggggffffffffaaacccccc +abccccccccccccccccaaaacccaaaaaacccccccccccccccccccccaaacaaaaaaccccccccccaaacccaaaaccccccacccccccccccaaaaaacccccccccccccccccaacccaaccccccccccccchhhhgmgggggggffaccccccaacccccc +abccccccccccccccccaaaacccaaaaacccccccccccccccccccccccccaaaaaaaacccccccccaaaaccaaacccccccaaacaaacccccaaaaaacccccccccccccccccaaccaaccccccccccccccchhhgggggggaaaaacccccccccccccc +abccccccccccccccccaaaacccaaaaaaccccccccccccccccccccccccaaaaaaaaccccccccccaaaaaaaaaccccccaaaaaaacccccaaaaaaccccccccccccccccccaaaaacaaccccccccccccchggggggaacccccccccccccccccca +abcccccccccccccccccaacccccccaaccccccccccccccccccccccccccccaacaccaaacccaacaaaaaaaacccccccaaaaaaccccccaaccaacaaaccacccccaaccccaaaaaaaaccccccccccccccccccaaaccccccccccccccccccca +abcccccaaccccccccccccccccaaccccccccaacccccccccaaacccccccccaacaaaaaacccaaacaaaaaacccccccaaaaaaacccccccccccccaaaaaacccaaaaccccccaaaaaccccaaacccccccccccccaaccccccccccccccaaaaaa +abccccaaaacccccccccccccccaaaacccaaaaccccccccccaaaacccccccccccaaaaaaccaaaaaaaaaacccccccaaaaaaaaaaccccccccccccaaaaacccaaaaaacccaaaaacccccaaaacccccccccccaaccccccccccccccccaaaaa +abccccaaaacccccccccccccaaaaaacccaaaaaaccccccccaaaaccccccccccaaaaaaaacaaaaaaaaaaaaaccccaaaaaaaaaaccccccccccaaaaaaaacccaaaaccccaacaaaccccaaaacccccccccccccccccccccccccccccaaaaa diff --git a/12/src/part-1.rs b/12/src/part-1.rs new file mode 100644 index 0000000..2e70ef9 --- /dev/null +++ b/12/src/part-1.rs @@ -0,0 +1,120 @@ +mod tensor; + +use std::collections::{BinaryHeap}; +use std::io::{BufRead}; + +use tensor::{Index, Shape, Tensor}; + +#[derive(Debug)] +struct Node { + idx: [usize; 2], + cost: usize +} + +impl Node { + fn new(idx: [usize; 2], cost: usize) -> Self { + Self { + idx: idx, + cost: cost + } + } +} + +impl PartialEq for Node { + fn ne(self: & Self, other: & Self) -> bool { + self.cost != other.cost + } + fn eq(self: & Self, other: & Self) -> bool { + self.cost == other.cost + } +} + +impl Eq for Node { +} + +impl PartialOrd for Node { + fn partial_cmp(self: & Self, other: & Self) -> Option { + other.cost.partial_cmp(& self.cost) + } +} + +impl Ord for Node { + fn cmp(self: & Self, other: & Self) -> std::cmp::Ordering { + other.cost.cmp(& self.cost) + } +} + +fn main() { + let stdin = std::io::stdin(); + let mut handle = stdin.lock(); + + let mut buf: Vec = Vec::new(); + + let mut tmp: Vec<(u8, bool)> = Vec::new(); + let mut m: usize = 0; + let mut n: usize = 0; + let mut n_check: usize = 0; + + loop { + let buf = handle.fill_buf().expect("IO error"); + let bytes_read = buf.len(); + if bytes_read == 0 { break; } + + for & b in buf.into_iter() { + if b == b'\n' { + m += 1; + assert_eq!(n_check, n); + n_check = 0; + } + else { + tmp.push((b, false)); + if m == 0 { + n += 1; + } + n_check += 1; + } + } + + handle.consume(bytes_read); + } + let mut terrain: Tensor<(u8, bool), 2> = Tensor::new_from([m, n], tmp); + + let mut unvisited: BinaryHeap = BinaryHeap::new(); + let mut done: bool = false; + let mut res: usize = usize::MAX; + + for i in 0..m { + for j in 0..n { + if terrain[[i, j]].0 == b'S' { + unvisited.push(Node::new([i, j], 0)); + } + } + } + + while let Some(node) = unvisited.pop() { + let idx = node.idx; + if !terrain[idx].1 { + let elev = terrain[idx].0; + let neighbor_idxs = [[idx[0].wrapping_sub(1), idx[1] ], + [idx[0] + 1 , idx[1] ], + [idx[0] , idx[1].wrapping_sub(1)], + [idx[0] , idx[1] + 1 ]]; + for neighbor_idx in neighbor_idxs.into_iter() { + if let Some(& (x, v)) = terrain.el(& neighbor_idx) { + if x == b'E' && (elev == b'y' || elev == b'z') { // The end node has elevation z. + res = node.cost + 1; + done = true; + break; + } + if !v && (x < elev || x - elev <= 1 || elev == b'S') { + unvisited.push(Node::new(neighbor_idx.clone(), node.cost + 1)); + } + } + } + terrain[idx].1 = true; + } + if done { break; } + } + + println!("{}", res); +} diff --git a/12/src/part-2.rs b/12/src/part-2.rs new file mode 100644 index 0000000..aa14ef8 --- /dev/null +++ b/12/src/part-2.rs @@ -0,0 +1,120 @@ +mod tensor; + +use std::collections::{BinaryHeap}; +use std::io::{BufRead}; + +use tensor::{Index, Shape, Tensor}; + +#[derive(Debug)] +struct Node { + idx: [usize; 2], + cost: usize +} + +impl Node { + fn new(idx: [usize; 2], cost: usize) -> Self { + Self { + idx: idx, + cost: cost + } + } +} + +impl PartialEq for Node { + fn ne(self: & Self, other: & Self) -> bool { + self.cost != other.cost + } + fn eq(self: & Self, other: & Self) -> bool { + self.cost == other.cost + } +} + +impl Eq for Node { +} + +impl PartialOrd for Node { + fn partial_cmp(self: & Self, other: & Self) -> Option { + other.cost.partial_cmp(& self.cost) + } +} + +impl Ord for Node { + fn cmp(self: & Self, other: & Self) -> std::cmp::Ordering { + other.cost.cmp(& self.cost) + } +} + +fn main() { + let stdin = std::io::stdin(); + let mut handle = stdin.lock(); + + let mut buf: Vec = Vec::new(); + + let mut tmp: Vec<(u8, bool)> = Vec::new(); + let mut m: usize = 0; + let mut n: usize = 0; + let mut n_check: usize = 0; + + loop { + let buf = handle.fill_buf().expect("IO error"); + let bytes_read = buf.len(); + if bytes_read == 0 { break; } + + for & b in buf.into_iter() { + if b == b'\n' { + m += 1; + assert_eq!(n_check, n); + n_check = 0; + } + else { + tmp.push((b, false)); + if m == 0 { + n += 1; + } + n_check += 1; + } + } + + handle.consume(bytes_read); + } + let mut terrain: Tensor<(u8, bool), 2> = Tensor::new_from([m, n], tmp); + + let mut unvisited: BinaryHeap = BinaryHeap::new(); + let mut done: bool = false; + let mut res: usize = usize::MAX; + + for i in 0..m { + for j in 0..n { + if terrain[[i, j]].0 == b'E' { + unvisited.push(Node::new([i, j], 0)); + } + } + } + + while let Some(node) = unvisited.pop() { + let idx = node.idx; + if !terrain[idx].1 { + let elev = if terrain[idx].0 == b'E' { b'z' } else { terrain[idx].0 }; + let neighbor_idxs = [[idx[0].wrapping_sub(1), idx[1] ], + [idx[0] + 1 , idx[1] ], + [idx[0] , idx[1].wrapping_sub(1)], + [idx[0] , idx[1] + 1 ]]; + for neighbor_idx in neighbor_idxs.into_iter() { + if let Some(& (x, v)) = terrain.el(& neighbor_idx) { + if (x == b'S' || x == b'a') && (elev == b'a' || elev == b'b') { + res = node.cost + 1; + done = true; + break; + } + if !v && (x > elev || elev - x <= 1) { + unvisited.push(Node::new(neighbor_idx.clone(), node.cost + 1)); + } + } + } + terrain[idx].1 = true; + } + if done { break; } + } + + println!("{}", res); +} diff --git a/12/src/tensor.rs b/12/src/tensor.rs new file mode 100644 index 0000000..2635886 --- /dev/null +++ b/12/src/tensor.rs @@ -0,0 +1,171 @@ +#![allow(dead_code)] + +pub type Shape = [usize; D]; +pub type Index = Shape; + +pub struct Tensor { + data: Vec, + shape: Shape, + strides: Shape +} + +pub type Tensor1 = Tensor; +pub type Tensor2 = Tensor; +pub type Tensor3 = Tensor; +pub type Tensor4 = Tensor; +pub type Index1 = Index<1>; +pub type Index2 = Index<2>; +pub type Index3 = Index<3>; +pub type Index4 = Index<4>; + + +impl Tensor { + pub fn new(shape: Shape, x: T) -> Self { + if D == 0 { panic!("Empty shape not allowed."); } + + let mut len = shape[D-1]; + let mut strides: Shape = [0; D]; + strides[D-1] = 1; + for d in (1..D).rev() { // d=D-1, …, 1. + strides[d-1] = shape[d]*strides[d]; + len *= shape[d-1]; + } + if len == 0 { panic!("Empty dimensions not allowed."); } + + Self { + data: vec![x; len], + shape: shape, + strides: strides, + } + } + + pub fn new_from(shape: Shape, x: Vec) -> Self { + if D == 0 { panic!("Empty shape not allowed."); } + + let mut len = shape[D-1]; + let mut strides: Shape = [0; D]; + strides[D-1] = 1; + for d in (1..D).rev() { // d=D-1, …, 1. + strides[d-1] = shape[d]*strides[d]; + len *= shape[d-1]; + } + if len == 0 { panic!("Empty dimensions not allowed."); } + + if len != x.len() { panic!("Vector of length {} cannot fill tensor with {} entries.", x.len(), len); } + + Self { + data: x, + shape: shape, + strides: strides, + } + } + + + #[inline(always)] + fn flatten_idx(self: & Self, idx: & Index) -> usize { + // NOTE: This is a very hot code path. Should benchmark versus explicit loop. + idx.iter().zip(self.strides.iter()).fold(0, |sum, (i, s)| sum + i*s) + } + + fn bound_check_panic(self: & Self, idx: & Index) -> () { + for d in 0..D { + let i = *(unsafe { idx.get_unchecked(d) }); + if i >= self.shape[d] { + panic!("{}-dimensional tensor index is out of bounds in dimension {} ({} >= {}).", D, d, i, self.shape[d]) + } + } + } + + pub fn in_bounds(self: & Self, idx: & Index) -> bool { + for d in 0..D { + let i = *(unsafe { idx.get_unchecked(d) }); + if i >= self.shape[d] { + return false; + } + } + true + } + + pub fn shape(self: & Self) -> & Shape { &self.shape } + + pub fn el(self: & Self, idx: & Index) -> Option<& T> { + if self.in_bounds(idx) { Some(unsafe { self.el_unchecked(idx) }) } + else { None } + } + + pub unsafe fn el_unchecked(self: & Self, idx: & Index) -> & T { self.data.get_unchecked(self.flatten_idx(idx)) } + + pub unsafe fn el_unchecked_mut(self: &mut Self, idx: & Index) -> &mut T { + let flat_idx = self.flatten_idx(idx); + self.data.get_unchecked_mut(flat_idx) + } + + pub fn flat_len(self: & Self) -> usize { self.data.len() } + pub fn size(self: & Self) -> usize { self.flat_len()*std::mem::size_of::() } + + pub fn fill_with(self: &mut Self, x: & [T]) -> () { + // Already panics on size mismatch. + self.data.copy_from_slice(x) + } + + pub fn fill(self: &mut Self, x: T) -> () { + self.data.fill(x); + } + + pub fn data(self: & Self) -> & [T] { & self.data } +} + +impl Tensor { + pub fn dirty_print(self: & Self) { + for i in 0..self.shape[0] { + for j in 0..self.shape[1] { + print!("{} ", self[[i, j]]); + } + println!(""); + } + } +} + +impl std::ops::Index> for Tensor { + type Output = T; + + fn index(self: & Self, idx: Index) -> & Self::Output { + self.bound_check_panic(&idx); + unsafe { self.el_unchecked(&idx) } + } +} + +impl std::ops::Index<& Index> for Tensor { + type Output = T; + + fn index(self: & Self, idx: & Index) -> & Self::Output { + self.bound_check_panic(idx); + unsafe { self.el_unchecked(idx) } + } +} + +impl std::ops::IndexMut> for Tensor { + fn index_mut(self: &mut Self, idx: Index) -> &mut Self::Output { + self.bound_check_panic(&idx); + unsafe { self.el_unchecked_mut(&idx) } + } +} + +impl std::ops::IndexMut<& Index> for Tensor { + fn index_mut(self: &mut Self, idx: & Index) -> &mut Self::Output { + self.bound_check_panic(idx); + unsafe { self.el_unchecked_mut(idx) } + } +} + +impl IntoIterator for Tensor { + type Item = T; + type IntoIter = std::vec::IntoIter; + + fn into_iter(self) -> Self::IntoIter { self.data.into_iter() } +} + + +// FIXME: Should have a proper IntoIter implementing IntoIter for &'a Tensor. + +// Note: Tensor is also sliceable (due to the Index implementations) diff --git a/12/test.txt b/12/test.txt new file mode 100644 index 0000000..86e9cac --- /dev/null +++ b/12/test.txt @@ -0,0 +1,5 @@ +Sabqponm +abcryxxl +accszExk +acctuvwj +abdefghi -- cgit v1.2.3