ABC393:Rustで解く!問題解説
AtCoder Beginner Contest 393のA~F問題をRustで解いた際の解法をまとめました。
A問題
問題
解説
入力で与えられる
-
とS_1 がS_2 sick
の場合 -
がS_1 sick
、 がS_2 fine
の場合 -
がS_1 fine
、 がS_2 sick
の場合 -
とS_1 がS_2 fine
の場合
各場合に応じた出力を行います。
コード
use proconio::input;
fn main() {
input! {
s1: String,
s2: String
}
if s1 == "sick" && s2 == "sick" {
println!("1");
} else if s1 == "sick" && s2 == "fine" {
println!("2");
} else if s1 == "fine" && s2 == "sick" {
println!("3");
} else {
println!("4");
}
}
B問題
問題
解説
等間隔で選んだ3つの文字がA
, B
, C
となる組み合わせの総数を数えます。数え上げは以下1~3の手順で行います。
-
A
の位置を決める。 - 1で決めた位置から、j文字ずつ飛ばし(
)で2つ選び、その2つがj=1,2,... B
とC
である場合、適切な組み合わせとしてカウントします。 - 全ての
A
の位置に対して、2を行います。
コード
use proconio::{input, marker::Chars};
fn main() {
input! {
s: Chars,
}
let sz = s.len();
let mut ans = 0;
// 開始の'A'の位置を全探索
for i in 0..sz {
if s[i] != 'A' { continue; }
// j個飛ばしで全探索
for j in 0..sz {
if i + 2 * j >= sz { break; }
// 'A', 'B', 'C'になっている個数を数える
if s[i + j] == 'B' && s[i + 2 * j] == 'C' {
ans += 1;
}
}
}
println!("{}", ans);
}
C問題
問題
解説
頂点ごとの隣接リストを集合で管理しながら作成します。その際、以下のいずれかに該当する辺の本数を数えます。
- 自己ループの辺
- 一度見たことがある2頂点の組の辺
コード
use proconio::{input, marker::Usize1};
use std::collections::HashSet;
fn main() {
input! {
n: usize, m: usize,
}
// 追加しなかった辺の本数
let mut ans = 0;
// 無向グラフの隣接リストを作る。(同じ組の辺や、自己ループの辺は除く)
let mut uv = vec![HashSet::new(); n];
for _ in 0..m {
input! {
u: Usize1, v: Usize1,
}
// 同じ組の辺や、自己ループの辺の場合は、その本数を数える
if is_myself_loop(u, v) || is_contained_edge(u, v, &uv) {
ans += 1;
}
// 隣接リストに辺を追加
else {
add_edge(u, v, &mut uv);
}
}
println!("{}", ans);
}
// 自己ループの辺かどうかをチェック
fn is_myself_loop(u: usize, v: usize) -> bool {
u == v
}
// 既にある組の辺かどうかをチェック
fn is_contained_edge(u: usize, v: usize, uv: &Vec<HashSet<usize>>) -> bool {
if uv[u].contains(&v) { return true; }
if uv[v].contains(&u) { return true; }
false
}
// 辺の追加
fn add_edge(u: usize, v: usize, uv: &mut Vec<HashSet<usize>>) {
uv[u].insert(v);
uv[v].insert(u);
}
D問題
問題
解説
1の固まりをどこに持ってくるのが最適なのかを考えます。文字列
- 1の固まりを左端に配置した場合:左側にずらす操作は左から順に1 + 2 + 4で計7回。
- 1の固まりを左端から2番目に配置した場合:左側にずらす操作は左から順に0 + 1 + 3で計4回。
- 1の固まりを左端から3番目に配置した場合:左側にずらす操作は左から順に0 + 2、右側にずらす操作は1で計3回。
- 1の固まりを左端から4番目に配置した場合:左側にずらす操作は左から順に1、右側にずらす操作は2 + 1で計4回となります。
ここで①~④について、1の固まりの配置により操作回数がどのように変化するかに注目します。
- ①→②に移した際は左側にずらす操作が3回減少 → 計3回減少。
- ②→③に移した際は左側にずらす操作が2回減少し、右側にずらす操作が1回増加 → 計1回減少。
- ③→④に移した際は左側にずらす操作が1回減少し、右側にずらす操作が2回増加 → 計1回増加。
このように調べていくと、1の固まりは両端に近いほど操作回数が増加し、中央に近いほど操作回数が減少することがわかります。
したがって、「元の文字列
コード
use proconio::{input, marker::Chars};
fn main() {
input!{
n: usize,
s: Chars,
}
// 1の出現位置を取得
let pos_ones = get_pos_ones(n, &s);
// 1の固まりの左端を取得
let start_pos = get_left_pos(&pos_ones);
// 「元の文字列の各1」と「1の固まりの各1」を左側からマッチングさせる
// ずれている距離の絶対値を加算
let mut ans = 0;
for i in 0..pos_ones.len() {
ans += (start_pos + i).abs_diff(pos_ones[i]);
}
println!("{}", ans);
}
// 1の出現位置を取得
fn get_pos_ones(n: usize, s: &Vec<char>) -> Vec<usize> {
let mut vec = Vec::new();
for i in 0..n {
if s[i] == '1' {
vec.push(i);
}
}
vec
}
// 1の固まりの左端を取得
fn get_left_pos(ones: &Vec<usize>) -> usize {
// 1の出現位置の中央値が、1の固まりの中央となるように合わせる
// 1の個数が偶数の場合は中央値の左側とする。
let middle = (ones.len() - 1) / 2;
// 1の固まりの左端を取得
// 1の固まりの中央値から、middle分だけずらす
ones[middle] - middle
}
E問題
問題
解説
本問題において最大公約数が
-
値
をd 倍、1 倍、... と見ていき、各2 と一致する個数を数えます。A_i
→ この数え上げにより、各 がA_i の倍数となる個数が分かります。d -
1の結果に基づき、
個以上で割り切れる値K について、d 倍、1 倍、... と見た値をマークします。2
→ を定数倍した値は、いずれも割り切れる個数がd 個以上なので、この時に見たK の最大値を記録します。d
コード
use proconio::input;
use std::cmp::max;
const MAX_VALUE: usize = 1_000_000;
fn main() {
input! {
n: usize, k: usize,
a: [usize; n],
}
// 入力数列の各値の出現回数をカウント(1~MAX_VALUE)
let cnt_array = build_cnt_array(&a);
// 各候補約数 d (1~MAX_VALUE) について、入力数列中の d の倍数の個数を求める
let divisor_array = erato_divisor_array(&cnt_array);
// 各 x (1~MAX_VALUE) について、x を割り切る候補約数の中で、
// 入力数列中に k 個以上の数が割り切れる約数のうち最大のものを求める
let ans = erato_max_valid_divisor(&divisor_array, k);
// 各入力値について、条件を満たす最大の約数を出力
for &num in &a {
println!("{}", ans[num]);
}
}
// 入力された数列の各値の出現回数をカウントし、
// 1~MAX_VALUEの範囲で頻度配列を作成する
fn build_cnt_array(a: &[usize]) -> Vec<usize> {
let mut ret = vec![0; MAX_VALUE + 1];
for &num in a {
ret[num] += 1;
}
ret
}
// 各候補約数 d (1~MAX_VALUE) について、
// 入力数列中の d の倍数の個数を求める
fn erato_divisor_array(cnt_array: &[usize]) -> Vec<usize> {
let mut ret = vec![0; MAX_VALUE + 1];
for d in 1..=MAX_VALUE {
let mut multi = d;
let mut cnt = 0;
while multi <= MAX_VALUE {
cnt += cnt_array[multi];
multi += d;
}
ret[d] = cnt;
}
ret
}
// 各 x (1~MAX_VALUE) について、x を割り切る候補約数のうち、
// 入力数列中に k 個以上の数が割り切れる約数の中で最大のものを求める
// (x の約数で条件を満たす最大公約数を求める)
fn erato_max_valid_divisor(divisor_array: &[usize], k: usize) -> Vec<usize> {
let mut ret = vec![0; MAX_VALUE + 1];
// 各候補約数 d について、d が条件を満たすならば、その倍数全体に対して d を更新
for d in 1..=MAX_VALUE {
if divisor_array[d] < k { continue; }
let mut multi = d;
while multi <= MAX_VALUE {
ret[multi] = max(ret[multi], d);
multi += d;
}
}
ret
}
F問題
問題
解説
LIS(最長増加部分列)を求めるアルゴリズムを用いて問題を解きます。LISの計算には二分探索を用いる方法とセグメントツリーを使う方法がありますが、配列の要素制約が
LISを求める際には、以下のDPテーブルを用意し、都度更新を行います。
DP[i] : 最長増加部分列のi個目の値の最小値
与えられるクエリについては、整数列を一度昇順に並び替えて
コード
use proconio::{input, marker::Usize1};
fn main() {
input! {
n: usize, q: usize,
a: [usize; n],
}
// クエリ(index, 値範囲, 入力順)をindexの昇順に並べる
let mut query = Vec::new();
for i in 0..q {
input! {
// r:index, x:値範囲
r: Usize1, x: usize,
}
query.push((r, x, i));
}
query.sort();
// LISのDP: dp[最長増加部分列i個] = i個目の値の最小値
let mut dp = Vec::new();
// 処理したクエリの個数
let mut exe_cnt = 0;
// ans: (入力順、値)
let mut ans = Vec::new();
for i in 0..n {
// LISに要素を挿入
insert_lis(&mut dp, a[i]);
// 同じindexのクエリをすべて処理する
while exe_cnt < q && query[exe_cnt].0 == i {
// 値範囲以下の最長増加部分列の個数を取得
let pos2 = below_bound(&dp, query[exe_cnt].1);
if pos2 == INFS {
ans.push((query[exe_cnt].2, 1));
} else {
ans.push((query[exe_cnt].2, pos2 + 1));
}
exe_cnt += 1;
}
}
// クエリ順に並べ直して答える
ans.sort();
for i in 0..q {
println!("{}", ans[i].1);
}
}
// LISに要素を挿入
fn insert_lis(dp: &mut Vec<usize>, x: usize) {
// x以上で最も小さい値を置き換え、置き換えが出来ないなら値を追加
let pos = lower_bound(&dp, x);
if pos == dp.len() {
dp.push(x);
} else {
dp[pos] = x;
}
}
// 二分探索
// x以上の最小のposを返す
pub fn lower_bound<T: std::cmp::PartialOrd>(dp: &Vec<T>, val: T) -> usize {
let mut l = 0;
let mut r = dp.len();
while r - l > 0 {
let m = (l + r) / 2;
if dp[m] < val {
l = m + 1;
} else {
r = m;
}
}
l
}
const INFS: usize = 1 << 60;
// 二分探索
// x以下の最大のposを返す。0番目より小さい値では異常値として「1<<60」を返す
pub fn below_bound<T: std::cmp::PartialOrd>(dp: &Vec<T>, val: T) -> usize {
let mut l = 0;
let mut r = dp.len();
while r - l > 0 {
let m = (l + r) / 2;
if dp[m] <= val {
l = m + 1;
} else {
r = m;
}
}
if l == 0 { INFS } else { l - 1 }
}
Discussion