ABC372

当日参加ではないし今更ですが、自力で結構解けたので自分が解けた問題の解法を乗せておきます。解説というよりかはアウトプット位のイメージです。
解けた問題はABCDEの五問、使用言語はRustです。

各セクションはタイトルのAとかEとか押したら問題のページに、提出を押すと私のAC提出に飛んでいくようにリンクを張っているつもりです。掲載されているコードはロジックに関係ある部分を抜粋したものです。

目次

  1. A
  2. B
  3. C
  4. D
  5. E

A

提出

fn main() {
    input! {
        s: Chars,
    }
    for c in s {
        if c != '.' {
            print!("{}", c);
        }
    }
    println!();
}
    

解き方

'.'以外の時だけprintする

B

提出

fn main() {
    input! {
        mut m: usize,
    }
    let mut ans = vec![];
    let mut i = 10;
    while m != 0 {
        if 3usize.pow(i) <= m {
            ans.push(i);
            m -= 3usize.pow(i);
        } else {
            i -= 1;
        }
    }
    println!("{}", ans.len());
    println!("{}", ans.iter().join(" "));
}
    

解き方

3^10は引けるか, 3^9は引けるかというように, 貪欲法的にできるだけ大きい3のべき乗数を引いていきます。

C

提出

fn main() {
    input! {
        n: usize,
        q: usize,
        mut s: Chars,
        qs: [(usize, char); q],
    }
    let mut ans = 0;
    for i in 2..n {
        if s[i - 2] == 'A' && s[i - 1] == 'B' && s[i] == 'C' {
            ans += 1;
        } 
    }
    for (p, c) in qs {
        let p = p - 1;
        if 2 <= p && is_ABC(&s, p - 2) {
            ans -= 1;
        } else if 1 <= p && is_ABC(&s, p - 1) {
            ans -= 1;
        } else if is_ABC(&s, p) {
            ans -= 1;
        }
        s[p] = c;
        if 2 <= p && is_ABC(&s, p - 2) {
            ans += 1;
        } else if 1 <= p && is_ABC(&s, p - 1) {
            ans += 1;
        } else if is_ABC(&s, p) {
            ans += 1;
        }
        println!("{}", ans);
    }
}
    

解き方

is_ABC(&s, index)というのはs[p..p+2]が"ABC"になっているかをbool型で返す関数です. まずABCの連続部分文字列が元の文字列にいくつあるか数え, クエリを処理する際はもともとABCがある位置の文字を変えていたらansからその一つを引き, そのあとABCが形成されていたらansにその一つを加えます。

D

提出

fn main() {
        input! {
            n: usize,
            hs: [usize; n],
        }
        let mut highers = vec![n; n];
        // highers[i]: iより左にありかつiより大きいもののうち最も右のindex, nならばhs[..i] < hs[i]
        for i in 1..n {
            let mut j = i - 1;
            while j > 0 && hs[j] < hs[i] && highers[j] != n {
                j = highers[j];
            }
            if hs[j] > hs[i] {
                highers[i] = j;
            }
        }
        lprintln!("{:?}", highers);
        let mut ans = vec![0; n + 1];
        for i in 0..n {
            ans[highers[i]] += 1;
        }
        lprintln!("{:?}", ans);
        let mut sum = ans[n];
        for i in 0..n {
            ans[highers[i]] -= 1;
            sum += ans[i];
            if highers[i] == n || highers[i] < i {
                sum -= 1;
            }
            lprint!("{:?} ", ans);
            println!("{}", sum);
        }
}
    

解き方

計算量はO(n)くらいですかね. ビル i とビル j の間にビル j より高いビルが存在しないという条件は, iとjが近い値であればあるほど満たしやすくなることはわかるかと思います. 各jについて, この条件が満たされる最小のiを求めます(コード内の配列highers). 前からビルiを走査していったときに, このiについて, 何個のビルjが条件を満たしているのかここから求めることができます.

E

提出

#[derive(Debug, Clone)]
        struct OriginalGraph {
            see_theres: Vec,
            largest_10: Vec<[usize; 10]>,
        }
        
        impl OriginalGraph {
            fn new(n: usize) -> Self {
                let see_theres = (0..=n).collect::<Vec>();
                let mut largest_10 = vec![[0; 10]; n + 1];
                for i in 0..=n {
                    largest_10[i][0] = i;
                }
                Self {
                    see_theres,
                    largest_10
                }
            }
        
            fn kth_largest(&mut self, i: usize, k: usize) -> usize {
                let i = self.see_where(i);
                self.largest_10[i][k - 1]
            }
        
            fn see_where(&mut self, i: usize) -> usize {
                if self.see_theres[i] == i {
                    i
                } else {
                    self.see_theres[i] = self.see_where(self.see_theres[i]);
                    self.see_theres[i]
                }
            }
        
            fn add_edge(&mut self, n: usize, m: usize) {
                let n = self.see_where(n);
                let m = self.see_where(m);
                let (n, m) = if n < m { (n, m) } else { (m, n) };
                self.see_theres[n] = m;
                if n == m {
                    return;
                }
                let mut it = self.largest_10[n].iter().chain(self.largest_10[m].iter()).map(|i| *i).sorted().rev().take(10);
                for i in 0..10 {
                    self.largest_10[m][i] = it.next().unwrap();
                }
            }
        }
        
        
        #[fastout]
        fn main() {
            input! {
                n: usize,
                q: usize,
                qs: [(usize, usize, usize); q],
            }
            let mut graph = OriginalGraph::new(n);
            for (t, a, b) in qs {
                match t {
                    1 => {
                        graph.add_edge(a, b);
                        lprintln!("{a} {b}");
                        lprintln!("{:?}", graph);
                    }
                    2 => {
                        lprintln!("{a} {b} {}", graph.see_where(a));
                        let ans = graph.kth_largest(a, b);
                        if ans == 0 {
                            println!("-1");
                        } else {
                            println!("{}", ans);
                        }
                    }
                    _ => unreachable!(),
                }
            }
        }
    

解き方

解説に書いてあるUnionFindに10個の最大値を持たせるっていうのと変わらないです. 最大値をマージするときのself.largest_10[n].iter().chain(self.largest_10[m].iter()).map(|i| *i).sorted().rev().take(10) のメソッドチェーンが結構お気に入り. これのためにRust使ってると言っても過言ではない. self.largest_10[n].iter().chain(self.largest_10[m].iter()).map(|i| *i).sorted().rev().take(10).enumerate().for_each(...)にすればforループでindexを回すのを避けられましたかね

感想

最初はC問題の部分文字列を非連続なものだと思ったり, E問題の連結を直接つながっているものと考えたりしていました. 自分の中の言葉の定義と違うものが多い回でした.

2-3-4木のRustによる実装

赤黒木を実装しようと思いましたが, 操作の意味などが分からなくて困っていたので, その元となると言われている2-3-4木の実装をRustで行ってみました。

複雑なデータ構造で, 一応テストはしたつもりですが何かバグがあるかもしれません。ある程度雰囲気をつかむくらいの感覚でお読みいただければ幸いです。

実装したファイルをgithubにアップロードしておきましたので興味があれば見ていただけましたら

目次

    2-3-4木とは何か

    この記事では, 2-3-4木は以下の定義を満たすデータ構造だと解釈しています

    1. 2-3-4木は木である
    2. 各ノードは1個以上3個以下のデータを持つ
    3. 各ノードは子を持たないか, 子を持つ場合はその個数はノードの持つデータの個数に1加えたものとなる
    4. 2-3-4木の根から任意の葉までの距離はすべて等しい

    2-3-4木は平衡探索木の一種で, 各ノードについて, その保持するデータと子の間には順序関係が存在します.

    具体的には, ノードの保持するデータはソートされており, どのデータも左右に二つの部分木が存在しかつ, データは左の部分木の最大値よりも大きく, 右の部分木よりも小さい状態になっています.

    例えば, 3つのデータを保持するノードの保持するデータと部分木は下図の様な大小関係で表されます.

    最小値
    最小値
    最大値
    最大値
    最も左の部分木
    最も左の部分木
    最小値
    最小値
    最大値
    最大値
    二番目の部分木
    二番目の部分木
    最小値
    最小値
    最大値
    最大値
    三番目の部分木
    三番目の部分木
    最小値
    最小値
    最大値
    最大値
    四番目の部分木
    四番目の部分木
    データ1
    データ1
    データ2
    データ2
    データ3
    データ3
    <
    <
    <
    <
    <
    <
    <
    <
    <
    <
    <
    <
    Text is not SVG - cannot display

    これによって, 2-3-4木ではある程度効率よく, データが木の中に存在するかなどを探索することができるようになります.

    ヘルパー関数

    具体的なロジックとは関係ないですが, unsafeを使った実装しか思いつかなかった部分がいくつかあったので, それらをヘルパー関数として定義しました.

    配列への挿入

    fn insert_to_array<S>(array: &mut [S], index: usize, value: S) {
        #[cold]
        #[cfg_attr(not(feature = "panic_immediate_abort"), inline(never))]
        #[track_caller]
        fn assert_failed(index: usize, len: usize) -> ! {
            panic!("insertion index (is {index}) should be <= len (is {len})");
        }
        let len = 3;
    
        if index > len {
            assert_failed(index, len);
        }
    
        unsafe {
            // infallible
            // The spot to put the new value
            let p = array.as_mut_ptr().add(index);
            // Shift everything over to make space. (Duplicating the
            // `index`th element into two consecutive places.)
            ptr::copy(p, p.add(1), len - index);
            // Write it in, overwriting the first copy of the `index`th
            // element.
            ptr::write(p, value);
        }
    }
        

    配列の指定した位置に値を挿入し, そこにあった値と以降の値を一つ右にずらします. lenは3で固定されており, 今回の実装のみにしか使えません.

    配列からの削除

    fn delete_from_array<S>(array: &mut [S], index: usize) -> S {
        #[cold]
        #[cfg_attr(not(feature = "panic_immediate_abort"), inline(never))]
        #[track_caller]
        fn assert_failed(index: usize, len: usize) -> ! {
            panic!("insertion index (is {index}) should be <= len (is {len})");
        }
        let len = 3;
    
        if index > len {
            assert_failed(index, len);
        }
    
        unsafe {
            // infallible
            // The spot to put the new value
            let p = array.as_mut_ptr().add(index);
            let ret = ptr::read(p);
            // Shift everything over to make space. (Duplicating the
            // `index`th element into two consecutive places.)
            ptr::copy(p.add(1), p, len - index);
            return ret;
        }
    }
        

    配列の指定した位置の値を削除し, それ以降の値を一つ左にずらします. 同じく今回の実装にしか使えません.

    どちらもstd::Vecの同様の関数の実装を参考にしています.

    メンバ変数の設計など

    2-3-4木のノードは以下のような構造体で表されます.

    pub struct Tree234<T> {
        data: [Option<T>; 4],
        children: [Option<Box<Tree234<T>>>; 4],
        size: usize,
    }
        

    dataはノードの保持するデータをOption型で保持しており, これがNoneの場合はデータが存在しないことを指します. 4つもデータ領域必要ないはずですが, 書き直すのがめんどくさいので4になっています.
    childrenはノードの子をOption型で保持しており, これがNoneの場合は子が存在しないことを指します.
    sizeはノードの保持するデータの個数を指します. この値は0, または1以上3以下の値を取ります. 0の場合は木が空であることを示しており, これは根ノードのみに許されるべきです.
    data, childrenはどちらもソートされた状態を保持します.

    また, 可読性のためにいくつか関数を用意しておきました.

    impl<T: Ord> Tree234<T> {
        /**
         * valueを含んでいるかもしれない子のインデックスを返す. もしself.dataにvalueが含まれている場合はそこにおけるインデックスを返す.
         */
        fn find_index(&self, value: &T) -> usize {
            for i in 0..self.size {
                if value <= self.data[i].as_ref().unwrap() {
                    return i;
                }
            }
            self.size
        }
        /**
         * selfが葉であるかどうかを返す.
         */
        fn is_leaf(&self) -> bool {
            self.children[0].is_none()
        }
    }
    

    挿入

    挿入の実装は以下のようになります. 解説のため, 本来のコードからは少し変えてあります.

    pub fn insert(&mut self, value: T) {
        let mut pos = self.find_index(&value);
        if self.size != 3 && self.is_leaf() {
            insert_to_array(&mut self.data, pos, Some(value));
            self.size += 1;
            return;
        }
    
        if self.size == 3 {
            // 自分がrootかつsizeが3の場合のみ if以下はすべてinsertが再帰的に呼び出される前に分割されるのでここには入らない.
            let mid = self.data[1].take();
            let left = Box::new(Self {
                data: [self.data[0].take(), None, None, None],
                children: [self.children[0].take(), self.children[1].take(), None, None],
                size: 1,
            });
            let right = Box::new(Self {
                data: [self.data[2].take(), None, None, None],
                children: [self.children[2].take(), self.children[3].take(), None, None],
                size: 1,
            });
            self.data = [mid, None, None, None];
            self.children = [Some(left), Some(right), None, None];
            self.size = 1;
        }
    
        let mut pos = self.find_index(&value);
        if self.children[pos].as_ref().unwrap().size == 3 {
            let mut child = delete_from_array(&mut self.children, pos).unwrap();
            let mid = child.data[1].take();
            let left = Box::new(Self {
                data: [child.data[0].take(), None, None, None],
                children: [
                    child.children[0].take(),
                    child.children[1].take(),
                    None,
                    None,
                ],
                size: 1,
            });
            let right = Box::new(Self {
                data: [child.data[2].take(), None, None, None],
                children: [
                    child.children[2].take(),
                    child.children[3].take(),
                    None,
                    None,
                ],
                size: 1,
            });
            let pos_ = self.find_index(mid.as_ref().unwrap());
            insert_to_array(&mut self.data, pos_, mid);
            insert_to_array(&mut self.children, pos_, Some(right));
            insert_to_array(&mut self.children, pos_, Some(left));
            self.size += 1;
            pos = self.find_index(&value);
        }
        self.children[pos].as_mut().unwrap().insert(value);
    }
        

    コードは再帰的な処理になっており, 大まかには再帰の終端処理->自分のノードが3つのデータをすでに保持している場合の処理->挿入されるべき子が3つのデータを保持している場合の処理->挿入されるべき子がすでに3つのデータを保持している場合の処理になります.

    まずは終端処理から

    let mut pos = self.find_index(&value);
    if self.size != 3 && self.is_leaf() {
        insert_to_array(&mut self.data, pos, Some(value));
        self.size += 1;
        return;
    }
        

    selfが葉ノードかつ新しいデータを入れる余地がある場合, 適切な位置にvalueを挿入して処理が終了となります.

    次に, selfに新しいデータを入れる余地がない場合, どうなるか見ていきます.

    2
    2
    1
    1
    3
    3
    4
    4
    5
    5
    Text is not SVG - cannot display

    上の図の木に6を挿入する場合を考えます
    2よりも6のほうが大きいので, 右のノードに6が挿入されることになりますが, 右のノードはすでに3つのデータを保持しているため, このノードを分割する必要があります.
    分割の手順は, まず中央値を親ノードに渡し, 左の値, 右の値をそれぞれ親ノードの子ノードとして追加します. これによって, 親ノードは保持するデータと子ノードが一つずつ増え, 2-3-4木の条件を満たします.
    また, 親ノードもまた3つのデータを保持している可能性もあり, その場合繰り上げのような処理が必要となるでしょう. 再帰を簡単にするために, 今回は道中にある3つのデータを保持するノードをすべて分割してから挿入することになっています.

    これに対応するコードは以下になります.

    if self.children[pos].as_ref().unwrap().size == 3 {
        let mut child = delete_from_array(&mut self.children, pos).unwrap();
        let mid = child.data[1].take(); // 中央値
        let left = Box::new(Self { // 左の値を保持するノード
            data: [child.data[0].take(), None, None, None],
            children: [
                child.children[0].take(), // もともと保持していた子ノードの左側にある二つはこれに持たせておく.
                child.children[1].take(),
                None,
                None,
            ],
            size: 1,
        });
        let right = Box::new(Self {
            data: [child.data[2].take(), None, None, None], // 右の値を保持するノード
            children: [
                child.children[2].take(),
                child.children[3].take(),
                None,
                None,
            ],
            size: 1,
        });
        let pos_ = self.find_index(mid.as_ref().unwrap());
        insert_to_array(&mut self.data, pos_, mid);
        insert_to_array(&mut self.children, pos_, Some(right)); // 同じインデックスに挿入する場合, 先に挿入したもののほうが後になる.
        insert_to_array(&mut self.children, pos_, Some(left));
        self.size += 1;
        pos = self.find_index(&value); // posを再計算
    }
    self.children[pos].as_mut().unwrap().insert(value); // 次の子ノードに再帰処理を託す
        

    ここで, 次の再帰ではself.children[pos]が親ノードとなりますが, これは高々2つのデータしか保持されないことがわかるかと思います.

    しかし, この方法では根ノードが3つの要素を持つ場合には対応できません. そのため, この場合は根ノードを分割する必要があります.

    根ノードの分割では, 親ノードが存在しないので, 新しい中間値をデータとする根ノードをつくり, もともと左, 右にあった値をその子とします.

            if self.size == 3 {
                // 自分がrootかつsizeが3の場合のみ if以下はすべてinsertが呼び出される前に分割されるのでここには入らない.
                let mid = self.data[1].take();
                let left = Box::new(Self {
                    data: [self.data[0].take(), None, None, None],
                    children: [self.children[0].take(), self.children[1].take(), None, None],
                    size: 1,
                });
                let right = Box::new(Self {
                    data: [self.data[2].take(), None, None, None],
                    children: [self.children[2].take(), self.children[3].take(), None, None],
                    size: 1,
                });
                self.data = [mid, None, None, None];
                self.children = [Some(left), Some(right), None, None];
                self.size = 1;
            }
        

    先にも述べたように, selfが何かの子ノードであった場合, そのsizeは高々2になるはずなので, この処理は根ノードにしか適用されないことになります.

    これによって, 挿入処理が行えるようになりました.

    削除

    削除の実装は以下のようになります.

            pub fn delete(&mut self, value: &T) -> bool {
                if self.is_leaf() {
                    let pos = self.find_index(value);
                    if pos < self.size && self.data[pos].as_ref().unwrap() == value {
                        delete_from_array(&mut self.data, pos);
                        self.size -= 1;
                        return true;
                    }
                    return false;
                }
                let pos = self.find_index(value);
                let is_internal = pos < self.size && self.data[pos].as_ref().unwrap() == value;
                if !is_internal && self.children[pos].as_ref().unwrap().size > 1 {
                    // いつかはここに引っかかるはず
                    // 子ノードの大きさが2以上の場合, それを起点に再帰的に削除を行う.
                    return self.children[pos].as_mut().unwrap().delete(value);
                }
                if !is_internal {
                    self.delete_balance(pos);
                    return self.delete(value);
                }
        
                self.delete_balance(pos);
                // 内部ノードである場合, 通り道全体にrotateとかしてから左側最大と交換して削除する
                let mut current = self.children[pos].as_mut().unwrap();
                while !current.is_leaf() {
                    current.delete_balance(current.size);
                    current = current.children[current.size].as_mut().unwrap();
                }
                let pos = self.find_index(value);
                let is_internal = pos < self.size && self.data[pos].as_ref().unwrap() == value;
                if is_internal {
                    let mut current = self.children[pos].as_mut().unwrap();
                    while !current.is_leaf() {
                        current.delete_balance(current.size);
                        current = current.children[current.size].as_mut().unwrap();
                    }
                    std::mem::swap(&mut self.data[pos], &mut current.data[current.size - 1]);
                    return current.delete(value);
                }
                self.delete(value)
            }
    
            fn delete_balance(&mut self, pos: usize) {
                // 隣接兄弟ノードの大きさが2以上の場合, 回転を行う
                if (pos > 0 && self.children[pos - 1].as_ref().unwrap().size > 1)
                    || (pos < self.size && self.children[pos + 1].as_ref().unwrap().size > 1)
                {
                    self.rotate(pos);
                } else if self.size > 1 {
                    // 隣接兄弟ノードの大きさが1で親要素の大きさが2以上の場合, マージを行う
                    self.merge(pos);
                } else {
                    // 高さを1下げる.
                    self.shrink();
                }
            }
        
        

    削除処理もまた再帰的な処理になっています. はじめに終端処理について説明します.

    if self.is_leaf() {
        let pos = self.find_index(value);
        if pos < self.size && self.data[pos].as_ref().unwrap() == value {
            delete_from_array(&mut self.data, pos);
            self.size -= 1;
            return true;
        }
        return false;
    }
    

    selfが葉ノードの場合, 値がデータとして保持されているか探し, 保持されている場合はそれを削除します.

    次に, selfのデータにvalueが存在せず, valueが存在する可能性のある部分木の保持するデータの個数が1より大きい場合です. これは単純に部分木に削除の再帰を渡すだけで済みます.

    let pos = self.find_index(value);
    let is_internal = pos < self.size && self.data[pos].as_ref().unwrap() == value;
    if !is_internal && self.children[pos].as_ref().unwrap().size > 1 {
        // いつかはここに引っかかるはず
        // 子ノードの大きさが2以上の場合, それを起点に再帰的に削除を行う.
        return self.children[pos].as_mut().unwrap().delete(value);
    }
    

    特別視して考える必要がある場合に, 削除する値が存在する部分木が1つのデータしか保持していない場合があります.

    2
    2
    1
    1
    3
    3
    10
    10
    8
    8
    6
    6
    7
    7
    9
    9
    11
    11
    5
    5
    Text is not SVG - cannot display

    上図から1や6, 8を削除する場合がそれにあたるでしょう. もし削除するデータがちょうどそこにあったら, データ数が0のノードが出来上がってしまいます. これを防ぐために, 削除の道中にある1つのデータしか保持していないノードはすべて2つ以上のデータを保持するように調整します. これには3つの方法があります.

    回転

    隣接する兄弟ノードのどちらかに2つ以上のデータを保持しているものがあれば, 回転と呼ばれる操作を行います. さきほど示した図でいう2や9がこれにあたります.

    回転は, 自分と兄弟ノードに挟まれたデータの値を自分の値に加え, それがもとにあった位置に兄弟ノードから適切な値を持ってきます. このとき, 2-3-4木のデータとノードの順序関係が満たされるように注意します. また, 兄弟ノードのデータが一つ減るので, 兄弟ノードが保持する子ノードのうち(甥ノード?)もっとも自分に近いものを自分の子に加えます.

    例として, 2を回転させる場合, 下図のようになります.

    2
    2
    5
    5
    1
    1
    3
    3
    10
    10
    6
    6
    7
    7
    9
    9
    11
    11
    8
    8
    Text is not SVG - cannot display

    2-3-4木の定義を満たしつつ, 2を含むノードのデータ数が2になっていることがわかるかと思います.

    回転のコードは以下のようになります.

    fn rotate(&mut self, pos: usize) { // self.children[pos].sizeを1から2にする
        if pos > 0 && self.children[pos - 1].as_ref().unwrap().size > 1 {
            // 左の兄弟から値を持ってくる
            let brother_pos = pos - 1;
            let parent_pos = pos - 1;
            // data = [parent_data, self.data, None];
            // children = [*brother.children, *self.children];
            let parent_data = delete_from_array(&mut self.data, parent_pos); // 自分より一つ左にあるデータをとってくる.
            insert_to_array(
                &mut self.children[pos].as_mut().unwrap().data,
                0, // 既存のデータより小さいことが保証されるので0に挿入
                parent_data, 
            );
            let brother_size = self.children[brother_pos].as_mut().unwrap().size;
            let brother_data = delete_from_array(
                &mut self.children[brother_pos].as_mut().unwrap().data,
                brother_size - 1, // brotherのデータの最大のものを持ってくる.
            );
            insert_to_array(&mut self.data, parent_pos, brother_data);
            let brother_child = delete_from_array(
                &mut self.children[brother_pos].as_mut().unwrap().children,
                brother_size,
            ); // brotherの子の最も右にあるものを自分の最も左の子とする.
            let children = &mut self.children[pos].as_mut().unwrap().children;
            insert_to_array(children, 0, brother_child);
            self.children[brother_pos].as_mut().unwrap().size -= 1;
            self.children[pos].as_mut().unwrap().size = 2;
        } else {
            // 右の兄弟から値を持ってくる
            let brother_pos = pos + 1;
            let parent_pos = pos;
            // data = [self.data, parent_data, None];
            // children = [*brother.children, *self.children];
            let parent_data = delete_from_array(&mut self.data, parent_pos);
            insert_to_array(
                &mut self.children[pos].as_mut().unwrap().data,
                1,
                parent_data,
            );
            let brother_data =
                delete_from_array(&mut self.children[brother_pos].as_mut().unwrap().data, 0);
            insert_to_array(&mut self.data, parent_pos, brother_data);
            let brother_child = delete_from_array(
                &mut self.children[brother_pos].as_mut().unwrap().children,
                0,
            );
            let size = self.children[pos].as_mut().unwrap().size;
            let children = &mut self.children[pos].as_mut().unwrap().children;
            insert_to_array(children, size, brother_child);
            self.children[brother_pos].as_mut().unwrap().size -= 1;
            self.children[pos].as_mut().unwrap().size = 2;
        };
    }
        

    マージ

    親のデータ数が2以上かつ兄弟ノードのどちらもデータ数が1の場合に使います. 先ほどの図では11を削除する場合がこれにあたるでしょう.

    この場合, 適当な兄弟ノードを選んで, 二つのデータとその間にあったデータを自分の新たなデータとし, データ数が3のノードとします.
    兄弟ノードの子ノードは自分のノードに加えます. これによって自分の子ノードの数は4になり, 親ノードのデータ数は1へり, 2-3-4木の定義を満たします.

    例として, 11を削除する場合, 下図のようになります.

    2
    2
    1
    1
    3
    3
    8
    8
    6
    6
    7
    7
    9
    9
    11
    11
    10
    10
    5
    5
    Text is not SVG - cannot display

    マージのコードは以下のようになります.

    fn merge(&mut self, pos: usize) {
        // 兄弟要素の値と親要素のいい感じの値を自分のdataとし, 兄弟要素の子要素を自分の子要素とする.
        self.size -= 1;
        self.children[pos].as_mut().unwrap().size = 3;
        if pos == 3 { // 基本的には右の兄弟を選ぶが, posが3の場合は左の兄弟を選ぶ.
            let brother_pos = pos - 1;
            let parent_pos = pos - 1;
            let parent_data = delete_from_array(&mut self.data, parent_pos);
            let brother_data =
                delete_from_array(&mut self.children[brother_pos].as_mut().unwrap().data, 0);
            let data = &mut self.children[pos].as_mut().unwrap().data;
            insert_to_array(data, 0, parent_data);
            insert_to_array(data, 0, brother_data); // dataは[兄弟のデータ, 親のデータ, 自分のデータ]の順になり, これはソートされている.
            // 兄弟の子ノードを自分の子ノードとする.
            let brother_child0 = delete_from_array(
                &mut self.children[brother_pos].as_mut().unwrap().children,
                0,
            );
            let brother_child1 = delete_from_array(
                &mut self.children[brother_pos].as_mut().unwrap().children,
                0,
            );
            let children = &mut self.children[pos].as_mut().unwrap().children;
            insert_to_array(children, 0, brother_child1);
            insert_to_array(children, 0, brother_child0);
        } else {
            let brother_pos = pos + 1;
            let parent_pos = pos;
            let parent_data = delete_from_array(&mut self.data, parent_pos);
            let brother_data =
                delete_from_array(&mut self.children[brother_pos].as_mut().unwrap().data, 0);
            let data = &mut self.children[pos].as_mut().unwrap().data;
            data[2] = parent_data;
            data[3] = brother_data;
            let brother_child0 = delete_from_array(
                &mut self.children[brother_pos].as_mut().unwrap().children,
                0,
            );
            let brother_child1 = delete_from_array(
                &mut self.children[brother_pos].as_mut().unwrap().children,
                0,
            );
            let children = &mut self.children[pos].as_mut().unwrap().children;
            children[2] = brother_child0;
            children[3] = brother_child1;
        }
    }
        

    最後に, 親も兄弟もひとつずつしかデータを持っていない場合を考えます. 先ほどの図でいう1がこれにあたります.

    この場合は, 縮小と呼ばれる処理を行います.

    縮小では, 自分のデータと兄弟のデータを親ノードにまとめて, 親ノードをデータ数3のノードにします. 自分と兄弟の子ノードも親ノードの子とし, 整合性を保たせます.

    1を削除した場合, 下図のようになります.

    2
    2
    3
    3
    1
    1
    10
    10
    8
    8
    6
    6
    7
    7
    9
    9
    11
    11
    5
    5
    Text is not SVG - cannot display

    高さが左の部分木と右の部分木で異なってしまっていますが, 実際の処理では, 親ノードが何かの子ノードであれば, そのデータ数が1となることはないので(回転やマージによって修正される), この処理は根ノードの場合にのみ行われ, 高さについての条件も守られます.

    縮小のコードは以下のようになります.

    fn shrink(&mut self) {
        // self.data = [left.data, self.data, right.data, None];
        // self.children = [*left.children, *right.children];
        self.size = 3;
        insert_to_array(&mut self.data, 0, self.children[0].as_mut().unwrap().data[0].take());
        self.data[2] = self.children[1].as_mut().unwrap().data[0].take();
        assert!(self.children[1].as_mut().unwrap().children[0].is_some());
        // 上書きされた後に取得することを防ぐため, 3, 2, 1, 0の順で保存する.
        self.children[3] = self.children[1].as_mut().unwrap().children[1].take();
        self.children[2] = self.children[1].as_mut().unwrap().children[0].take();
        self.children[1] = self.children[0].as_mut().unwrap().children[1].take();
        self.children[0] = self.children[0].as_mut().unwrap().children[0].take();
    }
        

    また, 今回のコードでは, delete_balanceという関数を用意し, これらのどれを行うかを判断するようにしています.

    fn delete_balance(&mut self, pos: usize) {
        // 隣接兄弟ノードの大きさが2以上の場合, 回転を行う
        if (pos > 0 && self.children[pos - 1].as_ref().unwrap().size > 1)
            || (pos < self.size && self.children[pos + 1].as_ref().unwrap().size > 1)
        {
            self.rotate(pos);
        } else if self.size > 1 {
            // 隣接兄弟ノードの大きさが1で親要素の大きさが2以上の場合, マージを行う
            self.merge(pos);
        } else {
            // 高さを1下げる.
            self.shrink();
        }
    }
        

    削除するデータが葉ノードにない場合

    削除するデータが葉ノードにない場合も一つの悩みになるでしょう.

    2
    2
    1
    1
    3
    3
    10
    10
    8
    8
    6
    6
    7
    7
    9
    9
    11
    11
    5
    5
    Text is not SVG - cannot display

    先ほど載せた図と同じものを再度載せておきます. この図で10をなにか特別な処理せずに削除した場合を考えましょう. データ数が1, 子ノード数が3あるノードができてしまいます.

    この事態を避けるために, 葉ノードではないノードにあるデータを削除する場合, これを葉ノードにある適切なデータと交換します.

    具体的には, このコードでは対象となるノードの左の部分木で最も大きい要素と交換することになっています.

    交換が終わった後の図は次のようになります.

    2
    2
    1
    1
    3
    3
    8
    8
    9
    9
    6
    6
    7
    7
    10
    10
    11
    11
    5
    5
    Text is not SVG - cannot display

    この操作は上で述べたマージや回転がすべて済んだ後に行う必要があります.

    以上のことを踏まえて, 再度先ほどのコードを見ていきます.

    if !is_internal { // internalでないかつ子ノードの大きさが1の場合
        self.delete_balance(pos);
        return self.delete(value);
    }
    

    自分の保持するデータにvalueが存在しない場合, delete_balanceを行ってから, もう一度is_internalの確認をすることになります.

    これは回転などによって自分のデータにvalueが来るのを対策しています.

    self.delete_balance(pos); // 根についてはここで回転やマージが行われる.
    // 内部ノードである場合, 通り道全体にrotateとかしてから左側最大と交換して削除する
    let mut current = self.children[pos].as_mut().unwrap();
    while !current.is_leaf() {
        current.delete_balance(current.size);
        current = current.children[current.size].as_mut().unwrap();
    }
        

    自分のデータにvalueが存在する場合, まずは根について, そしてそこから通り道全体に回転やマージが行われます.

    let pos = self.find_index(value);
    let is_internal = pos &l self.size && self.data[pos].as_ref().unwrap() == value;
    if is_internal {
        let mut current = self.children[pos].as_mut().unwrap();
        while !current.is_leaf() {
            current.delete_balance(current.size);
            current = current.children[current.size].as_mut().unwrap();
        }
        std::mem::swap(&mut self.data[pos], &mut current.data[current.size - 1]);
        return current.delete(value);
    }
    self.delete(value)
        

    その後, 自分のデータにvalueがある場合はそれを左側最小と交換してから削除し, そうでない場合はself.deleteを行い, 上で述べた自分のデータにvalueがないかつ対象の子ノードのデータ数が1より大きい場合に引っかかる

    終わりに

    2-3-4木のRustによる実装を説明しました. バグとか無限にありそうな気がしますが, 実際二分探索を行おうとするときにこれを使うことはないと思うので雰囲気だけ味わっていただければと思います.

    この記事を書く上で参考にしたウェブサイトなどを紹介します.

    暇があったら赤黒木の実装もしようと思います. 2-3-4木は特別な処理が多くてかなり時間を要してしまいましたが, 赤黒木はもう少し簡単だったらうれしいです.

    競プロのためのプリントデバッグ(Rust)

    RustのデバッガはたいていLLDBだと思いますが, コンテナの中身がうまく表示されなかったりするので, プリントデバッグが多めになってしまいます。 自分でGUIアプリ作るとかならコンソールに何出力しようが困らないですが, 競技プログラミングだといかんせんそうもいかないです。 なので, ローカルでは表示されるが, 提出時にはなにもでないようなコンソール出力マクロを定義しました。

    目次

      コンソール出力マクロ

      #[allow(unused_macros)]
      #[cfg(feature = "local")]
      macro_rules! lprintln {
              ($($arg:tt)*) => (println!($($arg)*));
      }
      
      #[allow(unused_macros)]
      #[cfg(not(feature = "local"))]
      macro_rules! lprintln {
          ($($arg:tt)*) => {};
      }
      
      #[allow(unused_macros)]
      #[cfg(feature = "local")]
      macro_rules! lprint {
              ($($arg:tt)*) => (print!($($arg)*));
      }
      
      #[allow(unused_macros)]
      #[cfg(not(feature = "local"))]
      macro_rules! lprint {
          ($($arg:tt)*) => {};
      }
          

      workspace>.vscode>settings.jsonに以下を書き加えます

      {
          "rust-analyzer.cargo.features": ["local"]
      }
          

      vscode拡張機能を使って実行するときは自動でlocalというfeatureフラグが付与されます. Atcoderではこのフラグが付与されないので, なにも実行されません。

      自分の手で実行するならcargo run -F localのようにします。

      おわりに

      一応lprintはlocal printの略のつもりですがダサいですね。hintとかのほうがスマートな感じがします。
      ほんとは自分のダイクストラの実装とか座標とか方向のための便利な構造体とかも載せようかと思いましたが、Rustのテンプレートに使われる<usize>がhtmlのタグに誤認されて鬱陶しかったのでやめました。 ありきたりですしね。
      世の中にはAtcoder用のライブラリがあるそうですが、自分で書いたほうがなんか楽しいので自分で書いてます。

      ABC369

      当日に参加したわけではないですが、結構解けたので自分が解けた問題の解法を乗せておきます。解説というよりかはアウトプット位のイメージです。
      解けた問題はABCDEの五問、使用言語はRustです。

      各セクションはタイトルのAとかEとか押したら問題のページに、提出を押すと私のAC提出に飛んでいくようにリンクを張っているつもりです。掲載されているコードはロジックに関係ある部分を抜粋したものです。

      目次

      1. A
      2. B
      3. C
      4. D
      5. E

      A

      提出

      fn main() {
          input! {
              a: usize,
              b: usize,
          }
          println!("{}", if a == b {
              1
          } else if (a + b) % 2 == 0 {
              3
          } else {
              2
          });
      }
          

      解き方

      A=Bならばx=Aしかありえないので一通り, AとBの差が奇数ならばxはA - BとB - Aの二通り, 偶数ならばそれに加えて(A + B) / 2の三通りがあるのでこれを出力する.

      B

      提出

      fn main() {
          input! {
              n: usize,
              a: [(usize, char); n],
          }
          let ls = a.iter().filter(|&x| x.1 == 'L').map(|x| x.0).collect::<Vec<_>>();
          let rs = a.iter().filter(|&x| x.1 == 'R').map(|x| x.0).collect::<Vec<_>>();
          let mut ans = 0;
          for i in 1..ls.len() {
              ans += ls[i].abs_diff(ls[i - 1]);
          }
          for i in 1..rs.len() {
              ans += rs[i].abs_diff(rs[i - 1]);
          }
          println!("{}", ans);
      }
          

      解き方

      入力を左手と右手に分けてから疲労度を求めるとコードがきれいになります。
      手の最初の位置云々ありますが最初に引く場所においておけば0になるので考慮しなくても大丈夫です。

      C

      提出

      fn main() {
          input! {
              n: usize,
              a: [isize; n],
          }
          if n == 1 {
              println!("{}", 1);
              return;
          }
          let mut diffs = vec![0; n - 1];
          for i in 0..n - 1 {
              diffs[i] = a[i + 1] - a[i];
          }
          let diffs = diffs;
          lprintln!("{:?}", diffs);
          let mut counter = vec![0; n - 1];
          let mut i = 0;
          let mut j = 0;
          // 連続している要素の数を数え上げ, 圧縮する
          while i < n - 1 {
              // 連続している部分をカウント
              while i + counter[j] < n - 1 && diffs[i + counter[j]] == diffs[i] {
                  counter[j] += 1;
              }
              i += counter[j];
              counter[j] += 1;
              j += 1;
          }
          lprintln!("{:?}", counter);
          let ans = counter.iter().filter(|&&x| x != 0).map(|x| x * (x + 1) / 2 - 1).sum::() + 1;
          println!("{}", ans);
      }
          

      解き方

      1 4 7 1 2 3という入力を例に説明します。 まずそれぞれの要素について, 次の要素との差をとります。入力は3 3 -6 1 1になります。
      次に, 連続している差の数を調べます。 3が2つ, -6が1つ, 1が2つなので, 2 1 2になります。
      このとき, 連続している要素の数は連続している差の数に1加えたものになるので, 各要素に1を加えます。 3 2 3
      要素が3個連続しているということは, ここから長さ3の等差数列が1つ, 長さ2の等差数列が2つ, 長さ1の等差数列が3つ得られるということです。 ただし, 最後の要素は次の連続している要素にも含まれるので, 1を引いておきます。一番最後の要素は次の要素がないので, 1を足しておきます。
      つまり, 答えは(1 + 2 + 3) - 1 + (1 + 2) - 1 + (1 + 2 + 3) - 1 + 1 = 13になります。

      D

      提出

      fn main() {
          input! {
              n: usize,
              a: [usize; n],
          }
          /*
              dp(i, 1/0) = i番目までのモンスターを見ていったときに, 次が偶数/奇数番目となるように倒したときの最大経験値
              dp(1, 1) = 0
              dp(1, 0) = a[0]
              dp(i, 1) = max of dp(i - 1, 0) + a[i - 1], dp(i - 1, 1)
              dp(i, 0) = max of dp(i - 1, 1) + 2 * a[i - 1], dp(i - 1, 0)
              */
          let mut dp = vec![[0; 2]; n + 1];
          dp[0][0] = 0;
          dp[0][1] = 0;
          dp[1][0] = 0;
          dp[1][1] = a[0];
          for i in 2..=n {
              dp[i][0] = dp[i - 1][0].max(dp[i - 1][1] + 2 * a[i - 1]);
              dp[i][1] = dp[i - 1][1].max(dp[i - 1][0] + a[i - 1]);
          }
          lprintln!("{:?}", dp);
          println!("{}", dp[n][0].max(dp[n][1]));
      }
          

      解き方

      動的計画法です。 最初はn*nのdpテーブルを作り, dp[i][j] = i番目までのモンスターに遭遇し, j匹倒したときの最大経験値となるように設計したのですが,
      メモリ使用量が最大で64 * 4 * 10^10 / 8 / 1024 / 1024 = 305175MBになりRE
      代えてdp(i, 1/0) = i番目までのモンスターを見ていったときに, 次に倒すのが偶数/奇数番目となるように倒したときの最大経験値 と設計しました。
      つまり, 3匹目までのモンスターまで見て, 1匹のみ倒したときより3匹倒したときのほうが経験値が大きかった場合, 次に倒すのが4匹目の偶数番目になるのでdp[3][0]にその経験値の値を入れます。
      更新式は

                  dp(i, 1) = max(
                      dp(i - 1, 0) + a[i], # i匹目のモンスターを倒すとき
                      dp(i - 1, 1) # i匹目のモンスターを倒さないとき
                  )
                  dp(i, 0) = max(
                      dp(i - 1, 1) + 2 * a[i], # i匹目のモンスターを倒すとき
                      dp(i - 1, 0) # i匹目のモンスターを倒さないとき
                  )
              

      となります。
      dpの問題はいつもテーブルのインデックスの取り方に悩まされています。ちょっとずるいような気がしますが, データの制約を見て大体のメモリ使用量と時間計算量を見積もりながら考えるようにしています。

      E

      提出

      fn main() {
          input! {
              n: usize,
              m: usize,
              edges: [(usize, usize, usize); m],
              q: usize,
          }
          let edges = edges
              .into_iter()
              .map(|(a, b, c)| (a - 1, b - 1, c))
              .collect::<Vec<_>>();
          let mut graph = vec![vec![usize::MAX; n]; n];
          for (u, v, c) in edges.iter() {
              graph[*u][*v] = graph[*u][*v].min(*c);
              graph[*v][*u] = graph[*v][*u].min(*c);
          }
          for i in 0..n {
              graph[i][i] = 0;
          }
          for k in 0..n {
              for i in 0..n {
                  for j in 0..n {
                      if graph[i][k] == usize::MAX || graph[k][j] == usize::MAX {
                          continue;
                      }
                      graph[i][j] = graph[i][j].min(graph[i][k] + graph[k][j]);
                  }
              }
          }
          for i in 0..n {
              lprintln!("{:?}", graph[i]);
          }
          for _ in 0..q {
              input! {
                  k: usize,
                  mut b: [usize; k],
              }
              let mut mi = usize::MAX;
              b.sort();
              for p in b.iter().map(|x| x - 1).permutations(k) {
                  for bits in 0..(1 << k) {
                      mi = mi.min(solve(&graph, &edges, &p, bits));
                  }
              }
              println!("{}", mi);
          }
      }
      
      fn solve(
          graph: &Vec<Vec>,
          edges: &Vec<(usize, usize, usize)>,
          p: &Vec,
          mut bits: usize,
      ) -> usize {
          let mut ans = 0;
          let mut start = 0;
          let n = graph.len();
          for i in 0..p.len() {
              let (from, to, cost) = edges[p[i]];
              if bits % 2 == 0 {
                  ans += graph[start][from] + cost;
                  start = to;
              } else {
                  ans += graph[start][to] + cost;
                  start = from;
              }
              bits /= 2;
          }
          ans += graph[start][n - 1];
          ans
      }
          

      解き方

      最短距離の問題で何回もクエリが飛んでくる場合はとりあえずワーシャルフロイド法で全点間の最短距離を求めます。
      制約が小さいので, 辺のとおり方の順列全探索, どっち向きに通るかのビット全探索を組み合わせて解きます。

      感想

      需要はないでしょうが何か書かないと締まらない感じがしますので競プロ関係で近況報告をば。
      AtcoderのABCは夜九時からということで, たいてい時間が合わない, そもそもやる気がないので参加していませんが暇な時に解いてみるくらいの気持ちで取り組んでいます。
      随分と易しかったですが、一応E問題が解けたので満足です。

      コンピュータは実数を正確に表すことができない

      はじめに

      0.1 + 0.1 + 0.1をプログラムに計算させてもぴったり0.3にはならない。というようなことがよく話題に上がります。これは、0.1が二進数では無限小数になってしまうことに起因します。なので、小数ではなく、分子と分母を整数で管理して、通分や約分の様な操作をしてあげることでこの問題は解決できます。

      しかし、世の中には分子と分母だけでは表せない数も存在します。例えば、 \pi,  \sqrt 2などがこれに該当します。分数で表せる数を有理数、表せない数を無理数、その二つを総称して実数と呼びます。

      もしも実数を正確にコンピュータで表現できたら、数学や物理の煩雑な計算はプログラミングで解決できるようになるでしょう。しかし、これは数学的に不可能であると証明できると考えられます。今回はその証明を考えてきたので披露します。

      プレビューで見るとところどころtexが反映されてないように見えますが、解決しそうにないので頑張って読んでください。

      定義

      コンピュータで数集合 Sを正確に表す、とはある n \in \mathbb{N}が存在して[tex: |S| \le |\mathbb{Nn}|]であることを指します。感覚的には、 k \in S n個の自然数の組に変換できるということを指しています。[tex: |\mathbb{Zn}| = |\mathbb{Nn}|]であるので, これで十分でしょう。例えば、分数だったら分子と分母の 2個の整数、四元数だったら 4個の整数となるでしょう。

      また、メモリのサイズなどの制約は考慮しないものとします。

      前提知識

      無限集合 S, Tについて,  |S| \le |T|とは Sの濃度よりも Tの濃度のほうが大きいことを指しており、これはある単射写像 f: S \rightarrow Tが存在することと同値です。

      また,  |S| = |T|とは, ある全単射写像 f: S \rightarrow Tが存在することと同値です。

       |\mathbb{R}| > |\mathbb{N}|は実数 整数 濃度みたいな検索をしてもらえればいくらでも証明が出てくるので今回は既知としておきます。

      証明の流れ

      帰納法によって \forall n \in \mathbb{N}, |\mathbb{N}| = |\mathbb{N}^n|を証明します。

      証明

       (n, m) \in \mathbb{N}^2に対して,  f(n, m)=\frac{1}{2} ( (n+m-1)(n+m) - (n-m) ) |\mathbb{N}^2|から |\mathbb{N}|への全単射となるので,  |\mathbb{N}| = |\mathbb{N}^2|が証明されます。

      感覚的には、下図のように、斜めに埋めていく感じです。

      次に,  g_nを[tex: (a_1, a_2, ..., a{n+1})]を引数にとって, [tex: (a_1, a_2, ..., f(a_n, a{n+1}))]とすれば,  g_n \mathbb{N}^{n+1}から \mathbb{N}^nへの全単射となります. すなわち,  |\mathbb{N}^{n+1}|=|\mathbb{N}^n|といえます.

      よって, 帰納的に[|\mathbb{N}^n| = |\mathbb{N}|]であることが示せました.

      おわりに

      左かっこを二回重ねたら脚注になるのめんどくさいですね