AtCoder Regular Contest 170 (ARC170) (AからEまで) 復習

Rustで競プロするのに慣れたりライブラリを整備したりするのも兼ねて、ARC170を解き直します。
コンテスト時はA~Bの2完でした。
2完した時点で既にac-predictorが水パフォを示していたので必死にC問題を解こうとしましたが解けず…
Aが遅かったのでしょうがないですね。

自作ライブラリを使用し、ローカルで事前に明らかに未使用な部分を除いて展開する形で提出しています。展開前のソースコードを記述します。
AtCoderで使えるクレートに含まれない関数などを使っている可能性があること、破壊的変更をこの記事の投稿後にしている可能性があることにご注意ください。
github.com

A問題

atcoder.jp
こういう問題本当に苦手です…とはいえ44分+3ペナは普通に遅いので反省。

まず最小回数を求めるのではなく、S=Tとできる条件を考えたほうがよかったと思います。
これは、明らかに「全てのBからAに変えたい部分の右に、最終的にBとなる部分がある」かつ「全てのAからBに変えたい部分の左に、最終的にAとなる部分がある」を満たすことです。
満たさないものを弾いたうえで、なるべくAからBに変えたい部分とBからAに変えたい部分は同時に操作したいです。操作できる組の個数は括弧列の要領でstackを持ちながら数えればよいです。そして、今試しているものは一致できることがわかっているので、組にならなかったものの個数を足したものが最小の操作回数となります。

また、こういう問題は考察に悩むくらいならランダムテストを書いたほうがよいということを学びました。
今回は明らかに同一のi,jに対して複数回操作をするのが無駄なので、順列全探索で楽にランダムテストを書くことができます。
そして、デバッグビルドであっても2\leq N\leq 4くらいなら100ms程度で全て確認してくれます。
これで提出デバッグを防ぐことができます。
atcoder.jp

#[allow(unused_attributes)] #[macro_use] #[allow(unused_imports)] use not_leonian_ac_lib::*;
use proconio::marker::Chars;
use superslice::Ext;

fn main() {
    /// 標準入力のマクロ(インタラクティブ問題ならば中のマクロを変更)
    macro_rules! input {
        ($($tt:tt)*) => {
            proconio::input!($($tt)*);
            // proconio::input_interactive!($($tt)*);
        };
    }

    input!(n:usize,s:Chars,t:Chars);
    let ans=solve(n, &s, &t);
    outputln!(ans);

    #[cfg(debug_assertions)]
    for i in 2..5 {
        for j in 0..1<<i {
            let mut s=Vec::new();
            for jj in 0..i {
                if (j>>jj)%2==0 {
                    s.push('A');
                } else {
                    s.push('B');
                }
            }
            for k in 0..1<<i {
                let mut t=Vec::new();
                for kk in 0..i {
                    if (k>>kk)%2==0 {
                        t.push('A');
                    } else {
                        t.push('B');
                    }
                }
                assert_eq!(solve(i, &s, &t), test(i, &s, &t));
            }
        }
    }
}

fn solve(n:usize,s:&Vec<char>,t:&Vec<char>) -> isize {
    let mut exists_a=false;
    for i in 0..n {
        if t[i]=='A' {
            exists_a=true;
        }
        if !exists_a && s[i]=='A' && t[i]=='B' {
            return -1;
        }
    }
    let mut exists_b=false;
    for i in (0..n).rev() {
        if t[i]=='B' {
            exists_b=true;
        }
        if !exists_b && s[i]=='B' && t[i]=='A' {
            return -1;
        }
    }
    let mut ans=0;
    let mut stack=Vec::new();
    for i in 0..n {
        if s[i]=='B' && t[i]=='A' {
            stack.push(i);
        }
        if s[i]=='A' && t[i]=='B' {
            if let Some(_)=stack.pop() {}
            ans+=1;
        }
    }
    ans+=stack.len();
    ans as isize
}

fn test(n:usize,s:&Vec<char>,t:&Vec<char>) -> isize {
    let mut p=Vec::new();
    if s==t {
        return 0;
    }
    for i in 0..n {
        for j in i+1..n {
            p.push((i,j));
        }
    }
    let mut ans=usize::MAX;
    do_while!(p.next_permutation(),{
        let mut ss=s.clone();
        for i in 0..p.len() {
            ss[p[i].0]='A';
            ss[p[i].1]='B';
            if ss==*t {
                ans.chmin(i+1);
                break;
            }
        }
    });
    if ans<usize::MAX {
        ans as isize
    } else {
        -1
    }
}

B問題

atcoder.jp
そんなに遅くないスピードで解けたのですが、Aの遅さは流石に巻き返せませんでした。

公式解説と異なる方法ですが、悪くない方法だと思います。
事前にsetのベクタでそれぞれ\{i\ :\ A_i=k\}を管理しておきます。
まず、lを固定します。等差数列i,j,kを全探索すると、setを用いてlかその右の最も左のiの位置がわかり、その位置より右の最も左のjの位置がわかり、その位置より右の最も左のkの位置がわかります。これがlに対する、条件を満たすrの最小値となり、それ以上のrは全て条件を満たします。
よって、そのような区間の個数の総和をとればよいです。

時間計算量のオーダーは\max AM、等差数列の項数をKとして、lの個数が\Theta(N)、等差数列の個数が\Theta(M^2/K)rを見つける時間計算量が\Theta(K\log N)であることより、\Theta(NM^2\log N)となるはずです。空間計算量を\Theta(N)から\Theta(NM)にすれば容易に時間計算量を\Theta(NM^2)に落とせるため、公式解説と遜色ない計算量になっていると思います。
atcoder.jp

use std::collections::BTreeSet;

#[allow(unused_attributes)] #[macro_use] #[allow(unused_imports)] use not_leonian_ac_lib::*;
use proconio::marker::Usize1;

fn main() {
    /// 標準入力のマクロ(インタラクティブ問題ならば中のマクロを変更)
    macro_rules! input {
        ($($tt:tt)*) => {
            proconio::input!($($tt)*);
            // proconio::input_interactive!($($tt)*);
        };
    }

    input!(n:usize,a:[Usize1;n]);
    let mut ans=0usize;
    let mut ss=vec![BTreeSet::new();10];
    for i in 0..n {
        ss[a[i]].insert(i);
    }
    for l in 0..n {
        let mut r=n;
        for d in 1..=4 {
            for i in 0..10-2*d {
                let j=i+d;
                let k=j+d;
                if let Some(&ip)=ss[i].range(l..).next() {
                    if let Some(&jp)=ss[j].range(ip+1..).next() {
                        if let Some(&kp)=ss[k].range(jp+1..).next() {
                            r.chmin(kp);
                        }
                    }
                }
            }
        }
        for i in 0..10 {
            if let Some(&ip)=ss[i].range(l..).next() {
                if let Some(&jp)=ss[i].range(ip+1..).next() {
                    if let Some(&kp)=ss[i].range(jp+1..).next() {
                        r.chmin(kp);
                    }
                }
            }
        }
        for d in 1..=4 {
            for k in 0..10-2*d {
                let j=k+d;
                let i=j+d;
                if let Some(&ip)=ss[i].range(l..).next() {
                    if let Some(&jp)=ss[j].range(ip+1..).next() {
                        if let Some(&kp)=ss[k].range(jp+1..).next() {
                            r.chmin(kp);
                        }
                    }
                }
            }
        }
        ans+=n-r;
    }
    outputln!(ans);
}

C問題

atcoder.jp
コンテスト中惜しいところまでいったと思っていたんですが、改めて考えると惜しくなかったです…
mexで種類数に注目するの、確かにだいぶ前に見たかもなぁ…

mexではなく種類数をDPテーブルでもつことで2次元DPになります。
S_i=1の場合、元から全種類埋まっている場合を除いてA_iは一意に定まり、種類数は1増えます。
一方、S_i=0で種類数がjの場合、元からあるj個を選べば種類数は増えません。また、残りのうちmexとなる1個を除いた(M-j)個を選べば種類数は1増えます。
これに気付くことができれば、あとは単純なDPになります。

(サンプルが弱いので凡ミスしてもだいたいサンプルは通ってしまうことに注意…)
atcoder.jp

#[allow(unused_attributes)] #[macro_use] #[allow(unused_imports)] use not_leonian_ac_lib::*;

fn main() {
    /// 標準入力のマクロ(インタラクティブ問題ならば中のマクロを変更)
    macro_rules! input {
        ($($tt:tt)*) => {
            proconio::input!($($tt)*);
            // proconio::input_interactive!($($tt)*);
        };
    }

    input!(n:usize,m:usize,s:[usize;n]);
    let max=min(n, m+1);
    let mut dp=vec![vec![Mint::new(0);max+1];n+1];
    dp[0][0]+=1;
    for i in 0..n {
        for j in 0..=max {
            if s[i]==1 {
                if j<max {
                    dp[i+1][j+1]=dp[i+1][j+1]+dp[i][j];
                }
            } else {
                dp[i+1][j]=dp[i+1][j]+dp[i][j]*j;
                if j<max {
                    dp[i+1][j+1]=dp[i+1][j+1]+dp[i][j]*(m-j);
                }
            }
        }
    }
    outputln!(dp[n].sum());
}

D問題

atcoder.jp
やることは丁寧な言い換えなのですが、upsolve中にWAを連発してしまいました。
こういう問題はむしろ本番のように集中しているときのほうが解けるかもしれませんね。

Bobが勝つということを言い換えていきます。
\begin{align*}\text{Bobが勝つ}&\Leftrightarrow\forall a\in A.\exists b\in B.\forall c\in A\setminus\{a\}.\lnot(\text{辺の長さがそれぞれ}a,b,c\text{の非退化な三角形が存在する})\\&\Leftrightarrow\forall a\in A.\exists b\in B.\forall c\in A\setminus\{a\}.\lnot(|a-b|\lt c\lt a+b)\\&\Leftrightarrow\forall a\in A.( \exists b\in \{b\in B\ |\ a\geq b\}.\forall c\in A\setminus\{a\}.\lnot(a-b\lt c\lt a+b)\lor\exists b\in \{b\in B\ |\ a\lt b\}.\forall c\in A\setminus\{a\}.\lnot(b-a\lt c\lt a+b) )\end{align*}
以降、\{b\in B\ |\ a\geq b\}=B_{a\geq},\ \{b\in B\ |\ a\lt b\}=B_{a\lt}とします。
b_1\leq b_2\ (b_1,b_2\in B_{a\geq})について、a-b_1\lt c\lt a+b_1ならばa-b_2\lt c\lt a+b_2なので、以下の言い換えが成立します。
\exists b\in B_{a\geq}.\forall c\in A\setminus\{a\}.\lnot(a-b\lt c\lt a+b)\Leftrightarrow\forall c\in A\setminus\{a\}.(B_{a\geq}\neq\varnothing\land\lnot(a-\min{B_{a\geq}}\lt c\lt a+\min{B_{a\geq}}))
また、\lnot(b-a\lt c\lt a+b)|b-c|\geq aと言い換えられます。
ここで、\exists b\in B_{a\geq}.\forall c\in A\setminus\{a\}.\lnot(b-a\lt c\lt a+b)が成立するならば、\exists b\in B_{a\geq}.\forall c\in A\setminus\{a\}.\lnot(a-b\lt c\lt a+b)も成立します。
これらを合わせると、Bobが勝つということはさらに言い換えられます。
\begin{align*}\text{Bobが勝つ}&\Leftrightarrow\forall a\in A.( \exists b\in B_{a\geq}.\forall c\in A\setminus\{a\}.\lnot(a-b\lt c\lt a+b)\lor\exists b\in B_{a\lt}.\forall c\in A\setminus\{a\}.\lnot(b-a\lt c\lt a+b) )\\&\Leftrightarrow\forall a\in A.( \forall c\in A\setminus\{a\}.(B_{a\geq}\neq\varnothing\land\lnot(a-\min{B_{a\geq}}\lt c\lt a+\min{B_{a\geq}}))\lor\exists b\in B_{a\lt}.\forall c\in A\setminus\{a\}.|b-c|\geq a\lor\exists b\in B_{a\geq}.\forall c\in A\setminus\{a\}.|b-c|\geq a )\\&\Leftrightarrow\forall a\in A.( \forall c\in A\setminus\{a\}.(B_{a\geq}\neq\varnothing\land\lnot(a-\min{B_{a\geq}}\lt c\lt a+\min{B_{a\geq}}))\lor\exists b\in B.\forall c\in A\setminus\{a\}.|b-c|\geq a )\end{align*}
最後に、\min,\ \maxの性質や不等式の変形を使ってさらに言い換えると以下のようになります。
\displaystyle\text{Bobが勝つ}\Leftrightarrow\forall a\in A.( (B_{a\geq}\neq\varnothing\land\min_{c\in A\setminus\{a\}}|a-c|\geq \min B_{a\geq})\lor\max_{b\in B}\min_{c\in A\setminus\{a\}}|b-c|\geq a )
前者は、ソートしたときにaと隣接するAの要素について見ることで容易に判定できます。
後者は、各bに最も近いAの要素および次に近いAの要素などをもっておき、\min_{c\in A\setminus\{a\}}|b-c|を(多重)集合で管理することで判定できます。
atcoder.jp

#[allow(unused_attributes)] #[macro_use] #[allow(unused_imports)] use not_leonian_ac_lib::*;

fn main() {
    /// 標準入力のマクロ(インタラクティブ問題ならば中のマクロを変更)
    macro_rules! input {
        ($($tt:tt)*) => {
            proconio::input!($($tt)*);
            // proconio::input_interactive!($($tt)*);
        };
    }

    input!(t:usize);
    for _ in 0..t {
        input!(n:usize,mut a:[usize;n],mut b:[usize;n]);
        a.sort();
        b.sort();
        let mut bob=true;
        let mut badiffs=vec![Vec::new();n];
        let mut minbidx=vec![Vec::new();n];
        let mut bminmset=MultiSet::new();
        for i in 0..n {
            let j=usize_binary_search(n, false, |mid| a[mid]>=b[i]);
            if j<n {
                badiffs[i].push((a[j]-b[i],j));
            }
            if j+1<n {
                badiffs[i].push((a[j+1]-b[i],j+1));
            }
            if j>0 {
                badiffs[i].push((b[i]-a[j-1],j-1));
            }
            if j>1 {
                badiffs[i].push((b[i]-a[j-2],j-2));
            }
            badiffs[i].sort();
            while badiffs[i].len()>2 {
                badiffs[i].pop();
            }
            minbidx[badiffs[i][0].1].push(i);
            bminmset.insert_one(badiffs[i][0].0);
        }
        for i in 0..n {
            if a[i]>=b[0] {
                let mut min=usize::MAX;
                if i>0 {
                    min.chmin(a[i]-a[i-1]);
                }
                if i<n-1 {
                    min.chmin(a[i+1]-a[i]);
                }
                if min>=b[0] {
                    continue;
                }
            }
            for &idx in &minbidx[i] {
                bminmset.remove_one(badiffs[idx][0].0);
                if badiffs[idx].len()>1 {
                    bminmset.insert_one(badiffs[idx][1].0);
                }
            }
            if *bminmset.last_key_value().unwrap().0<a[i] {
                bob=false;
                break;
            }
            for &idx in &minbidx[i] {
                if badiffs[idx].len()>1 {
                    bminmset.remove_one(badiffs[idx][1].0);
                }
                bminmset.insert_one(badiffs[idx][0].0);
            }
        }
        outputif(bob, "Bob", "Alice");
    }
}

E問題

Dまでのupsolveで結構日数を使ってしまったことと、自分がまともに闘えるレベル帯の問題ではないことから、Berlekamp-MasseyアルゴリズムとBostan-Moriアルゴリズムの練習のみします。

まず、Nについての答えの列が線形漸化式になることを願います。そして、漸化式中の項数が多くないことも願います。(この問題で線形漸化式であると願うのは神の一手すぎる)
そのうえで、3\leq N\lt 17くらいまではビット全探索とVecDequeを用いた愚直なシミュレーションでも答えを全て出すことができます。
Berlekamp-MasseyアルゴリズムとBostan-Moriアルゴリズムを用いると、シミュレーションで出した答えから問題の答えを高速に求めることができます。えぇ…

一応、Berlekamp-MasseyアルゴリズムとBostan-Moriアルゴリズムはライブラリに追加しておきました。
atcoder.jp

use std::collections::VecDeque;

#[allow(unused_attributes)] #[macro_use] #[allow(unused_imports)] use not_leonian_ac_lib::*;

const MAX:usize=14;

fn main() {
    /// 標準入力のマクロ(インタラクティブ問題ならば中のマクロを変更)
    macro_rules! input {
        ($($tt:tt)*) => {
            proconio::input!($($tt)*);
            // proconio::input_interactive!($($tt)*);
        };
    }

    let mut prefix=vec![vec![Mint::new(0);MAX];100];
    for p in 1..100 {
        let prob=Mint::new(p)/100;
        for n in 3..MAX+3 {
            for i in 0..(1<<n) as usize {
                let mut deque=VecDeque::from([(0,0)]);
                let mut seen=vec![false;n];
                let mut e=Mint::new(0);
                let mut cnt=0;
                let mut seen_cnt=0;
                while let Some((x,d))=deque.pop_front() {
                    if !seen[x] {
                        seen[x]=true;
                        e+=d;
                        seen_cnt+=1;
                        if !seen[(x+1)%n] {
                            if (i>>cnt)%2>0 {
                                deque.push_front(((x+1)%n,d+1));
                            } else {
                                deque.push_back(((x+1)%n,d+1));
                            }
                            cnt+=1;
                        }
                        if !seen[(x+n-1)%n] {
                            if (i>>cnt)%2>0 {
                                deque.push_front(((x+n-1)%n,d+1));
                            } else {
                                deque.push_back(((x+n-1)%n,d+1));
                            }
                            cnt+=1;
                        }
                    }
                    if seen_cnt==n {
                        break;
                    }
                }
                prefix[p][n-3]+=e*prob.pow(i.count_ones() as u64)*(Mint::new(1)-prob).pow(n as u64-i.count_ones() as u64);
            }
        }
    }
    let bm=vec_range(0, 100, |i| prefix[i].berlekamp_massey());
    input!(t:usize);
    for _ in 0..t {
        input!(n:usize,p:usize);
        outputln!(prefix[p].calculate_nth_term(n-3, &bm[p]));
    }
}

流石にF問題はやめておきます。