ARC086 F - Shift and Decrement

最初面倒 + 間違っている方針を考えていたが、某にもっと簡単な方法があると言われたので簡単な方法に気づくことができた。
F - Shift and Decrement

問題

N個の非負整数A_1, ..., A_Nが黒板に書かれている。以下の操作をK回まで行う。

  • 以下の2個から一つ選ぶ。
    • 操作A: 黒板の全ての整数xについて、それをfloor(x/2)で置き換える。
    • 操作B: 黒板の全ての整数xについて、それをx - 1で置き換える。これは全ての整数が1以上であるときにのみ行える。

最終的に黒板に書かれている非負整数の組み合わせとしてあり得るものをmod (10^9 + 7)で出力せよ。

解法

まず、どちらの操作も広義単調増加な変換しかしないので、操作によって整数の大きさの順番が変わることはない。よって、Aは多重集合ではなく数列として扱うことができる。Aは昇順にソートされていると仮定してよい。
どんな状態へも、[(操作B1回以下) -> (操作A)]の有限回の繰り返し -> 操作Bの有限回の繰り返しで到達することができる。最後の操作Aの前に操作Bを2回行う場合、それを直後の操作Aの後の操作B1回に置き換えられるためである。最適(操作回数最小)なものがこの形に限ることもほぼ明らか。
1を長さNのすべて1からなる数列とし、減算や乗算を項ごとに定めることにする。[(操作B1回以下) -> (操作A)]の有限回の繰り返しでAからvに到達したとすると、そこからはv, v - 1, v - 21, ..., v -k1に行くことができる。
逆に、到達可能な状態は全てこのように表せるので、上の(v, k)を全列挙すれば良い。これにはメモ化再帰が使える。
さて、このように(v, k)が列挙されたとき、それをまとめることを考えよう。vの各要素からvの最小値を引いてできる配列をd(v)としたとき、v1 - s1 = v2 - t1が成立するのはd(v1) = d(v2), v1[1] - s = v2[1] - tのとき、及びそのときに限る。(仮定からv1もv2もソートされていることに注意。)
以上から、d(v)が同じ状態をまとめると、それぞれについてあり得る最終状態はd(v) + u1 (uは然るべき区間のunionの上を動く)であることが分かる。各d(v)について、uが動く集合(下記コードのrangesのunion)の大きさを求めればよい。これは区間の出現・消失イベントの起きる時刻でソートすればできる。
計算量はどうなるだろうか? 最初のメモ化再帰で訪れる頂点の個数を見積もる。操作Aを使う個数を固定し、最後に操作Aを使った状態だけを考えると、現れる数列はA/2^b - 1からA/2^bの間に収まるので、高々N通りである。探索順を工夫すれば、各状態を2回以上探索しないようにできるため、探索すべき状態の個数はO(N * log max A)程度である。1状態ごとにかかる時間はO(N)なので、全体の計算量はO(N^2 * log max A)である。

登場する典型

  • 最適戦略についての考察
  • メモ化再帰
  • 区間のunionの大きさ

実装上の注意点

  • traverseの順番に気をつける
    • kが大きい順にtraverseすることで、ある状態に2回目以降訪れた場合にそこから先の探索が起きないことが保証できる(ダイクストラ法に近い)

提出: Submission #3974898 - AtCoder Regular Contest 086 (Rust)

fn chmax<T: Ord>(a: &mut T, b: T) -> bool {
    if *a >= b { return false; }
    *a = b;
    true
}

fn dfs(a: Vec<i64>, k: i64, memo: &mut HashMap<Vec<i64>, i64>) {
    {
        let entry = memo.entry(a.clone()).or_insert(-1);
        if !chmax(entry, k) { return }
    }
    if k == 0 { return };
    let half = a.iter().map(|&x| x / 2).collect();
    dfs(half, k - 1, memo);
    if k > 1 && a[0] > 0 {
        let half = a.iter().map(|&x| (x - 1) / 2).collect();
        dfs(half, k - 2, memo);
    }
}

const MOD: i64 = 1_000_000_007;

fn solve() {
    let out = std::io::stdout();
    let mut out = BufWriter::new(out.lock());
    macro_rules! puts {
        ($($format:tt)*) => (write!(out,$($format)*).unwrap());
    }
    input! {
        n: usize,
        k: i64,
        a: [i64; n],
    }

    let mut a = a;
    a.sort();

    let mut memo = HashMap::new();
    dfs(a, k, &mut memo);
    // compress
    let mut comp = HashMap::new();
    for (a, k) in memo {
        let mi = a[0];
        let mut diff = vec![0; n - 1];
        for i in 0 .. n - 1 { diff[i] = a[i + 1] - mi; }
        let range = (max(0, mi - k), mi);
        let entry = comp.entry(diff).or_insert(Vec::new());
        entry.push(range);
    }
    let mut tot = 0;
    for (_, ranges) in comp {
        let mut events = Vec::new();
        for (x, y) in ranges {
            events.push((x, 1));
            events.push((y + 1, -1));
        }
        events.sort();
        let mut cur = 0;
        let mut last = 0;
        for (t, delta) in events {
            if cur > 0 {
                tot += t - last;
                tot %= MOD;
            }
            last = t;
            cur += delta;
        }
    }
    puts!("{}\n", tot);
}

まとめ

本番誰も解けなかったのが意外である。ちなみに私は時間ギリギリにEを通して狂喜してた。

ARC080 F - Prime Flip

最初見た時こんなの無理だろと思ったが、期間をおいて考え直したら分かった。
F - Prime Flip

問題

無限枚のカードがある。カードには1番から番号が振られている。最初x[1], ..., x[N]番目のカードは表向きで、それ以外は裏向きである。
以下の操作を何回か行うことができる。

  • 3以上の素数pを選ぶ。連続するp枚のカードをひっくり返す。

最終的に全てのカードを裏向きにする時、操作回数の最小値を求めよ。

  • 1 <= N <= 100
  • xはソートされている

解法

まず、範囲クエリなのでimos法を使い、Z/2Zの元を要素として持つ配列aに以下の操作を繰り返して全て0にする問題に変換する:

  • 3以上の素数pと正の整数uを選ぶ。a[u] += 1, a[u + p] += 1を同時に行う。

ここで、a[u]とa[v]を同時に1から0にするのに必要なコストを考えよう。v - uが3以上の素数なら1である。v - uが偶数の時、素数がたくさんあることを考えるとp1 - p2 = v - uなる素数p1, p2があるので、コストは2である。v - uが素数でない奇数の時、同様にコストは3である。
よってa[u] = 1であるような奇数と偶数をマッチングし、マッチングできたものをコスト1でマッチング、できなかったものをコスト2または3でマッチングするという実行可能解が存在することが分かる。以降最適解の中には必ずこの形のものがあることを証明する。
最適解でflipする整数全体を頂点集合とし、同時にflipする整数の間に辺を設けた無向グラフGを考える。このグラフは単純であるとしてよい。次数3以上の頂点は、上の議論を用いて次数のうち2個を別の整数にすることができるので、Gの頂点は全て次数2以下としてよい。
次数2以下の頂点からなるグラフはパスかサイクルかその直和である。サイクルは無駄なのでないとしてよい。Gの各パスについて、上の議論およびGの最適性からそれらは長さ3以下である。

登場する典型

  • Z/2Z上の範囲加算クエリをたくさん飛ばせるので、imosして2点更新クエリにする
  • 素数はたくさんあるので、2, 3個組み合わせれば全ての整数を表現できる
  • 最適解が特定の形を持つことを示し、探索すべき範囲を減らす

実装上の注意点

  • bipartite_matchingの内部で、n = 0の時にg[0]を参照しないようにする(2敗(は?))
  • Dinic法で殴る際は、辺の重みを適切に設定する(この場合全て1) (1敗)

提出: Submission #3957344 - AtCoder Regular Contest 080 (Rust)

fn prime_sieve(n: usize) -> Vec<bool> {
    let mut ans = vec![true; n];
    ans[0] = false;
    ans[1] = false;
    for i in 2 .. n {
        if !ans[i] { continue; }
        for j in 2 .. (n - 1) / i + 1 {
            ans[i * j] = false;
        }
    }
    ans
}

fn bipartite_matching(g: &[Vec<bool>]) -> usize {
    let n = g.len();
    if n == 0 { return 0; }
    let m = g[0].len();
    let mut to = vec![None; m];
    let mut visited = vec![false; n];
    let mut ans = 0;
    fn augment(v: usize, g: &[Vec<bool>],
               visited: &mut [bool], to: &mut [Option<usize>])
               -> bool {
        if visited[v] { return false; }
        visited[v] = true;
        for i in 0 .. g[v].len() {
            if !g[v][i] { continue; }
            if let Some(w) = to[i] {
                if augment(w, g, visited, to) {
                    to[i] = Some(v); return true;
                }
            } else {
                to[i] = Some(v); return true;
            }
        }
        false
    }
    for i in 0 .. n {
        for v in visited.iter_mut() { *v = false; }
        if augment(i, &g, &mut visited, &mut to) { ans += 1; }
    }
    ans
}

fn solve() {
    let out = std::io::stdout();
    let mut out = BufWriter::new(out.lock());
    macro_rules! puts {
        ($($format:tt)*) => (write!(out,$($format)*).unwrap());
    }
    input! { x: [i64] }
    const W: usize = 1 << 24;
    let is_prime = prime_sieve(W);
    let mut hs = HashSet::new();
    for x in x {
        if hs.contains(&x) {
            hs.remove(&x);
        } else {
            hs.insert(x);
        }
        let x = x + 1;
        if hs.contains(&x) {
            hs.remove(&x);
        } else {
            hs.insert(x);
        }
    }
    let mut even = Vec::new();
    let mut odd = Vec::new();
    for x in hs {
        if x % 2 == 0 { even.push(x); }
        else { odd.push(x); }
    }
    let n = even.len();
    let m = odd.len();
    let mut g = vec![vec![false; m]; n];
    for i in 0 .. n {
        for j in 0 .. m {
            if is_prime[(even[i] - odd[j]).abs() as usize] {
                g[i][j] = true;
            }
        }
    }
    let size = bipartite_matching(&g);
    let mut tot = 3 * ((n - size) % 2);
    tot += 2 * ((n - size) / 2);
    tot += 2 * ((m - size) / 2);
    tot += size;
    puts!("{}\n", tot);
}

まとめ

それっぽい方針を思いついた後の正当性の証明に手こずった。

ARC093 F - Dark Horse

当時E問題と同じセットで出題され、全完が5人しかいなかった。強い人にとっては典型。

  • 例:

F - Dark Horse

問題

2^N人の人がトーナメントで優勝者を決める。各ゲームは基本的に番号の小さい人が勝つが、M人のダークホースがおり、その人は1に勝てる。
人の並べ方(2^N)!通りのうち、1が優勝者になる並び方の総数をmod (10^9 + 7)で求めよ。

解法

1は先頭に固定して良い(あとで場合の数を2^N倍する)。1の対戦に注目すると、最初は1人と、次は2人の中で勝ち上がった者(番号minの者)と、次は4人の中で…最後は2^{N-1}人の中で番号minの者と戦う。これらの中にダークホースがいなければよい。それぞれの人の集合にS_1, ..., S_Nという名前をつける。
包除原理を使うと、集合B \subseteq {1, ..., N}について、i in Bなるi全てについてmin S_iがダークホースであるような人の並べ方の総数が分かれば良いことが分かる。番号の大きいダークホースから見ていき、添字を[最後に加えたダークホースの番号][埋めた集合]とするDPを実行すると、すべてのBに対する値がO(2^N * N * M^2)で計算できる。遷移の係数は以下の命題を使うと計算できる。

1からLまで番号付けされたL個のものがあり、K個の箱がある。i番目の箱にはd_i個のものを入れ、入っているものの番号のminがc_iである必要がある。c_iは昇順に並んでいる。このとき箱へのものの入れ方はC(L - c_K, d_K - 1) * C(L - c{K -1} - d_K, d_{K - 1} - 1) * C(L - c_{K - 2} - d_K - d_{K - 1}, d_{K - 2} - 1) * ...である。

この命題では箱へものを入れる順番の違いは無視されるので、最後に1! * 2! * 4! * ... * (2^{N - 1})!倍する必要がある。

登場する典型

  • 包除原理
  • 「ある箱にはhogehoge番目以降しか入れられない」のような制約がある中での数え上げ
    • ソートして大きい方から見れば正確に計算できる
  • DPの結果使い回し
    • これをしないとO(3^N poly(N, M))くらいになりそう

実装上の注意点

  • あとで数倍する処理を忘れないようにする。
  • DPの遷移で±1などの部分を間違えないようにする(1敗)。
  • 複数の添字があるので、添字の順番を誤らないように注意する(1敗)。
  • 組み合わせの計算を誤らないようにする(1敗)。
  • 数え上げなので、ある程度大きなサンプルが合っていれば安心して良い。

提出: (2019/01/09 22:05 修正, ModIntライブラリの修正により) Submission #3968098 - AtCoder Regular Contest 093 (Rust)

// ModInt省略

fn fact_init(w: usize) -> (Vec<ModInt>, Vec<ModInt>) {
    let mut fac = vec![ModInt::new(1); w];
    let mut invfac = vec![0.into(); w];
    for i in 1 .. w {
        fac[i] = fac[i - 1] * i as i64;
    }
    invfac[w - 1] = fac[w - 1].inv();
    for i in (0 .. w - 1).rev() {
        invfac[i] = invfac[i + 1] * (i as i64 + 1);
    }
    (fac, invfac)
}

fn solve() {
    let out = std::io::stdout();
    let mut out = BufWriter::new(out.lock());
    macro_rules! puts {
        ($($format:tt)*) => (write!(out,$($format)*).unwrap());
    }
    input! {
        n: usize,
        m: usize,
        a: [usize1; m],
    }
    let (fac, invfac) = fact_init(200100);
    let mut dp = vec![vec![ModInt::new(0); 1 << n]; m + 1];
    dp[m][0] = 1.into();
    for bits in 0 .. 1 << n {
        for i in 0 .. n {
            if (bits & 1 << i) == 0 { continue; }
            for k in 0 .. m {
                let factor = if bits + a[k] <= 1 << n {
                    let x = (1 << n) - bits - a[k];
                    let y = (1 << i) - 1;
                    fac[x + y] * invfac[x] * invfac[y]
                } else {
                    0.into()
                };
                for j in k + 1 .. m + 1 {
                    dp[k][bits] += dp[j][bits ^ 1 << i] * factor;
                }
            }
        }
    }
    let mut inex: ModInt = 0.into();
    for bits in 0 .. 1 << n {
        let mut sum: ModInt = 0.into();
        for i in 0 .. m + 1 {
            sum += dp[i][bits];
        }
        sum *= fac[(1 << n) - 1 - bits];
        for i in 0 .. n {
            if (bits & 1 << i) == 0 {
                sum *= invfac[1 << i];
            }
        }
        if bits.count_ones() % 2 == 1 {
            sum = -sum;
        }
        inex += sum;
    }
    inex *= 1 << n;
    for i in 0 .. n {
        inex *= fac[1 << i];
    }
    puts!("{}\n", inex);
}

まとめ

25分くらいで実装できたのでよかった。達成感を味わえる問題だと思う。

ARC093 E - Bichrome Spanning Tree

本番解けなかったけど、知識が増えた状態でやり直してみたら解けたのでよかった。
E - Bichrome Spanning Tree

問題

N頂点M辺の無向グラフがある。各辺を白か黒で塗るとき、以下の条件を満たすような辺の塗り方を求めよ。

  • 白い辺と黒い辺を1個以上含む全域木が存在する。また、そのような全域木の最小の重みはXである。
  • 1 <= N <= 1000
  • 1 <= M <= 2000
  • 1 <= (辺の重み) <= 10^9
  • 1 <= X <= 10^12
  • 与えられるグラフは連結で単純である。

解法

以降白い辺と黒い辺を1個以上含む全域木のことを白黒全域木と呼ぶことにする。
まず、問題を累積的な形に変換する。以下の値をg(y)とすると、g(X) - g(X - 1)を計算すれば良い。

  • g(y) := 重みy以下の白黒全域木が存在するような辺の塗り方の総数

これを計算するにはどうすればよいだろうか? 適当に最小全域木Sを固定した時、白黒全域木の中で最小の重みを持つものとしてあり得るのは、Sが白黒ならばSと同じ重みを持つ全域木、Sが単色ならば、Sから辺を1本取り除き1本付け加えたものに限られる。これは以下の命題を経由することで証明できる。

命題
Sを上で固定した最小全域木とする。Tを任意の全域木としたとき、S != Tならば、あるe in T \ S, f in S \ Tが存在し、T - e + fはT以下の重みを持つ。

証明
どのようなe in T \ Sに対しても、どのようにf in S \ Tをとってもw(T - e + f) > w(T)となってしまうと仮定する。このときw(e) < w(f)である。S != Tより、f' in S \ Tを一つ選ぶとあるe' in T \ Sに対してS - f' + e'は全域木である。ところが仮定よりw(e') < w(f')であるため、Sの最小性と矛盾する。(証明終)*1

この定理を使うと、任意の白黒全域木Tについて、白黒であるという条件を保ったままTから辺を消しSの辺を加える、という操作が可能になり、最終的には|T \ S| <= 1となるまでこの操作ができる。よって、Sから辺を1本取り除き1本加えた全域木だけを考えれば良い。

頂点u, vに対し、ma[u][v]を、S内のu-vパスに含まれる辺の重みの最大値とすると、辺(u, v)を加えて適切な辺を1本加える時の全域木の重みの最小値は、w(S) - ma[u][v] + (u-vの重み)であることがわかる。この時の全域木の重みがy以下となる辺全て(K)について、それらが白黒となる(つまり、白だけあるいは黒だけにならない)ことが、g(y)で数え上げられる条件と同値であることがわかる。
maは特に工夫なくO(N^2)で計算できる。集合Kの大きさ|K|はO(M)でわかる。|K|さえ分かれば、g(y) = (2^|K| - 2) * 2^{n - 1 - |K|}という形になる。(ただし、|K| = 0の時は0通り。)

実装上の注意点

  • 計算すべきものが多いので、意図通りの値が入っているか確認する。
    • maの計算をバグらせて20分溶かした

提出: Submission #3933175 - AtCoder Regular Contest 093 (Rust)

// UnionFind省略
// ModInt省略

// Stores in ma[i] the maximum weight from v to i for every i.
fn dfs_ma(mst: &[Vec<(usize, i64)>], v: usize, par: usize, cur: i64,
          ma: &mut [i64]) {
    ma[v] = cur;
    for &(w, cost) in mst[v].iter() {
        if w == par { continue; }
        dfs_ma(mst, w, v, max(cur, cost), ma);
    }
}

// Finds #ways to color g's edges s.t. there exists at least one spanning
// tree that has weight <= y.
fn calc(g: &[Vec<(usize, i64)>], ma: &[Vec<i64>], mst_weight: i64, y: i64)
        -> ModInt {
    if y < mst_weight { return ModInt::new(0); }
    let n = g.len();
    let mut relevant_edges = 0;
    let mut all_edges = 0;
    for v in 0 .. n {
        for &(w, cost) in g[v].iter() {
            if v > w { continue; }
            let diff = mst_weight - ma[v][w] + cost;
            if diff <= y { relevant_edges += 1; }
            all_edges += 1;
        }
    }
    assert!(relevant_edges >= 1);
    let two = ModInt::new(2);
    let mut ans = two.pow(relevant_edges) - two;
    ans *= two.pow(all_edges - relevant_edges);
    ans
}

fn solve() {
    let out = std::io::stdout();
    let mut out = BufWriter::new(out.lock());
    macro_rules! puts {
        ($($format:tt)*) => (write!(out,$($format)*).unwrap());
    }
    input! {
        n: usize,
        m: usize,
        x: i64,
        uvw: [(usize1, usize1, i64); m],
    }
    let mut g = vec![Vec::new(); n];
    let mut edges = Vec::new();
    for (u, v, w) in uvw {
        g[u].push((v, w));
        g[v].push((u, w));
        edges.push((w, u, v));
    }
    edges.sort();
    let mut mst = vec![Vec::new(); n];
    let mut mst_weight = 0;
    let mut uf = UnionFind::new(n);
    for (cost, u, v) in edges {
        if uf.is_same_set(u, v) { continue; }
        uf.unite(u, v);
        mst[u].push((v, cost));
        mst[v].push((u, cost));
        mst_weight += cost;
    }
    // ma[i][j]: maximum weight in the unique path i-j in mst
    let mut ma = vec![vec![-INF; n]; n];
    for i in 0 .. n {
        dfs_ma(&mst, i, n, -INF, &mut ma[i]);
    }
    puts!("{}\n", calc(&g, &ma, mst_weight, x) - calc(&g, &ma, mst_weight, x - 1));
}

まとめ

本番時のリベンジができて嬉しかったけど、それでもデバッグ含め50分かかってしまった。

*1:多分この手の議論はマトロイドで頻出の議論のはずです。参考: マトロイド - Wikipedia, 様々な全域木問題

ARC092 F - Two Faced Edges

この手の全体の中から一つ取り除くタイプの問題は、「sigmaさんが『yosupoがこういうの得意です』と言っていたタイプの問題」という印象が強い。
F - Two Faced Edges

問題

N頂点M辺の有向グラフがある。各辺について、その辺だけの向きを反転した時、グラフの強連結成分(SCC)の個数が変わるかどうかを判定せよ。

  • 2 <= N <= 1000
  • 1 <= M <= 200000
  • グラフに自己ループや多重辺はない。互いに逆向きの辺はありえる。

解法

各辺a -> bについて、その辺を除いた時のa => bの到達可能性(A)と、b => aの到達可能性(B)が分かればよいことを示す。

  • A = trueのとき、a -> bを取り除いた後にSCCの個数は変化しない。B = falseなら、b -> aの辺を加えればaとbが同じSCCに属し、SCCの個数が1減る。B = trueなら、aとbはもともと同じSCCに属するので、b -> aの辺を加えてもSCCの個数は変化しない。
  • A = falseのとき、a -> bを取り除くとaからbへは到達不能になる。よってb -> aを加えるタイミングではSCCの個数は変化しない。B = trueの場合は、もともとaとbは同じSCCに属していたので、a -> bを取り除く時にSCCの個数が1個増える。B = falseの場合は、もともとaとbは別のSCCに属していたので、a -> bを取り除く時にSCCの個数は増えない。

以上より、各辺に対して上のAとBを求めれば良い。a -> bの有無はb => aの到達可能性には影響しないので、Bは簡単である。
Aについて調べたい。a => bへ辺a -> bを使わずに行けるのは、aから出る辺はa -> bより番号が小さいものしか使わずにbに行ける時か、aから出る辺はa -> bより番号が大きいものしか使わずにbに行ける時、およびその時に限る。(a以外から出る辺は全て使って良い。)よって、各頂点aおよびそこに接続する各辺e_i = (a -> b_i)からDFSをして、aから出るi番目以下の辺だけを使って到達可能な点、aから出るi番目以上の辺だけを使って到達可能な点を全て調べればよい。ナイーブにやるとO(M^2)かかるが、あらかじめ隣接リストを頂点ごとにソート・逆順ソートし、各DFSでインクリメンタルに更新することで、計算量をO(MN)に抑えることができる。
全体の計算量はO(N^2 + MN)である。

実装上の注意点

  • ボトルネックがO(MN)と重いので、極力重い定数倍を載せないようにする
  • 隣接リストのソートをやっても良いが、普通にグラフを作るとそもそも不要
  • DFSをやる時に、連結性の更新処理のタイミングに注意を払う

提出: Submission #3929024 - AtCoder Regular Contest 092 (Rust)

type Idx = i32;
const INF: Idx = 1 << 20;

fn dfs<F>(g: &[Vec<(Idx, usize)>], lt: &F,
          conn: &mut [Idx], v: usize, cur: Idx)
where F: Fn(Idx, Idx) -> bool {
    if !lt(cur, conn[v]) { return; }
    conn[v] = cur;
    for &(_, w) in g[v].iter() {
        dfs(g, lt, conn, w, cur);
    }
}

// conn[i][j] = k: i -> j is reachable if you use edges from i with index <= k
// and edges that are not from i.
fn traverse<F>(g: &[Vec<(Idx, usize)>], lt: F, min: Idx) -> Vec<Vec<Idx>>
where F: Fn(Idx, Idx) -> bool {
    let n = g.len();
    let mut conn = vec![vec![-min; n]; n];
    for i in 0 .. n {
        conn[i][i] = min;
        for &(idx, w) in g[i].iter() {
            dfs(&g, &lt, &mut conn[i], w, idx);
        }
    }
    conn
}


fn solve() {
    let out = std::io::stdout();
    let mut out = BufWriter::new(out.lock());
    macro_rules! puts {
        ($($format:tt)*) => (write!(out,$($format)*).unwrap());
    }
    input! {
        n: usize,
        ab: [(usize1, usize1)],
    }
    let mut g = vec![Vec::new(); n];
    for (i, &(a, b)) in ab.iter().enumerate() {
        g[a].push((i as Idx, b));
    }
    // g[i] is already in the increasing order
    let conn_orig = traverse(&g, |x, y| x < y, -INF);
    // decreasing order
    for adj in g.iter_mut() {
        adj.reverse();
    }
    let conn_rev = traverse(&g, |x, y| x > y, INF);
    for (i, (a, b)) in ab.into_iter().enumerate() {
        let i = i as Idx;
        let orig = conn_orig[a][b] != i || conn_rev[a][b] != i;
        let rev = conn_orig[b][a] < INF;
        puts!("{}\n", if orig ^ rev { "diff" } else { "same" });
    }
}

まとめ

本番解けずに(確か)rngさんの動画解説(通った辺のminやmaxみたいな何かを管理する)を聞いた時は「こんなの思いつけるわけないだろ」と思ったが、改めて考え直すと結構自然な考え方だと思った。
ただ最初自然に考えた結果「i番目未満の辺だけで行けるかi番目より大きい辺だけで行けるならOK」みたいになってしまい、サンプル1で間違いになった。(は?)

  • 当然これは誤りで、サンプル1だと1 -> 3は1番目の辺1 -> 2と3番目の辺2 -> 3を両方使うことでのみ行ける

2019年の目標

2019年の目標です。

競技プログラミング

  • レーティングをAtCoder 2900以上、Codeforces 2500以上(、Topcoder 2200以上)にする
  • 週に1回は、難しい問題を解説を見て通す
    • 実装が難しい問題が望ましい
  • 競プロ共通ライブラリプロジェクトを進める

開発

  • RustやErlangで何か書く
  • (UPD 2019/04/09) Kaggleに取り組む
    • (UPD 2019/04/09) 何らかの大会に参加する

趣味

  • 年で5回以上作曲して公開する
  • スプラトゥーン2で全ルールS+安定になるようにする

CODE FESTIVAL 2017 Final J - Tree MST (解説を見た)

本番見すらしなかった問題だけど、解説を見たりドワコン5th本戦C問題の準備をしたりして理解が深まったので解けた。
J - Tree MST

問題

辺に重みがついているN頂点の木、および長さNの数列Xが与えられる。このとき、以下の完全グラフの最小全域木の重みを求めよ。

  • 頂点数はN
  • (u, v)の重みは、X[u] + X[v] + (uv間の距離)である。

制約

  • 2 <= N <= 2 * 10^5
  • 1 <= X[i] <= 10^9
  • 重みは1から10^9までの整数
  • 入力は全て整数

解法

最小全域木なので、PrimかKruskalかブルーフカ法のどれかをまず検討すべきである。
今回はブルーフカ法を使う。頂点や連結成分ごとに一番近い点を求めることが、全て合わせてO(N)時間程度で求めることができるためである。
連結成分を色だと思うと、各頂点uについてX[v] + (uv間の距離)が最小となる点vとその色、および2番手の色について同じものが分かれば良い。これは全方位木DPでできる。
それができたら、連結成分ごとに一番近い別の連結成分を求めて、それらに対し辺を張る。
計算量はO(N log N)である。ただしVecを使いまくるので、定数倍は重い。

実装上の注意点

  • オーバーフローに気をつける。今回は全ての木の辺をつなぐという実行可能な解が存在し、それのコストは高々2N * max(X_i) + \sum (辺の重み) <= 10^15なので、辺の重みをi64で管理すればオーバーフローはしない。
  • 同じ重みを持つ辺のタイブレイクは一貫させる!(そうでないと、例えば辺が全て同じ重みの3角形で壊れる)
    • 頂点番号でよい
  • ブルーフカ法の各ステップでは、各頂点からではなく、各連結成分から一番近い連結成分に向かって辺を生やす!!
    • 今の所各頂点からで失敗する簡単な例はわからないが、その方法で実装して提出してみるとWAになった
    • 追記: 例は下にある。
  • mergeはO(N)でできるが、サボってsortを使うとO(N log N)になる。結構リスキーなのでO(N)にしたいところ。

提出: Submission #3917559 - CODE FESTIVAL 2017 Final (Rust)

type Cost = (i64, usize, usize);

fn merge(pool: Vec<(Cost, usize)>) -> Vec<(Cost, usize)> {
    if pool.len() == 0 { return pool; }
    let &mi = pool.iter().min().unwrap();
    let mut ans = vec![mi];
    let mi2 = pool.iter().filter(|&&(_, color)| color != mi.1).min();
    match mi2 {
        None => (),
        Some(&x) => ans.push(x),
    }
    ans
}

fn dfs1(v: usize, par: usize, x: &[i64], g: &[Vec<(usize, i64)>], color: &[usize], dp1: &mut [Vec<(Cost, usize)>]) {
    let mut pool = Vec::new();
    for &(w, cost) in g[v].iter() {
        if w == par { continue; }
        dfs1(w, v, x, g, color, dp1);
        for &((a, _, u), b) in dp1[w].iter() {
            pool.push(((a + cost, v, u), b));
        }
        pool.push(((cost + x[w], v, w), color[w]));
    }
    dp1[v] = merge(pool);
}

fn dfs2(v: usize, par: usize, x: &[i64], g: &[Vec<(usize, i64)>],
        color: &[usize],
        dp1: &[Vec<(Cost, usize)>], dp2: &mut [Vec<(Cost, usize)>],
        pardist: i64) {
    let mut pool = Vec::new();
    let n = g.len();
    if par < n {
        for &((d, _, dec), c) in dp2[par].iter() {
            pool.push(((d + pardist, v, dec), c));
        }
        pool.push(((pardist + x[par], v, par), color[par]));
    }
    pool.append(&mut dp1[v].clone());
    dp2[v] = merge(pool);

    for &(w, cost) in g[v].iter() {
        if w == par { continue; }
        dfs2(w, v, x, g, color, dp1, dp2, cost);
    }
}

const INF: i64 = 1 << 60;

fn calc(x: &[i64], g: &[Vec<(usize, i64)>]) -> i64 {
    let n = x.len();
    let mut uf = UnionFind::new(n);
    let mut conn = n;
    let mut tot: i64 = 0;
    while conn > 1 {
        let oldconn = conn;
        let mut color = vec![0; n];
        for i in 0 .. n { color[i] = uf.root(i); }
        let mut dp1 = vec![Vec::new(); n];
        let mut dp2 = vec![Vec::new(); n];
        dfs1(0, n, &x, &g, &color, &mut dp1);
        dfs2(0, n, &x, &g, &color, &dp1, &mut dp2, 0);
        let mut edges = vec![(INF, n, n); n];
        for i in 0 .. n {
            let ((cost, anc, dec), _idx) =
                if !uf.is_same_set((dp2[i][0].0).1, (dp2[i][0].0).2) {
                    dp2[i][0]
                } else {
                    dp2[i][1]
                };
            let cost = cost + x[i];
            let (anc, dec) = if anc > dec {
                (dec, anc)
            } else {
                (anc, dec)
            };
            assert_ne!(anc, dec);
            let re = &mut edges[uf.root(i)];
            *re = min(*re, (cost, anc, dec));
        }
        for (cost, anc, dec) in edges {
            if cost < INF && !uf.is_same_set(anc, dec) {
                tot = tot.checked_add(cost).unwrap();
                uf.unite(anc, dec);
                conn -= 1;
            }
        }
        assert_ne!(oldconn, conn);
    }
    tot
}

まとめ

連結成分ごとではなく頂点ごとに一番近い点へ辺を伸ばすのも同じくらいうまくいきそうなのに、なぜダメなのだろうか? (簡単な反駁用ケースが欲しい)
追記 (2019/01/03 19:08): 以下のようなケースで反駁できる。ここで0と1は重み1の辺で連結しており、他の頂点はすべて1点からなる連結成分をなしている。仮にここで各頂点から最も近い連結成分への辺を追加すると、重み3, 2, 1000の辺が追加されるが、明らかにこれは最小全域木にはならない。

f:id:koba-e964:20190103190525p:plain
https://csacademy.com/app/graph_editor/で作った