ABC392:Rustで解く!問題解説
AtCoder Beginner Contest 392のA~F問題をRustで解いた際の解法をまとめました。
A問題
問題
解説
整数列
コード
use proconio::input;
fn main() {
input!{
mut a: [usize; 3],
}
a.sort();
println!("{}", if a[0] * a[1] == a[2] {"Yes"} else {"No"});
}
B問題
問題
解説
- 差集合の要素数
- 差集合の各要素
コード
use proconio::input;
use std::{collections::BTreeSet, iter::FromIterator};
use itertools::Itertools;
fn main() {
input!{
n: usize, m: usize,
a: [usize; m],
}
// 1 ~ Nの集合1
let mut st_1 = BTreeSet::new();
for i in 1..=n { st_1.insert(i); }
// 数列aの集合2
let st_2 = BTreeSet::from_iter(a.iter().cloned());
// 差集合
let st = &st_1 - &st_2;
// 差集合の要素数と各要素を出力
println!("{}", st.len());
println!("{}", st.iter().join(" "));
}
C問題
問題
解説
問題文で求められている以下の内容について考えます。
i が書かれたゼッケンを着けている人が見つめている人の着けているゼッケンにかかれている数
上記を整理すると、以下の3点がポイントになります。これらを順番に処理していきます。
- ゼッケン
を着けている人i x - 人
が見つめている人x y - 人
が着けているゼッケンの番号y z
これに対し、人
- 人
が見つめている人i → 上記の2番目に相当P_i - 人
が着けているゼッケンの番号i → 上記の3番目に相当Q_i
ここで、入力の内容を逆に見ると、さらに以下のことが言えます。
- 人
を見ている人P_i i - ゼッケンの番号
を着けている人Q_i → 上記の1番目に相当i
したがって、人
コード
use itertools::Itertools;
use proconio::{input, marker::Usize1};
fn main() {
input! {
n: usize,
p: [Usize1; n],
q: [Usize1; n],
}
// ①ゼッケンの番号Q_iを着けている人i
let mut zekken_info = vec![0; n];
for i in 0..n {
zekken_info[q[i]] = i;
}
// ②人iが見つめている人P_i
let mut stare_info = vec![0; n];
for i in 0..n {
stare_info[i] = p[i];
}
// ③人iが着けているゼッケンの番号Q_i
let mut wear_info = vec![0; n];
for i in 0..n {
wear_info[i] = q[i];
}
// ①→②→③と変換
let mut ans = Vec::new();
for i in 0..n {
let mut val = zekken_info[i];
val = stare_info[val];
val = wear_info[val];
ans.push(val + 1);
}
println!("{}", ans.iter().join(" "));
}
D問題
問題
解説
各サイコロについて、
これをすべてのパターンで試し、その中で最も大きい値を出力します。
コード
use proconio::input;
const SZ: usize = 100_000;
fn main() {
input! {
n: usize,
}
// 確率リスト
// px_list[サイコロi個目][出目j] = 確率
let mut px_list = vec![vec![0.0; SZ + 1]; n];
for i in 0..n {
input! {
k: usize,
a: [usize; k]
}
px_list[i] = get_px_data(k, &a);
}
// サイコロ2個を全探索
// ぞろ目確率の合計を計算し、最大値を求める
let mut ans = 0.0;
for i in 0..n {
for j in i + 1..n {
let tot_px = calc_px(i, j, &px_list);
ans = f64_max(ans, tot_px);
}
}
println!("{}", ans);
}
fn get_px_data(k: usize, a: &Vec<usize>) -> Vec<f64> {
// 出目ごとの個数を数える
let mut cnt = vec![0; SZ + 1];
for &aa in a {
cnt[aa] += 1;
}
// 出目ごとの確率に変換する
let mut px_data = vec![0.0; SZ + 1];
for i in 1..=SZ {
px_data[i] = cnt[i] as f64 / k as f64;
}
px_data
}
fn calc_px(i: usize, j: usize, px_list: &Vec<Vec<f64>>) -> f64 {
// ぞろ目の確率を合計
let mut tot = 0.0;
for k in 0..=SZ {
tot += px_list[i][k] * px_list[j][k];
}
tot
}
fn f64_max(a: f64, b: f64) -> f64 {
if a >= b { return a; }
b
}
E問題
問題
解説
サーバーを頂点、ケーブルを辺とするグラフを考え、すべての頂点を行き来できる全域木を作成します。全域木の作成にはUnion-Findというデータ構造を使用します。
まず、
なお、①については、頂点
コード
use proconio::{input, marker::Usize1};
use std::mem::swap;
fn main() {
input!{
n: usize, m: usize,
}
// Union-Find 構造体の初期化
let mut uf = UnionFind::new(n);
// 連結不要な辺を記録する(辺の番号、始点)
let mut edge = Vec::new();
// 連結が必要な辺をすべてマージする
for i in 0..m {
input!{
u: Usize1, v: Usize1,
}
// 同じグループであれば不要な辺を記録する
if uf.same(u, v) {
edge.push((i, u));
}
// 異なるグループであればマージする
else {
uf.merge(u, v);
}
}
// 既に見た頂点を管理するための変数
let mut epos = 0;
// 追加でマージした辺の情報を保存するベクター
let mut merge_edge_info = Vec::new();
// 記録した辺を使用してマージを行う
for (idx, spos) in edge {
// 全頂点が全域木になるか、マージが必要な頂点が見つかるまでループ
while epos < n && uf.same(spos, epos) { epos += 1; }
// 全域木になっている場合はループを抜ける
if epos == n { break; }
// マージを行う
uf.merge(spos, epos);
merge_edge_info.push((idx, spos, epos));
}
// 追加でマージした辺の本数と、その辺の情報を出力する
println!("{}", merge_edge_info.len());
for (idx, spos, epos) in merge_edge_info {
println!("{} {} {}", idx + 1, spos + 1, epos + 1);
}
}
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;
// xの親を根に付け替える
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);
// 既に同じ集合であれば false を返す
if x == y { return false; }
// 木のサイズが小さいものを大きいものに併合する (Union by size)
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
}
// 2つの頂点が同じ集合かどうかを判定
#[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]
}
}
F問題
問題
解説
配列に要素を insert
で一つずつ追加するだけの単純な問題に見えますが、insert()
1回あたりの計算量は
このため、実行時間制限に間に合わないため、他の方法で解く必要があります。
具体的には、挿入操作の順番を逆にし、「値が未格納の配列」の先頭から
二分探索とセグメントツリーの計算量はそれぞれ
コード
use proconio::input;
use itertools::Itertools;
fn main() {
input! {
n: usize,
p: [usize; n],
}
// セグメントツリーの初期化(0:値格納済み、1:値未格納)
let mut sg = SegmentTree::new(n + 1, 0, combine_func);
for i in 1..=n { sg.set(i, 1); }
let mut ans = vec![0; n + 1];
// pを後ろから処理する。
for i in (1..=n).rev() {
// セグメントツリー上を二分探索して、
// 未格納の数が丁度p[i]個の位置を見つける
let pos = find_lower_bound(&mut sg, 1, n, p[i - 1]);
ans[pos] = i;
// p[i]を格納済みにする。
sg.set(pos, 0);
}
// 配列の2番目以降を出力
println!("{}", ans.iter().skip(1).join(" "));
}
// セグメントツリーライブラリ
struct SegmentTree<F, T> {
length: usize, // 入力の長さ
n: usize, // 列の長さ
array: Vec<T>, // 二分木
identity_e: T, // 単位元(0:合計、max、1:積、INF:min)
combine_f: F, // 評価関数
}
impl<F: Fn(T, T) -> T, T: Copy + std::fmt::Display> SegmentTree<F, T> {
// 初期化
#[allow(unused)]
fn new(length: usize, identity_e: T, combine_f: F) -> Self {
let mut n = 1;
while n < length { n <<= 1; }
SegmentTree { length, n, array: vec![identity_e; 2 * n], identity_e, combine_f }
}
// [1点更新] 位置 index (0-indexed) を値 value で更新
#[allow(unused)]
fn set(&mut self, index: usize, val: T) {
let mut i = self.n + index;
self.array[i] = val;
while i > 1 {
i >>= 1;
self.array[i] = (self.combine_f)(
self.array[i << 1 | 0],
self.array[i << 1 | 1],
);
}
}
// [1点取得] 位置 index (0-indexed) 内の値を取得
#[allow(unused)]
fn get(&mut self, index: usize) -> T {
let mut l = index + self.n;
let mut r = index + 1 + self.n;
let mut val_l = self.identity_e;
let mut val_r = self.identity_e;
while l < r {
if l & 1 != 0 {
val_l = (self.combine_f)(val_l, self.array[l]);
l += 1;
}
if r & 1 != 0 {
r -= 1;
val_r = (self.combine_f)(self.array[r], val_r);
}
l >>= 1; r >>= 1;
}
(self.combine_f)(val_l, val_r)
}
// [区間取得] 区間 [l, r) (0-indexed) 内の要素について
// l 番目から順に、 combine_f を適応した結果を返す
#[allow(unused)]
fn prod(&mut self, lindex: usize, rindex: usize) -> T {
let mut l = lindex + self.n;
let mut r = rindex + self.n;
let mut val_l = self.identity_e;
let mut val_r = self.identity_e;
while l < r {
if l & 1 != 0 {
val_l = (self.combine_f)(val_l, self.array[l]);
l += 1;
}
if r & 1 != 0 {
r -= 1;
val_r = (self.combine_f)(self.array[r], val_r);
}
l >>= 1; r >>= 1;
}
(self.combine_f)(val_l, val_r)
}
#[allow(unused)]
pub fn dbg_print(&mut self) {
for i in 0..self.length {
print!("{} ", self.get(i));
}
println!();
}
}
// 評価関数
pub fn combine_func(a: usize, b: usize) -> usize {
a + b
}
#[allow(unused)]
// セグメントツリー上の二分探索
fn find_lower_bound(
sg: &mut SegmentTree<impl Fn(usize, usize) -> usize, usize>, // セグメントツリー
lpos: usize, // 左閉区間
rpos: usize, // 右開区間
val: usize // 値
) -> usize {
let mut ng = lpos - 1;
let mut ok = rpos;
while ok - ng > 1 {
let mid = (ok + ng) / 2;
// 右開区間で取得する
let x = sg.prod(lpos, mid + 1);
if x >= val {
ok = mid;
} else {
ng = mid;
}
}
ok
}
Discussion