緑コーダーがRustで解説してみた(ABC420 A~E)
AtCoder Beginner Contest 420のA~E問題を緑コーダーが自分なりの解説と解答コードをまとめました。
参考になりましたら幸いです。
ABC420-A
問題
解説
現在の月
コード
use proconio::input;
fn main() {
// 入力
input! {
x: usize, // 現在の月
y: usize, // 加算する月数
}
let mut nx = x + y;
// 年をまたいだ場合は12を引く
if nx > 12 {
nx -= 12;
}
// 答えを出力
println!("{}", nx);
}
ABC420-B
問題
少数派に投票をすると得点が入るという問題です。
解説
各人の投票結果は縦に並んでいるので、1列ずつ調べていきます。各ラウンドごとに全員の投票を確認して、 0 と 1 のどちらが少数派かを判定し、少数派に投票した人に1点を加算します。
全ラウンド終了後、得点が最も高い人を出力します。
コード
use proconio::{input, marker::Chars};
fn main() {
// 入力
input! {
n: usize, // 人
m: usize, // ラウンド数
s: [Chars; n], // 投票情報
}
// 各人のスコア
let mut score = vec![0; n];
// ラウンド毎に見る
for round in 0..m {
// 0と1の個数を集計して、少数派の値を求める
let minority = get_minority_num(n, &s, round);
// 少数派の人に得点を加算
update_score(n, &s, round, &mut score, minority);
}
// スコアが最も高い人を全員出力
let max_score = *score.iter().max().unwrap();
for i in 0..n {
if score[i] == max_score {
print!("{} ", i + 1); // 1-indexで出力
}
}
println!();
}
// 少数派の値を判定する関数
fn get_minority_num(n: usize, s: &Vec<Vec<char>>, round: usize) -> usize {
let mut cnt0 = 0;
let mut cnt1 = 0;
for i in 0..n {
if s[i][round] == '0' {
cnt0 += 1;
} else {
cnt1 += 1;
}
}
if cnt0 < cnt1 { 0 } else { 1 }
}
// 少数派の人に得点を加算する関数
fn update_score(n: usize, s: &Vec<Vec<char>>, round: usize, score: &mut Vec<usize>, minority: usize) {
for i in 0..n {
if minority == 0 && s[i][round] == '0' {
score[i] += 1;
}
if minority == 1 && s[i][round] == '1' {
score[i] += 1;
}
}
}
ABC420-C
問題
数列A, Bの要素ごとの最小値の総和を求める問題です。
解説
各クエリで数列AまたはBの値が変更されるたびに、対応するインデックスの最小値を更新し、最小値の総和を効率的に計算する必要があります。最小値の総和は、以下1~3のようにして変更されたインデックスの差分のみを更新します。
- 更新前のインデックスの最小値を引きます。
- 更新箇所のインデックスの新しい最小値を求めます。
- 更新後のインデックスの最小値を加えます。
これにより、クエリごとの計算量が
コード
use proconio::{input, marker::Usize1};
use std::cmp::min;
fn main() {
// 入力
input! {
n: usize, // 長さN
q: usize, // クエリ数
mut a: [i64; n], // 数列A
mut b: [i64; n], // 数列B
}
// 数列a,Bのminリスト
let mut ab_min = vec![0; n];
// 数列A,Bのminの総和
let mut sum_min = 0;
// minリスト,minの総和を初期化
for i in 0..n {
ab_min[i] = min(a[i], b[i]);
sum_min += ab_min[i];
}
// クエリ処理
for _ in 0..q {
input! {
c: char, // 変更する数列(A,B)
x: Usize1, // 変更するindex
y: i64, // 変更する値
}
// 変更前のminを引く
sum_min -= ab_min[x];
// 値の更新
if c == 'A' {
a[x] = y;
}
else {
b[x] = y;
}
ab_min[x] = min(a[x], b[x]);
// 変更後のminを足す
sum_min += ab_min[x];
// 更新後の値を出力
println!("{}", sum_min);
}
}
ABC420-D
問題
スイッチマスの通過でドアの開閉が行われるグリッド上で、スタートマスからゴールマスへの最短距離を求める問題です。
解説
幅優先探索(BFS)を用いることで解くことができます。
スイッチマス ? を通過すると、現在の状態(スイッチを押しているか押していないか)が反転します。スイッチを押していない状態(はじめの状態)を「表面」、スイッチを押した状態を「裏面」として扱い、スタートマスからゴールマスまでの最短距離を求めます。通常のマス . では、状態を維持したまま移動し、スイッチマス ? では、状態を反転させて移動します。
ゴールマスに到達した際、表面と裏面のうち、より短い距離を答えとします。なお、ゴールマスに到達できない場合は -1 を出力します。
コード
use proconio::{input, marker::Chars};
use std::collections::VecDeque;
use std::cmp::min;
const INF: usize = 1 << 60;
const D4: [Position; 4] = [Position{x:!0, y:0}, Position{x:0, y:!0}, Position{x:1, y:0}, Position{x:0, y:1}]; // 上左下右
#[derive(Clone)]
struct Position {
x: usize,
y: usize,
}
fn main() {
input! {
h: usize, // グリッドの行
w: usize, // グリッドの列
s: [Chars; h], // グリッド
}
// スイッチoff/onのグリッド(2面)を初期化
let mut grid = vec![vec![vec!['.'; 2]; w]; h];
init_grid(h, w, &s, &mut grid);
// スタートとゴールの位置を取得
let spos = get_pos(h, w, &s, 'S');
let gpos = get_pos(h, w, &s, 'G');
// BFS
let dist_min = bfs(h, w, &grid, spos, gpos);
// ゴールに到達できない場合は-1
if dist_min == INF {
println!("-1");
} else {
// ゴールに到達できた場合はその値を出力
println!("{}", dist_min);
}
}
fn init_grid(h: usize, w: usize, s: &Vec<Vec<char>>, grid: &mut Vec<Vec<Vec<char>>>) {
for i in 0..h {
for j in 0..w {
// #や?は、on/off共通でその文字を入れる
if s[i][j] == '#' || s[i][j] == '?' {
grid[i][j][0] = s[i][j];
grid[i][j][1] = s[i][j];
}
// xは表面なら、#
if s[i][j] == 'x' {
grid[i][j][0] = '#';
}
// oは裏面なら、#
if s[i][j] == 'o' {
grid[i][j][1] = '#';
}
}
}
}
fn get_pos(h: usize, w: usize, s: &Vec<Vec<char>>, c: char) -> Position {
for i in 0..h {
for j in 0..w {
if s[i][j] == c {
return Position { x: i, y: j };
}
}
}
Position { x: 0, y: 0 }
}
fn bfs(
h: usize,
w: usize,
grid: &Vec<Vec<Vec<char>>>,
spos: Position,
gpos: Position,
) -> usize {
// 最短距離の初期化
let mut dist = vec![vec![vec![INF; 2]; w]; h];
dist[spos.x][spos.y][0] = 0;
// キューの初期化
let mut dq = VecDeque::new();
dq.push_back((spos, 0));
// BFSを実施
while let Some((cur_pos, cur_state)) = dq.pop_front() {
// 4方向移動を試す
for di in D4 {
let mut next_pos = Position { x: 0, y: 0 };
next_pos.x = cur_pos.x.wrapping_add(di.x);
next_pos.y = cur_pos.y.wrapping_add(di.y);
// グリッドの範囲外へは移動不可
if next_pos.x >= h || next_pos.y >= w {
continue;
}
// 壁へ移動は不可
if grid[next_pos.x][next_pos.y][cur_state] == '#' {
continue;
}
// .のマスへ移動する場合
if grid[next_pos.x][next_pos.y][cur_state] == '.' {
if dist[cur_pos.x][cur_pos.y][cur_state] + 1 < dist[next_pos.x][next_pos.y][cur_state] {
dist[next_pos.x][next_pos.y][cur_state] = dist[cur_pos.x][cur_pos.y][cur_state] + 1;
dq.push_back((next_pos.clone(), cur_state));
}
}
// ?のマスへ移動する場合
if grid[next_pos.x][next_pos.y][cur_state] == '?' {
let next_state = cur_state ^ 1;
if dist[cur_pos.x][cur_pos.y][cur_state] + 1 < dist[next_pos.x][next_pos.y][next_state] {
dist[next_pos.x][next_pos.y][next_state] = dist[cur_pos.x][cur_pos.y][cur_state] + 1;
dq.push_back((next_pos.clone(), next_state));
}
}
}
}
// 表面, 裏面のうち、より小さい最短距離を返す
min(dist[gpos.x][gpos.y][0], dist[gpos.x][gpos.y][1])
}
ABC420-E
問題
N頂点の無向グラフにおいて、黒色の頂点に到達できるかどうかを判定する問題です。
解説
Union-Find(素集合データ構造)とHashSet(集合)を用いて、以下の3種類のクエリを効率的に処理します。
-
頂点
とu を結ぶ無向辺を追加する。v
頂点 とu を連結します。この際、連結成分の親ノードが持つ黒色頂点の集合もマージします。v -
頂点
の色を白から黒、または黒から白に変更する。v
頂点 の色を反転させます。この際、連結成分の親ノードが持つ黒色頂点の集合も更新します。v -
頂点
が所属する連結成分に黒色の頂点が含まれるかを判定する。v
頂点 の連結成分に黒色の頂点が含まれるかを判定します。これは、連結成分の親ノードが持つ黒色頂点の集合に要素が1つ以上あるかを確認することで判断できます。v
コード
use proconio::{input, marker::Usize1};
use std::collections::HashSet;
use std::mem::swap;
fn main() {
// 入力
input! {
n: usize, // 頂点
q: usize, // クエリ数
}
// Union-Findの初期化
let mut uf = UnionFind::new(n);
// 各グループの親が持っている黒頂点の集合を管理
let mut black = vec![HashSet::new(); n];
// クエリ処理
for _ in 0..q {
input! {
nm: usize, // クエリ番号
}
if nm == 3 {
input! {
v: Usize1, // 頂点v(0-index)
}
// 黒頂点が1つ以上あるかどうかを判定する
let leader = uf.find(v);
println!("{}", if black[leader].len() >= 1 {"Yes"} else {"No"});
}
else if nm == 2 {
input! {
v: Usize1, // 頂点v(0-index)
}
// 頂点vの白と黒を入れ替え。各グループの親に反映する
let leader = uf.find(v);
if black[leader].contains(&v) {
black[leader].remove(&v);
}
else {
black[leader].insert(v);
}
}
else {
input! {
mut u: Usize1, // 頂点u(0-index)
mut v: Usize1, // 頂点v(0-index)
}
let leader_u = uf.find(u);
let leader_v = uf.find(v);
// 頂点uとvをマージ
if uf.merge(u, v) {
// 黒頂点集合のマージ
if uf.find(u) == leader_u {
// 親がuの場合は、vをマージ
let tmp = std::mem::take(&mut black[leader_v]);
black[leader_u].extend(tmp);
}
else {
// 親がvの場合は、uをマージ
let tmp = std::mem::take(&mut black[leader_u]);
black[leader_v].extend(tmp);
}
}
}
}
}
// Union-Find構造体
struct UnionFind {
size: Vec<usize>, // 各頂点の集合サイズ
parent: Vec<isize>, // 親情報
}
impl UnionFind {
// 初期化
fn new(n: usize) -> Self {
UnionFind { size: vec![1; n], parent: vec![-1; n] }
}
// 代表元を求める
fn find(&mut self, u: usize) -> usize {
if self.parent[u] == -1 {
return u;
}
let p = self.parent[u] as usize;
let root = self.find(p);
// 経路圧縮
self.parent[u] = root as isize;
root
}
// 2つの集合を併合
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);
}
self.parent[y] = x as isize;
self.size[x] += self.size[y];
true
}
// 集合のサイズを取得
#[allow(unused)]
fn size(&mut self, u: usize) -> usize {
let root = self.find(u);
self.size[root]
}
}
Discussion