ABC401:Rustで解く!問題解説
AtCoder Beginner Contest 401のA~E問題をRustで解いた際の解法をまとめました。
A問題
問題
解説
Success
を出力し、それ以外の場合は Failure
を出力します。
コード
use proconio::input;
fn main() {
// 入力
input! {
s: usize,
}
// 出力
println!("{}", if s >= 200 && s <= 299 { "Success" } else { "Failure" });
}
B問題
問題
解説
ログイン状態をフラグで管理しながら、与えられた操作を順にシミュレーションし、認証エラーが発生した( private
の操作がログアウト状態で実行された)回数を求めます。
入力に基づき、以下の処理を行います:
-
login
またはlogout
の入力を受け取った場合-
login
ならログイン状態を「有効」にします。 -
logout
ならログイン状態を「無効」にします。
-
-
private
の入力を受け取った場合- ログイン状態が「無効」であれば認証エラーとし、エラー回数をカウントします。
コード
use proconio::input;
fn main() {
// 入力
input! {
n: usize,
}
let mut is_login = false;
let mut err_cnt = 0;
for _ in 0..n {
// 各操作を入力
input! {
s: String,
}
if s == "login" {
is_login = true;
} else if s == "logout" {
is_login = false;
} else if s == "private" && !is_login {
err_cnt += 1;
}
}
// 出力
println!("{}", err_cnt);
}
C問題
問題
解説
以下の条件を満たす数列
- 初めの
項目まではK である。1 -
項目以降は、直前のK+1 項の和である。K
ただし、
直前の
- 初めの
項はすべてK なので、その分の累積和はそのまま足して初期化します。1 -
項目以降は、直前のK+1 項の和を利用して計算します。これは、直前の合計から最も古い値(K 項前)を引いた上で、新たな値を加えることで効率よく求めることができます。K
これにより、直前の
2.の計算について
まず、
ここで、
となります。
よって、
また、答えは
コード
use proconio::input;
const MOD: usize = 1_000_000_000;
fn main() {
// 入力
input! {
n: usize,
k: usize,
}
let mut dp = vec![0; n + 1];
let mut tot = 0;
for i in 0..=n {
if i < k {
// 初めのK項は1
dp[i] = 1;
tot += dp[i];
tot %= MOD;
} else {
// K+1項目以降
dp[i] = tot;
tot += dp[i] + MOD;
tot -= dp[i - k];
tot %= MOD;
}
}
println!("{}", dp[n]);
}
D問題
問題
解説
この問題では、長さ ?
を o
または .
に置き換えることで、以下の条件を満たすようにします。
o
の個数がちょうどK個
o
が連続しない
この問題を解くために、以下の手順で処理を行います。
-
前処理
文字列中のo
の両隣にある?
を.
に置き換えて、o
が連続しないようにします。 -
ランレングス圧縮
文字列を連続する文字ごとにまとめ、(文字, 長さ) のペアのリストを作成します。これにより、?
の連続部分やo
の個数を効率的に処理できます。 -
現在の
o
の個数と最大可能なo
の個数を計算
ランレングス圧縮した結果を基に、確定しているo
の個数と、?
を最大限o
に変えた場合の最大o
の個数を計算します。 -
場合分け
以下の3つのケースに分けて処理します。
(1)確定している o
の個数が
→ ?
をすべて .
に置き換えて出力します。
(2)確定している o
の個数と最大可能な o
の個数の合計が
→置き換えなくてよい o
が存在するので、o
の決め方が一意に定まりません。そのため、入力文字列の ?
は変更せずにそのまま出力します。
(3)確定している o
の個数と最大可能な o
の個数の合計がちょうど
→ ?
の連続部分毎の個数によって決まります。
(3)-1. ?
が連続する個数が奇数の場合
→ 最初を o
として、交互に o
と .
を割り当てると適切に置き換えができるので、?
を o
または .
に置き換えて出力します。
(3)-2. ?
が連続する個数が偶数の場合
→o
から始める場合と .
が始める場合の2パターン考えられるので、o
の決め方が一意に定まりません。そのため、入力文字列の ?
は変更せずにそのまま出力します。
これらの手順を整理することで、問題を効率的に解くことができます。
コード
use proconio::{input, marker::Chars};
fn main() {
input! {
n: usize,
k: usize,
s: Chars,
}
let mut ss = s.clone();
// 前処理: 'o' の両隣の '?' を '.' に変える
pre_change(&mut ss, n);
// ランレングス圧縮: 文字列を圧縮して (文字, 長さ) のペアのリストを作成
let rangs = ranglen(&ss);
// 確定している 'o' の個数を数える
let confirmed_o_cnt = get_confirm_o_cnt(&rangs);
// '?' を最大限 'o' に変えた場合の最大 'o' の個数を計算
let possible_o_max_cnt = get_possible_o_max_cnt(&rangs);
// 場合分けして出力
if confirmed_o_cnt == k {
// 確定している 'o' の数がちょうど k の場合
print_alternating_dot(&rangs);
} else if confirmed_o_cnt + possible_o_max_cnt > k {
// 確定している 'o' と '?' を最大限 'o' に変えると、 k を超える場合
print_compressed_string(&rangs);
} else if confirmed_o_cnt + possible_o_max_cnt == k {
// 確定している 'o' と '?' を最大限 'o' に変えてちょうど k の場合
print_alternating_o_and_dot(&rangs);
}
}
// 前処理: 'o' の両隣の '?' を '.' に変える
fn pre_change(string: &mut Vec<char>, length: usize) {
for i in 0..length {
if string[i] == 'o' {
if i > 0 && string[i - 1] == '?' {
string[i - 1] = '.';
}
if i < length - 1 && string[i + 1] == '?' {
string[i + 1] = '.';
}
}
}
}
// ランレングス圧縮: 文字列を (文字, 長さ) のペアのリストに変換
fn ranglen(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
}
// 確定している 'o' の個数を数える
fn get_confirm_o_cnt(rangs: &Vec<(char, usize)>) -> usize {
let mut ret = 0;
for &(c, l) in rangs {
if c == 'o' { ret += l; }
}
ret
}
// '?' を最大限 'o' に変えた場合の最大 'o' の個数を計算
fn get_possible_o_max_cnt(rangs: &Vec<(char, usize)>) -> usize {
let mut ret = 0;
for &(c, l) in rangs {
// '?' を交互に 'o' に変える
if c == '?' { ret += (l + 1) / 2; }
}
ret
}
// 圧縮された文字列をそのまま出力
fn print_compressed_string(rangs: &Vec<(char, usize)>) {
for &(char, length) in rangs {
for _ in 0..length {
print!("{}", char);
}
}
println!();
}
// '?' を'.'に置き換えて出力
fn print_alternating_dot(rangs: &Vec<(char, usize)>) {
for &(char, length) in rangs {
if char == '?' {
for _ in 0..length {
print!("{}", '.');
}
} else {
for _ in 0..length {
print!("{}", char);
}
}
}
println!();
}
// '?' を交互に 'o' と '.' に変えて出力
fn print_alternating_o_and_dot(rangs: &Vec<(char, usize)>) {
for &(char, length) in rangs {
if char == '?' && length % 2 == 1 {
for i in 0..length {
print!("{}", if i % 2 == 0 { 'o' } else { '.' });
}
} else {
for _ in 0..length {
print!("{}", char);
}
}
}
println!();
}
E問題
問題
解説
この問題は、頂点1から頂点
具体的には、以下の2つのUnion-Find構造を用意します。
- 頂点1から頂点
までを連結するためのUnion-Find。以下uf_smallとします。K - 頂点1から順番に見ていき、現時点で連結可能な頂点を管理するUnion-Find。以下uf_largeとします。
アルゴリズムの流れは以下の通りです:
- 各頂点について、その頂点から辿れる辺を順次処理します。
- uf_largeでは、現在の頂点番号より大きい番号の頂点とマージします。
- uf_smallでは、現在の頂点番号以下の頂点とマージします。
-
uf_smallが、頂点1から現在の頂点まで全て連結されていない場合、-1を出力します。
そうでない場合、uf_largeの連結サイズからuf_smallの連結サイズを引いた値が、不要な頂点の数となるので、その値を出力します。
コード
use proconio::{input, marker::Usize1};
use std::mem::swap;
fn main() {
// 入力
input! {
n: usize, m: usize,
}
// 無向グラフの辺
let mut edges = vec![Vec::new(); n];
for _ in 0..m {
input! {
u: Usize1, v: Usize1,
}
edges[u].push(v);
edges[v].push(u);
}
// Union-Findを2つ用意
let mut uf_small = UnionFind::new(n);
let mut uf_large = UnionFind::new(n);
for cpos in 0..n {
for &npos in &edges[cpos] {
if cpos < npos {
uf_large.merge(cpos, npos);
} else {
uf_small.merge(cpos, npos);
}
}
// uf_smallは1~cposまで全て連結でないといけない
if uf_small.size(cpos) != cpos + 1 {
println!("-1");
}
// uf_largeの連結サイズからuf_smallの連結サイズを引いたものが不要な頂点
else {
println!("{}", uf_large.size(cpos) - uf_small.size(cpos));
}
}
}
struct UnionFind {
size: Vec<usize>, // 各頂点のグラフサイズ
parent: Vec<isize>, // 親情報
}
impl UnionFind {
fn new(n : usize) -> Self {
UnionFind { size : vec![1; n], parent : vec![-1; n]}
}
// 代表値を求める。(経路圧縮も行う)
#[allow(unused)]
fn find(&mut self, u: usize) -> usize {
let mut mut_u = u;
if self.parent[mut_u] == -1 { return mut_u; }
let p = self.parent[mut_u] as usize;
let par = self.find(p);
self.parent[mut_u] = par as isize;
return self.parent[mut_u] as usize;
}
// 併合
fn merge(&mut self, u: usize, v: usize) -> bool {
// 代表値を取得
let mut x = self.find(u);
let mut y = self.find(v);
// 既に同じ集合
if x == y { return false; }
// 木のサイズが小さいものを大きいものに併合
if self.size[x] < self.size[y] {
swap(&mut x, &mut y);
}
// yをxの子とする
self.parent[y] = x as isize;
self.size[x] += self.size[y];
true
}
// 同じ集合かどうかを判定
#[allow(unused)]
fn same(&mut self, u: usize, v: usize) -> bool {
self.find(u) == self.find(v)
}
// 集合のサイズを求める
#[allow(unused)]
fn size(&mut self, u: usize) -> usize {
let x = self.find(u);
self.size[x]
}
}
Discussion