👨‍🍳

ABC403:Rustで解く!問題解説

に公開

AtCoder Beginner Contest 403のA~E問題をRustで解いた際の解法をまとめました。

A問題

問題

https://atcoder.jp/contests/abc403/tasks/abc403_a

解説

数列 A の奇数番目(1-indexed)の値のみを足した合計値を求めます。

コード

abc403a.rs
use proconio::input;

fn main() {
    // 入力
    input! {
        n: usize,
        a: [usize; n],
    }

    // 奇数番目(1-indexed)に対応する偶数インデックス(0-indexed)の値を足す
    let mut ans = 0;
    let mut cur_pos = 0;
    while cur_pos < n {
        ans += a[cur_pos];
        cur_pos += 2;
    }

    // 出力
    println!("{}", ans);
}

B問題

問題

https://atcoder.jp/contests/abc403/tasks/abc403_b

解説

この問題は、文字列 T が文字列 U を部分文字列として含むかどうかを判定する問題です。

文字列 T の各位置を開始位置と仮定し、そこから U の長さ分の文字が一致するかを確認します。なお、文字列 T に含まれる ? はワイルドカードとして扱い、文字列 U のどの文字とも一致するとみなして問題ありません。

探索範囲は T の長さを |T|U の長さを |U| とした場合、|T| - |U| + 1 回のチェックを行います。

コード

abc403b.rs
use proconio::{input, marker::Chars};

fn main() {
    // 入力
    input! {
        t: Chars,
        u: Chars,
    }

    // 部分文字列の一致判定
    let mut found = false;
    for start_pos in 0..=t.len() - u.len() {
        if is_match(start_pos, &t, &u) {
            found = true;
            break;
        }
    }

    // 結果を出力
    println!("{}", if found { "Yes" } else { "No" });
}

// 部分文字列が一致するかを判定する関数
fn is_match(start_pos: usize, t: &Vec<char>, u: &Vec<char>) -> bool {
    for i in 0..u.len() {
        if t[start_pos + i] != '?' && t[start_pos + i] != u[i] {
            return false;
        }
    }
    true
}

C問題

問題

https://atcoder.jp/contests/abc403/tasks/abc403_c

解説

この問題は、ユーザ X がページ Y の閲覧権限を持っているかどうかを判定する問題です。

権限の管理は以下のように行います。

  • 個別の閲覧権限:ユーザごとにHashSetを用いて管理します。
  • 全体の閲覧権限:ユーザごとにbool型の配列を用いて管理します。

クエリに応じて以下の処理を行います。

  1. クエリ1(ユーザ X にページ Y の閲覧権限を付与)
    • ユーザ XHashSet にページ Y を追加します。
  2. クエリ2(ユーザ X に全体の閲覧権限を付与)
    • ユーザ X の全体権限フラグを true に設定します。
  3. クエリ3(ユーザ X がページ Y の閲覧権限を持っているか判定)
    • 以下の条件で判定します。
      • 全体権限が true の場合:閲覧可能
      • 全体権限が false かつ HashSet にページ Y が含まれる場合:閲覧可能
      • 上記いずれにも該当しない場合:閲覧不可

コード

abc403c.rs
use proconio::{input, marker::Usize1};
use std::collections::HashSet;

fn main() {
    // 入力
    input! {
        n: usize, _m: usize, q: usize,
    }

    // 各ユーザの個別の閲覧権限を管理するHashSet
    let mut perm_pages = vec![HashSet::new(); n];
    // 各ユーザの全体権限を管理するフラグ
    let mut all_allow = vec![false; n];

    // クエリ処理
    for _ in 0..q {
        input! {
            query_type: usize,
        }

        if query_type == 1 {
            // クエリ1: ユーザXにページYの権限を付与
            input! {
                x: Usize1, y: Usize1,
            }
            perm_pages[x].insert(y);
        } else if query_type == 2 {
            // クエリ2: ユーザXに全体権限を付与
            input! {
                x: Usize1,
            }
            all_allow[x] = true;
        } else if query_type == 3 {
            // クエリ3: ユーザXがページYの権限を持っているか判定
            input! {
                x: Usize1, y: Usize1,
            }
            if all_allow[x] || perm_pages[x].contains(&y) {
                println!("Yes");
            } else {
                println!("No");
            }
        }
    }
}

D問題

問題

https://atcoder.jp/contests/abc403/tasks/abc403_d

解説

この問題は、整数列 A の全ての組 (A_i, A_j) について、絶対値が D とならないように整数列 A から要素を削除するときの削除個数を最小化する問題です。

以下の1~4の手順を考えることで解くことができます。

  1. 整数列 A の並び替え
    元の整数列 A のままでは、要素の削除要否を考えるのは難しいです。全ての組を考える際、整数列 A の並び変えても調べる組は同じであるため、整数列 A を昇順にソートします。これにより、絶対値が D という条件は A_j - A_i = D に限定して調べるだけでよくなります。

  2. 余りによるグループ分け
    D で割った余りが同じ値同士の要素は、絶対値が D となる可能性があります。そのため、D で割った余りごとにグループ分けを行います。ただし、D=0 の場合は特別な処理が必要です(後述)。

  3. グループごとの削除個数の最小化
    各グループ内で、差が D の倍数である要素を削除する必要があります。このとき、動的計画法(DP)を用いて、削除個数を最小化します。

    • 状態: dp[\text{cur}][\text{選択}]
      • \text{cur} : 現在見ている要素のインデックス
      • \text{選択} : 現在の要素を削除するかしないか(0 : 削除, 1 : 非削除)
    • 遷移:
      1. 削除する場合: 前回の状態が削除/非削除のどちらでも遷移可能
      2. 削除しない場合: 前回の状態が削除である必要がある。ただし、前回の値との差が D より大きい場合は非削除が可能
  4. D=0 の場合の特別な処理
    D=0 の場合、余りでグループ分けすることができません。この場合、数列 A の中で重複している要素を削除する必要があります。
    具体的には、n - \text{種類数} が削除する個数になります。

以上をまとめると、整数列 A を昇順にソートし、D=0 の場合とそれ以外の場合で分けて処理することで解くことができます。

コード

abc403d.rs
use proconio::input;
use std::collections::{HashSet, BTreeMap};
use std::cmp::min;
const INF: usize = 1 << 60;

fn main() {
    // 入力
    input! {
        n: usize, d: usize,
        mut a: [usize; n],
    }
    a.sort();

    // Dが0のときの解法
    if d == 0 {
        solve_d_zero(n, &a);
    }
    // Dが0でないときの解法
    else {
        solve_d_nonzero(d, &a);
    }
}

fn solve_d_zero(n: usize, a: &Vec<usize>) {
    // Aの重複を排除した分が答え
    let mut unique_elements = HashSet::new();
    for &value in a {
        unique_elements.insert(value);
    }
    println!("{}", n - unique_elements.len());
}

fn solve_d_nonzero(d: usize, a: &Vec<usize>) {
    // Aの各値を昇順にまとめる
    let mut frequency_map = BTreeMap::new();
    for &value in a {
        *frequency_map.entry(value).or_insert(0) += 1;
    }

    // Dで割った余りでグループ分け
    let mut groups = vec![Vec::new(); d];
    for (&key, &_value) in &frequency_map {
        let idx = key % d;
        groups[idx].push(key);
    }

    let mut ans = 0;

    // グループごとにDP
    for group in groups {
        let sz = group.len();

        // DP[cur個目まで見た][cur個目の選択(削除/非削除)] = 削除個数の最小値
        let mut dp = vec![vec![INF; 2]; sz + 1];
        dp[0][0] = 0; // 初期状態: 削除
        dp[0][1] = 0; // 初期状態: 非削除

        // DP遷移
        for cur in 1..=sz {
            let del_cnt = *frequency_map.get(&group[cur - 1]).unwrap();

            // 削除する場合
            dp[cur][0] = min(dp[cur - 1][0], dp[cur - 1][1]) + del_cnt;

            // 削除しない場合
            dp[cur][1] = dp[cur - 1][0];
            if cur == 1 || group[cur - 1] - group[cur - 2] != d {
                dp[cur][1] = min(dp[cur][1], dp[cur - 1][1]);
            }
        }

        // グループ内の最小削除個数を加算
        ans += min(dp[sz][0], dp[sz][1]);
    }

    // 出力
    println!("{}", ans);
}

E問題

問題

https://atcoder.jp/contests/abc403/tasks/abc403_e

解説

この問題は、集合 Y に含まれる各文字列について、集合 X に含まれる文字列の接頭辞でないものの個数を答える問題です。

各クエリで集合 X または集合 Y に文字列を追加し、接頭辞でないものの個数を毎回答える必要があります。文字列を1つ追加するたびに集合 X や集合 Y の文字列を調べ直す方法を取ると、全体の計算量は O(Q^2) となり、実行時間制限内に解くことができません。そのため、接頭辞の判定を効率よく行う必要があります。

そこで、Trie木というデータ構造を用いることで、接頭辞の判定を効率化します。Trie木は、文字列の先頭部分の共通部分を共有して保存する木構造で、各文字を順番に辿ることで接頭辞を効率よく管理できます。

以下の1~2の手順で問題を解きます。

  1. Trie木の構築
    全てのクエリ情報を先に読み込み、Trie木を構築します。この際、各文字列の末端ノードを記録します。

  2. クエリ処理

    • クエリが T = 1 の場合(集合 X に文字列を追加する場合)
      • 追加した文字列が指すノードとその子ノード全てを切り離します。また、切り離したノードで数えていた接頭辞でない文字列の個数を減らします。
    • クエリが T = 2 の場合(集合 Y に文字列を追加する場合)
      • 追加する文字列が切り離されていないノードであれば、接頭辞でない文字列の個数をインクリメントします。

この方法により、効率よく接頭辞の判定を行い、計算量を削減できます。

コード

abc403e.rs
use proconio::input;
use std::collections::HashMap;

fn main() {
    // 入力
    input! {
        q: usize,
    }

    // クエリ情報を格納
    let mut t = vec![0; q];
    let mut s = vec![String::new(); q];
    input_ts(&mut t, &mut s, q);

    // Trie木の構築
    let mut trie = Trie::new();
    let mut nodelist = Vec::new();
    for i in 0..q {
        let node = trie.add_string(&s[i]);
        nodelist.push(node);
    }

    // Trie木のカウントを初期化
    trie.initialize_count();

    // クエリ処理
    for i in 0..q {
        if t[i] == 1 {
            trie.regist_x(nodelist[i]);
        } else {
            trie.regist_y(nodelist[i]);
        }
        println!("{}", trie.ans);
    }
}

// クエリ情報を入力
fn input_ts(t: &mut Vec<usize>, s: &mut Vec<String>, q: usize) {
    for i in 0..q {
        input! {
            tt: usize,
            ss: String,
        }
        t[i] = tt;
        s[i] = ss;
    }
}

// Trie木の構造体
struct Trie {
    node: Vec<HashMap<char, usize>>, // ノード:[文字] = 次のノードのインデックス
    ans: usize,                      // 接頭辞でない文字列の個数
    connect: Vec<bool>,              // ノードの連結状態
    cnt_y: Vec<usize>,               // 集合Yに追加されたノードの文字列数
}

impl Trie {
    fn new() -> Self {
        Trie {
            node: vec![HashMap::new()],
            ans: 0,
            connect: Vec::new(),
            cnt_y: Vec::new(),
        }
    }

    // 文字列をTrie木に追加し、末端ノードのインデックスを返す
    fn add_string(&mut self, s: &str) -> usize {
        let mut cur = 0;
        for c in s.chars() {
            if let Some(&next) = self.node[cur].get(&c) {
                cur = next;
            } else {
                let new_idx = self.node.len();
                self.node.push(HashMap::new());
                self.node[cur].insert(c, new_idx);
                cur = new_idx;
            }
        }
        cur
    }

    // Trie木のカウントを初期化
    fn initialize_count(&mut self) {
        self.ans = 0;
        let n = self.node.len();
        self.connect = vec![true; n];
        self.cnt_y = vec![0; n];
    }

    // 集合Xに文字列を追加
    fn regist_x(&mut self, cur: usize) {
        if !self.connect[cur] {
            return;
        }
        self.connect[cur] = false;
        self.ans -= self.cnt_y[cur];

        let children: Vec<usize> = self.node[cur].values().cloned().collect();
        for next in children {
            self.regist_x(next);
        }
    }

    // 集合Yに文字列を追加
    fn regist_y(&mut self, cur: usize) {
        if !self.connect[cur] {
            return;
        }
        self.cnt_y[cur] += 1;
        self.ans += 1;
    }
}

Discussion