ABC408: Rustで解く!問題解説
AtCoder Beginner Contest 408のA-E問題をRustで解いた際の解法をまとめました。
A 問題
問題
解説
この問題は、
具体的には、最初に
コード
use proconio::input;
fn main() {
// 入力
input! {
n: usize, // 時刻の数
s: usize, // 寝るまでの秒数
t: [usize; n], // 肩をたたく時刻のリスト
}
let mut cur = 0; // 前回肩をたたいた時刻
let mut awake = true; // 起きているかどうかのフラグ
// 時刻差が s より大きい場合は寝ていると判定
for tt in t {
if tt - cur > s {
awake = false;
}
cur = tt;
}
// 起きていたかどうかを判定
println!("{}", if awake { "Yes" } else { "No" });
}
B 問題
問題
解説
長さ
順序付き集合(BTreeSet)に配列の要素を格納すると、重複なしで昇順に出来るのでこのデータ構造を利用して出力します。
コード
use std::collections::BTreeSet;
use itertools::Itertools;
use proconio::input;
fn main() {
// 入力
input! {
n: usize,
a: [usize; n],
}
// 順序付き集合に格納
let bst = BTreeSet::from_iter(a);
// 個数と要素を出力
println!("{}", bst.len());
println!("{}", bst.iter().join(" "));
}
C 問題
問題
解説
1からNまでのマスについて、砲台によって守られていない無防備なマスを作る際に、壊す必要がある砲台の数を最小化する問題です。
砲台が守る区間
imos法の手順は以下の通りです。
- 区間の開始位置に加算、終了位置の次に減算を行います。
cnt[l] ← cnt[l] + 1 cnt[r+1] ← cnt[r+1] - 1
- 配列全体に累積和を取ることで、各位置の最終的な値を計算します。
コード
use proconio::input;
use std::cmp::min;
fn main() {
// 入力
input! {
n: usize, // マスの数
m: i64, // 砲台の数
}
// 配列の初期化
let mut cnt = imos_init(n);
for _ in 0..m {
// 区間 [l, r] を取得
input! {
l: usize, r: usize,
}
// 区間の変化量をセット
imos_set(l, r, &mut cnt);
}
// 累積和を計算
imos_exe(&mut cnt, n);
// 1~Nの最小値を取得
let mut ans = m;
for i in 1..=n {
ans = min(ans, cnt[i]);
}
// 結果を出力
println!("{}", ans);
}
// imos法の初期化
fn imos_init(n: usize) -> Vec<i64> {
// 余裕を持たせて n+2 にする
vec![0; n + 2]
}
// 区間 [l, r] の変化量を設定
fn imos_set(l: usize, r: usize, cnt: &mut Vec<i64>) {
cnt[l] += 1;
cnt[r + 1] -= 1;
}
// 累積和を計算
fn imos_exe(cnt: &mut Vec<i64>, n: usize) {
for i in 0..n {
cnt[i + 1] += cnt[i];
}
}
D 問題
問題
解説
この問題では、長さ
この形式を達成するために、文字列を3つの状態に分けて考えます。
- 状態0:左側の0の区間
- 状態1:中央の1の区間
- 状態2:右側の0の区間
動的計画法(DP)を用いて、各文字を見たときにどの状態に遷移するかを考え、操作回数を最小化します。DPテーブル dp[cur][state]
は、文字列の先頭から cur
文字目までを見たときに、状態 state
(0, 1, 2)で終了するための最小操作回数を表します。
現在の文字(0,1)、現在の状態(0~2)と次に選択する文字(0,1)の計12通りについて、次の状態と必要操作回数を考えると以下表の通りです。
現在の文字 | 現在の状態 | 次の選択 | 次の状態 | 必要操作回数 |
---|---|---|---|---|
'0' | 状態0 | '0' | 状態0 | 0回 |
'0' | 状態0 | '1' | 状態1 | 1回 |
'0' | 状態1 | '0' | 状態1 | 1回 |
'0' | 状態1 | '1' | 状態2 | 0回 |
'0' | 状態2 | '0' | 状態2 | 0回 |
'0' | 状態2 | '1' | 遷移不可 | - |
'1' | 状態0 | '0' | 状態0 | 1回 |
'1' | 状態0 | '1' | 状態1 | 0回 |
'1' | 状態1 | '0' | 状態1 | 0回 |
'1' | 状態1 | '1' | 状態2 | 1回 |
'1' | 状態2 | '0' | 状態2 | 1回 |
'1' | 状態2 | '1' | 遷移不可 | - |
最後に、文字列全体を見た後の状態0, 1, 2の中で最小の操作回数を出力します。
コード
use proconio::{input, marker::Chars};
use std::cmp::min;
const INF: usize = 1 << 60;
fn main() {
// テストケース数の入力
input! {
t: usize,
}
for _ in 0..t {
solve();
}
}
fn solve() {
// 入力
input! {
n: usize,
s: Chars,
}
// DP[curまで見た][現在の状態0,1,2] = 操作回数の最小値
let mut dp = vec![vec![INF; 3]; n + 1];
dp[0][0] = 0;
for cur in 0..n {
for state in 0..=2 {
// 現在の状態を維持する場合
state_no_change(&mut dp, cur, state, s[cur]);
// 次の状態に遷移する場合
state_change(&mut dp, cur, state, s[cur]);
}
}
// 状態0, 1, 2の最小値を求める
let ans: usize = *dp[n].iter().min().unwrap();
// 結果を出力
println!("{}", ans);
}
// 現在の状態を維持する場合
fn state_no_change(dp: &mut Vec<Vec<usize>>, cur: usize, state: usize, c: char) {
// 操作コスト
let mut cost = 0;
// '1' を '0' に変える場合
if (state == 0 || state == 2) && c == '1' {
cost += 1;
}
// '0' を '1' に変える場合
else if state == 1 && c == '0' {
cost += 1;
}
// DPテーブルを更新
dp[cur + 1][state] = min(dp[cur + 1][state], dp[cur][state] + cost);
}
// 次の状態に遷移する場合
fn state_change(dp: &mut Vec<Vec<usize>>, cur: usize, state: usize, c: char) {
// 状態2からは遷移できない
if state == 2 {
return;
}
// 操作コスト
let mut cost = 0;
// '0' を '1' に変える場合
if state == 0 && c == '0' {
cost += 1;
}
// '1' を '0' に変える場合
else if state == 1 && c == '1' {
cost += 1;
}
// DPテーブルを更新
dp[cur + 1][state + 1] = min(dp[cur + 1][state + 1], dp[cur][state] + cost);
}
E 問題
問題
解説
頂点1から頂点Nまでの単純パスにおいて、経由する辺の重みの
解法としては、移動を許可するビットの集合を管理し、上位ビットから順に特定のビットを禁止しても頂点1から頂点Nまで移動可能かを調べます。移動可否の判定には深さ優先探索(DFS)を用います。
具体的には以下の手順で解きます。
- 初期状態ではすべてのビットを許可します。
- 上位ビットから順に、特定のビットを禁止しても移動可能かをDFSで判定します。
- 移動可能であれば、そのビットを禁止(不要)とします。
- 最後まで判定した後、残ったビットの集合が答えとなります。
コード
use proconio::{input, marker::Usize1};
fn main() {
// 入力
input! {
n: usize, m: usize,
}
// 無向グラフの構築
let mut graph = vec![Vec::new(); n];
for _ in 0..m {
input! {
u: Usize1, v: Usize1, w: usize,
}
graph[u].push((v, w));
graph[v].push((u, w));
}
// 必要なビットの集合を初期化(全ビットを許可)
let mut need_mask = (1 << 30) - 1;
// 上位ビットから順に調べる
for bit in (0..30).rev() {
// 禁止するビットを試す
let ban_bit = 1 << bit;
// 禁止しても移動可能なら、そのビットは不要
if dfs(0, n, &graph, need_mask ^ ban_bit, &mut vec![false; n]) {
need_mask ^= ban_bit;
}
}
// 結果を出力
println!("{}", need_mask);
}
// 深さ優先探索(DFS)で移動可能かを判定
fn dfs(
cpos: usize, n: usize,
graph: &Vec<Vec<(usize, usize)>>, allow_mask: usize,
used: &mut Vec<bool>,
) -> bool {
// 現在の頂点を訪問済みにする
used[cpos] = true;
// 頂点Nに到達した場合
if cpos == n - 1 {
return true;
}
// 次の頂点を探索
for &(npos, weight) in &graph[cpos] {
// 訪問済み、または許可されていないビットを含む場合はスキップ
if used[npos] || (weight & allow_mask != weight) {
continue;
}
// 再帰的に探索
if dfs(npos, n, graph, allow_mask, used) {
return true;
}
}
false
}
Discussion