👨‍🍳

ABC406: Rustで解く!問題解説

に公開

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

A 問題

問題

https://atcoder.jp/contests/abc406/tasks/abc406_a

解説

この問題は、締め切り時刻と提出時刻を比較し、締め切り前に提出できたかを判定する問題です。
まず、締め切り時刻と提出時刻をそれぞれ分単位に変換します。その後、締め切り時刻 \geq 提出時刻のときは Yes 、そうでない場合は No を出力します。

コード

abc406a.rs
use proconio::input;

fn main() {
    // 入力
    input! {
        a: usize, // 締め切り時刻の時
        b: usize, // 締め切り時刻の分
        c: usize, // 提出時刻の時
        d: usize  // 提出時刻の分
    }
    
    // 時刻を分に変換
    let deadline = a * 60 + b;
    let submit = c * 60 + d;

    // 締め切り時刻と提出時刻を比較して結果を出力
    println!("{}", if deadline >= submit { "Yes" } else { "No" });
}

B 問題

問題

https://atcoder.jp/contests/abc406/tasks/abc406_b

解説

この問題では、数列 A に含まれる値を順番に掛け算していきます。
ただし、掛け算の結果が 10^K 以上になった場合、その時点で掛け算の結果を 1 にリセットします。最終的に N 回の掛け算を行った後の値を出力します。
注意点として、掛け算の結果が 64bit の最大値を超える可能性があるため、128bit 型の変数を使用して計算を行います。また、10^K を計算する際には、繰り返し掛け算を行う関数を用意しています。

コード

abc406b.rs
use proconio::input;

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

    // 10^kを求める
    let limit = power(10, k) as u128;

    let mut cur = 1u128;

    // 数列の値を順番に掛け算
    for aa in a {
        if cur * aa >= limit {
            // 掛け算結果が上限以上になった場合、1にリセット
            cur = 1;
        } else {
            cur *= aa;
        }
    }

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

// x^k を計算する関数
fn power(x: usize, k: usize) -> usize {
    let mut ret = 1;
    for _ in 0..k {
        ret *= x;
    }
    ret
}

C 問題

問題

https://atcoder.jp/contests/abc406/tasks/abc406_c

解説

この問題では、数列 (1, 2, \ldots, N) を並び替えたものについて、指定した範囲 [l, r] の部分列が「上に凸、下に凸」の形になるものの個数を求めます。

具体的には、指定した範囲の要素間の差が「増加→減少→増加」の順となっているものの個数を以下1~3の手順で求めます。

  1. 数列の隣接する要素間の差を調べ、前の値より大きい場合は '<'、小さい場合は '>' として記録します。

  2. この '<''>' の列をランレングス圧縮を用いて、(種類, 個数) のペア型で表します。
    ランレングス圧縮とは、連続する同じ文字を1つのグループとしてまとめる手法です。

  3. 圧縮された列の中から、'<', '>', '<' の形を持つ部分を探します。このとき、左側の '<' の個数と右側の '<' の個数を掛け合わせて、その総和を求めます。

コード

abc406c.rs
use proconio::{input, marker::Usize1};

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

    // 隣接する要素間の差を記録
    let mut diffs = Vec::new();
    for i in 0..n-1 {
        if p[i] < p[i+1] { diffs.push('<');}
        else { diffs.push('>');}
    }

    // '<''>'をランレングス圧縮する
    let rungs = runglen(&diffs);

    // サイズが3未満の場合は0
    if rungs.len() < 3 {
        println!("0");
        return;
    }

    // '<','>','<'を見つけて、個数を計算
    let mut ans = 0;
    for i in 0..rungs.len()-2 {
        if rungs[i].0 == '<' && rungs[i+1].0 == '>' && rungs[i+2].0 == '<' {
            ans += rungs[i].1 * rungs[i+2].1;
        }
    }
    
    // 出力
    println!("{}", ans);
}

// ランレングス圧縮
pub fn runglen(arr: &Vec<char>) -> Vec<(char, usize)> {
    let len = arr.len();
    let mut arr_ret = Vec::new();
    let mut tail = 0;
    let mut top = 0;
    while tail < len {
        let now_c = arr[tail];
        while top < len && now_c == arr[top] {
            top += 1;
        }
        arr_ret.push((now_c, top - tail));
        tail = top;
    }
    arr_ret
}

D 問題

問題

https://atcoder.jp/contests/abc406/tasks/abc406_d

解説

この問題は、指定された行または列にあるゴミの個数を出力し、その後その行または列に含まれるゴミを取り除く操作を行う問題です。

ゴミの座標を行ごと、列ごとに独立して管理し、クエリに応じて適切な操作を行います。

  1. クエリ1の場合
    指定された行に含まれるゴミの個数を出力し、その行に含まれるゴミを列方向のデータ構造からも削除します。

  2. クエリ2の場合
    指定された列に含まれるゴミの個数を出力し、その列に含まれるゴミを行方向のデータ構造からも削除します。

行と列のデータ構造を相互に更新することで、効率的にクエリを処理します。

コード

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

fn main() {
    // 入力
    input! {
        h: usize, w: usize, n: usize,
    }

    // ゴミの座標を行ごと、列ごとに管理
    let mut row = vec![HashSet::new(); h];
    let mut col = vec![HashSet::new(); w];
    input_garbage(n, &mut row, &mut col);

    // クエリの処理
    input! {
        q: usize,
    }

    for _ in 0..q {
        input! {
            query_type: usize,
        }

        if query_type == 1 {
            remove_row(&mut row, &mut col);
        } else {
            remove_col(&mut row, &mut col);
        }
    }
}

// ゴミの座標を入力し、行と列のデータ構造に登録
fn input_garbage(n: usize, row: &mut Vec<HashSet<usize>>, col: &mut Vec<HashSet<usize>>) {
    for _ in 0..n {
        input! {
            x: Usize1, y: Usize1,
        }
        row[x].insert(y);
        col[y].insert(x);
    }
}

// 指定された行のゴミを取り除く
fn remove_row(row: &mut Vec<HashSet<usize>>, col: &mut Vec<HashSet<usize>>) {
    input! {
        x: Usize1,
    }
    // 指定された行に残っているゴミの個数を出力
    println!("{}", row[x].len());

    // 行に含まれるゴミを列のデータ構造から削除
    for &item in &row[x] {
        col[item].remove(&x);
    }
    // 行のデータを空にする
    row[x] = HashSet::new();
}

// 指定された列のゴミを取り除く
fn remove_col(row: &mut Vec<HashSet<usize>>, col: &mut Vec<HashSet<usize>>) {
    input! {
        y: Usize1,
    }
    // 指定された列に残っているゴミの個数を出力
    println!("{}", col[y].len());

    // 列に含まれるゴミを行のデータ構造から削除
    for &item in &col[y] {
        row[item].remove(&y);
    }
    // 列のデータを空にする
    col[y] = HashSet::new();
}

E 問題

問題

https://atcoder.jp/contests/abc406/tasks/abc406_e

解説

この問題では、N 以下の正の整数 x について、popcount(※)の値がちょうど K であるものの総和を求めます。
※popcountとは、整数を二進数表記した際に1となっているビットの個数を指します。

この問題を解くために、動的計画法(DP)を用いて以下の値を計算します。

  • dp\_cur[j]:1が立っている個数が j の整数の個数
  • dp\_tot[j]:1が立っている個数が j の整数の総和

DPの遷移は以下のように行います。

  1. N 未満の値からの遷移

    • 0を選択する場合
      • next\_dp\_cnt[j] += cur\_dp\_cnt[j]
      • next\_dp\_tot[j] += cur\_dp\_tot[j]
    • 1を選択する場合
      • next\_dp\_cnt[j+1] += cur\_dp\_cnt[j]
      • next\_dp\_tot[j+1] += cur\_dp\_tot[j] + cur\_dp\_cnt[j] \times (i ビット目の値)
  2. N の値からの遷移(ただし、現在のビットが1の場合のみ)

    • 0を選択する場合
      • next\_dp\_cnt[j] += 1
      • next\_dp\_tot[j] += 現在の値

最終的に、DPで計算した cur\_dp\_tot[K] が答えとなります。ただし、N 自身のpopcountが K の場合は、N を加算します。

コード

abc406e.rs
use proconio::input;
use std::mem::swap;

const BITSZ: usize = 60;
const MOD: u128 = 998244353;

fn main() {
    // テストケース数を入力
    input! {
        t: usize,
    }
    for _ in 0..t {
        solve();
    }
}

fn solve() {
    // 各テストケースの入力
    input! {
        n: u128,
        k: usize,
    }
    // 計算結果を出力
    println!("{}", calc(n, k));
}

fn calc(n: u128, k: usize) -> u128 {
    // dp[j] = 0以上N未満の、1が立っている個数jの個数と総和
    let mut cur_dp_cnt = vec![0usize; BITSZ + 1];
    let mut cur_dp_tot = vec![0u128; BITSZ + 1];

    // 現在のpopcountと値
    let mut cur_pop_cnt = 0usize;
    let mut cur_num = 0u128;

    // 上位ビットから順に処理
    for i in 0..BITSZ {
        let mut next_dp_cnt = vec![0usize; BITSZ + 1];
        let mut next_dp_tot = vec![0u128; BITSZ + 1];

        // 現在のビットのマスク
        let mask = 1u128 << (BITSZ - 1 - i);

        // N未満の遷移
        trans_dp_less(
            &cur_dp_cnt,
            &cur_dp_tot,
            &mut next_dp_cnt,
            &mut next_dp_tot,
            mask,
        );

        // 現在のビットが1の場合のみ
        if n & mask != 0 {
            // Nの遷移
            trans_dp_equal(
                cur_pop_cnt,
                cur_num,
                &mut next_dp_cnt,
                &mut next_dp_tot,
            );

            // 現在の値を更新
            cur_pop_cnt += 1;
            cur_num += mask;
        }

        // 現在のDPと次のDPを交換
        swap(&mut cur_dp_cnt, &mut next_dp_cnt);
        swap(&mut cur_dp_tot, &mut next_dp_tot);
    }

    // N自身がpopcount = Kの場合を加算
    let mut result = cur_dp_tot[k];
    if cur_pop_cnt == k {
        result = (result + n) % MOD;
    }
    result
}

fn trans_dp_less(
    cur_dp_cnt: &Vec<usize>,
    cur_dp_tot: &Vec<u128>,
    next_dp_cnt: &mut Vec<usize>,
    next_dp_tot: &mut Vec<u128>,
    mask: u128,
) {
    for j in 0..BITSZ {
        // 0を選択時
        next_dp_cnt[j] += cur_dp_cnt[j];
        next_dp_tot[j] += cur_dp_tot[j];
        next_dp_tot[j] %= MOD;

        // 1を選択時
        next_dp_cnt[j + 1] += cur_dp_cnt[j];
        next_dp_tot[j + 1] += cur_dp_tot[j] + cur_dp_cnt[j] as u128 * mask;
        next_dp_tot[j + 1] %= MOD;
    }
}

fn trans_dp_equal(
    cur_pop_cnt: usize,
    cur_num: u128,
    next_dp_cnt: &mut Vec<usize>,
    next_dp_tot: &mut Vec<u128>,
) {
    // 0を選択時
    next_dp_cnt[cur_pop_cnt] += 1;
    next_dp_tot[cur_pop_cnt] += cur_num;
    next_dp_tot[cur_pop_cnt] %= MOD;
}

Discussion