「みんなのプロコン 2019」E - Odd Subrectangles

この問題まではムーブが完璧だった。
E - Odd Subrectangles

問題

N行M列の、要素が0と1からなる行列Aが与えられる。行と列の部分集合(U, V)の組は2^{N+M}通りありえるが、その中で以下を満たすものを数え上げよ。

  • Aを(U, V)に制限したときにできる行列に含まれる1の個数は奇数である

解法

まず行の集合Uを固定しよう。このとき問題の条件をみたす列の集合Vは何通りあるだろうか? 簡単な考察で、以下の事実がわかる。

  • Uとの交差部分に含まれる1の個数が奇数であるような列全体の集合をKとする。(U, V)に制限した行列の中に1が奇数個あることと、|V \cap K|の大きさが奇数であることは同値である。そのようなVは、|K| = 0のとき0個、|K| > 0のときちょうど全組み合わせの半分(つまり2^{M-1}個)ある。

よって、|K| = 0となるようなUの個数を数え上げることができればよい。これは各行を行ベクトルと見做した時に、Uに含まれる列ベクトルの和が0であるようなUの個数と等しい。
各行について、それがUに含まれるかどうかを0/1の変数で表すことにすると、結局N次元の0/1ベクトルvであって、Z/2Z上で計算したときvA = 0となるものの個数を計算すれば良いことがわかる。Z/2Zはであるため、普通の線形代数が適用できて、このようなvはker Aの住人、およびそれに限ることがわかる。|ker A| = 2^{dim (ker A)}であり、dim (ker A) = N - rank Aであるので、rank Aを計算すればよい。これはGauss-Jordanのアルゴリズムなどでできる。

登場する典型

  • Z/2Z上の線形代数
    • rank, ker, ものの有り無しを0/1ベクトルと見做して線形代数に帰着させる

実装上の注意点

  • Gauss-Jordanは難しいので実装に気をつける

提出: Submission #4212889 - Yahoo Programming Contest 2019 (Rust)

// ModInt省略

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: [[i32; m]; n],
    }
    let mut a = a;
    let mut c = 0;
    for r in 0..m {
        if c >= n {
            break;
        }
        let mut c2 = None;
        for i in c..n {
            if a[i][r] == 1 {
                c2 = Some(i);
                break;
            }
        }
        if c2.is_none() {
            continue;
        }
        let c2 = c2.unwrap();
        a.swap(c, c2);
        for k in c + 1..n {
            if a[k][r] == 1 {
                for j in 0..m {
                    a[k][j] ^= a[c][j];
                }
            }
        }
        c += 1;
    }
    let two = ModInt::new(2);
    // eprintln!("n = {}, c = {}", n, c);
    let mut prod = two.pow(n as i64) - two.pow((n - c) as i64);
    prod *= two.pow(m as i64 - 1);
    puts!("{}\n", prod);
}

まとめ

ここまで完璧だっただけにFのミスが激痛ですね。

「みんなのプロコン 2019」 D - Ears

E問題まではほぼ完璧に近いムーブができていたんだけどなあ。
D - Ears

問題

長さLの数列a[i]が与えられる。以下の規則で長さLの数列b[i]を作る時、\sum abs(a[i] - b[i])を最小化し、その最小値を求めよ。

  • b[i]の要素をすべて0で初期化する。0からLまでの番号がついた点がある。初期地点と終了地点を適当に決め、任意回以下の操作を行う。
    • 現在点Xにいるとき、X-1またはX+1のどちらかに移動する。(存在しない点には移動できない。)この移動でi-1とiの間を通った時、b[i] += 1を行う。

解法

bとしてあり得る数列はどのようなものだろうか? これは(0の連続)-(2以上の偶数の連続)-(奇数の連続)-(2以上の偶数の連続)-(0の連続)の形に限られることが示せる。

  • bをつくるときのパスの始点をs、終点をtとすると、パスにtからsへの一本道を足すと閉路になる。閉路において各区間を通る回数は偶数なので、もともとのパスにおいてはs-t間だけが奇数、それ以外が0より大きい偶数だったことになる。
  • 逆に、c = (0の連続)-(2以上の偶数の連続)-(奇数の連続)-(2以上の偶数の連続)-(0の連続)として、b = cとするためのパスが必ず存在することを示す。点iと点i + 1の間にc[i]本の辺を設けた無向グラフを考える。Eulerの定理より、このグラフは連結で、次数が奇数の点が0個または2個であるため、オイラーパスが存在する。そのオイラーパスによってできるbはcと一致する。

この形の数列を全探索してaとの差分を求めることは不可能である。以下の観察を利用する:

  • 0以外の整数をb[i]とする場合、a[i]から2以上離れた値を使う意味はない。

これにより、現在のbが上の5状態のどれにあるかを状態として持つDPができる。状態数は5で遷移の個数は3である。

登場する典型

実装上の注意点

  • 遷移がそこそこ複雑なので、遷移をあらかじめテーブルにして、ロジックとデータを分離する

提出: Submission #4211296 - Yahoo Programming Contest 2019 (Rust)

fn solve() {
    let out = std::io::stdout();
    let mut out = BufWriter::new(out.lock());
    macro_rules! puts {
        ($($format:tt)*) => (write!(out,$($format)*).unwrap());
    }
    input! {
        l: usize,
        a: [i64; l],
    }
    const INF: i64 = 1 << 58;
    // 0: not started
    // 1: started, odd not started
    // 2: odd started
    // 3: started, odd ended
    // 4: ended
    let mut dp = vec![[INF; 5]; l + 1];
    dp[0][0] = 0;
    for i in 0..l {
        let mut cost = vec![0; 3];
        cost[0] = a[i];
        if a[i] == 0 {
            cost[1] = 1;
            cost[2] = 2;
        } else {
            let r = a[i] % 2;
            cost[1] = 1 - r;
            cost[2] = r;
        }
        // 5: not allowed
        let trans =
            [
                [0, 2, 1],
                [4, 2, 1],
                [4, 2, 3],
                [4, 5, 3],
                [4, 5, 5],
            ];
        for j in 0..5 {
            for k in 0..3 {
                let to = trans[j][k];
                if to >= 5 { continue; }
                dp[i + 1][to] = min(dp[i + 1][to], dp[i][j] + cost[k]);
            }
        }
    }
    let mi: i64 = dp[l].iter().min().cloned().unwrap();
    puts!("{}\n", mi);
}

まとめ

企業コンらしい非自明考察 + 強実装の問題という印象。解けたら誇っていいと思います。

Codeforces Round #536 (Div. 2) F. Lunar New Year and a Recursive Sequence

暗号とか勉強したことがあれば実家。
https://codeforces.com/contest/1106/problem/F

問題

p = 998244353とする。正の整数列(f_i)_iが以下のように定められている:

  • f_1 = ... = f_{k-1} = 1
  • f_k = x
  • i > kのときf_i = (f_{i-1}^{b_1} f_{i-2}^{b_2} ... f_{i-k}^{b_k}) mod p

f_n = mがわかっている。このとき、xとしてあり得る1以上p未満の値を一つ出力するか、またはそのような値がないことを指摘せよ。

制約

  • 1 <= k <= 100
  • 1 <= b_i < p
  • k < n <= 10^9
  • 1 <= m < p

解法

f_i = x^{h_i} mod pの形で表すことにすると、h_iは線形漸化式

  • h_1 = ... = h_{k - 1} = 0
  • h_k = 1
  • i > kのときh_i = b_1 * h_{i-1} + b_2 * h_{i-2} + ... + b_k * h_{i-k}

を満たす。このような数列の第n項は行列累乗を行うことでO(k^3 log n)時間で計算できる。
e = h_nと置くことにする。x^n = mなので、これを満たすxを求めたい。
ここで、mod pで見たときに、0以外のすべての整数は単一の整数の冪乗で表現できることを利用する。p = 998244353の場合は3がその性質を満たす、つまり任意の1からp-1までの整数mに対してm = 3^y mod pとなる整数yが存在する。
問題のmについてそのようなyが求められるのであれば、求めたいものはx^n = 3^yとなる整数xである。まずそのようなyを求めよう。Baby-step giant-stepと呼ばれるアルゴリズムを適用する。S = 40000 (~= sqrt(p))として、1, 3^S, 3^{2S}, ..., 3^{(S-1)S}をキー、0, S, 2S, ..., (S-1)Sを値にして、連想配列を作る。mが与えられた時、m, 3m, 3^2 m, ..., 3^{S - 1}mのどれかは3^Sの冪乗の形になる、つまり連想配列の中に含まれる。これの計算量はO(sqrt(p))である。
ここで、3^{p-1} = 1 (mod p)、また3^z = 1 (mod p)ならばzはp-1の倍数であることに気をつけると、上の問題は、ある整数u,vが存在してun = y + (p-1)vであるかどうかを調べることと同値である。これは拡張ユークリッドの互除法でできる。ここで求まったuについてx = 3^u mod pとすればそれが答え。uが複数あり得る場合はどれでもよい。

以上をまとめると、計算量はO(k^3 log n + sqrt(m))であり、十分間に合う。

登場する典型

  • 行列累乗
  • (Z/pZ)^*が巡回群であること
    • あるg (原始根)が存在して、全ての整数はg^k mod pの形で表せる
  • 離散対数問題

実装上の注意点

  • uが負の数の場合はp-1の倍数を足して非負にする(1敗)

提出: Submission #49502575 - Codeforces (Rust)

// ModInt 省略

fn squmul(a: &[Vec<i64>], b: &[Vec<i64>], mo: i64) -> Vec<Vec<i64>> {
    let n = a.len();
    let mut ret = vec![vec![0; n]; n];
    for i in 0..n {
        for j in 0..n {
            for k in 0..n {
                ret[i][k] += a[i][j] * b[j][k];
                ret[i][k] %= mo;
            }
        }
    }
    ret
}

fn squpow(a: &[Vec<i64>], mut e: i64, mo: i64) -> Vec<Vec<i64>> {
    let n = a.len();
    let mut sum = vec![vec![0; n]; n];
    for i in 0..n { sum[i][i] = 1; }
    let mut cur = a.to_vec();
    while e > 0 {
        if e % 2 == 1 {
            sum = squmul(&sum, &cur, mo);
        }
        cur = squmul(&cur, &cur, mo);
        e /= 2;
    }
    sum
}

fn extgcd(x: i64, y: i64) -> (i64, i64, i64) {
    if y == 0 {
        return (x, 1, 0);
    }
    let r = x % y;
    let q = x / y;
    let (g, a, b) = extgcd(y, r);
    (g, b, a - b * q)
}

fn solve() {
    let out = std::io::stdout();
    let mut out = BufWriter::new(out.lock());
    macro_rules! puts {
        ($($format:tt)*) => (write!(out,$($format)*).unwrap());
    }
    input! {
        k: usize,
        b: [i64; k],
        n: i64,
        m: i64,
    }
    let mut trans = vec![vec![0; k]; k];
    for i in 0..k - 1 {
        trans[i + 1][i] = 1;
    }
    for j in 0..k {
        trans[k - 1 - j][k - 1] = b[j] % (MOD - 1);
    }
    let exp = squpow(&trans, n - 1, MOD - 1)[k - 1][0];
    // Find x s.t. x^exp = m
    let gen = ModInt::new(3);
    const SQRT: i64 = 40000;
    let mut cur = ModInt::new(1);
    let prog = gen.pow(SQRT);
    let mut tbl = HashMap::new();
    for i in 0..SQRT {
        tbl.insert(cur, SQRT * i);
        cur *= prog;
    }
    let discrete_log = |x: ModInt| {
        for i in 0..SQRT {
            let key = x * gen.pow(i);
            if let Some(&y) = tbl.get(&key) {
                return (y - i + MOD - 1) % (MOD - 1);
            }
        }
        unreachable!();
    };
    let logm = discrete_log(ModInt::new(m));
    let (g, inv, _) = extgcd(exp, MOD - 1);
    if logm % g != 0 {
        puts!("-1\n");
        return;
    }
    let inv = (inv + MOD - 1) % (MOD - 1);
    let ans = (logm / g * inv) % (MOD - 1);
    puts!("{}\n", gen.pow(ans));
}

まとめ

原始根もいずれは誰でも知っている定跡になりそう。

CODE THANKS FESTIVAL 2018 G - Sum of Cards

普通に難しくて何時間もかかってしまった。
CODE THANKS FESTIVAL 2018 G - Sum of Cards

問題

Nを正の整数とする。表に1からNまでの整数が、裏に1からNまでの整数が書かれたカードがN枚テーブルの上に存在する。現在の配置について、表に書かれている数字は1からNまでそれぞれちょうど1個存在し、裏に書かれている数字についても同じことが言える。
N枚のカードのうち任意の枚数を裏返し、見える整数の合計を最大化したい。ただし、見える整数がK種類以上存在する必要がある。条件を満たす配置における整数の合計の最大値を求めよ。

制約

  • 1 <= K <= N <= 5000
  • i枚目のカードの表にはA[i]が、裏にはB[i]が書かれている。
    • A[1], ..., A[N]は1からNまでの並び替えで、B[1], ..., B[N]は1からNまでの並び替え

解法

2回以上登場する数字の種類の数をuとすると、見える整数がK種類以上であることとu <= N - Kは同値。
ここで、uをN-K以下にする代わりに、実数xを決めたとき各整数を2回以上選択した場合のペナルティをxにした場合の最大化問題を考える(ラグランジュ緩和)。これは最小カット問題に帰着できる:

  • 頂点数2 + N
  • 1 -> 2 + i: 重み -B[i]
  • 2 + i -> 2: 重み -A[i]
  • A[u] = B[v]のとき 2 + u -> 2 + v: 重みx

このネットワークにおいて1と2をカットするとき、1側の頂点がA[i]を選ぶカード、2側の頂点がB[i]を選ぶカードに対応する。
このネットワークの最小カットはDinic法などで計算できる。これをxを変えて実行し二分探索して、重みxの辺をカットする回数がN-K以下になる最大のxを見つける。
xはどのあたりの範囲で探索すればよいだろうか? まず範囲について考えよう。x=Nなら重みxの辺は全くカットされず、x=0なら全く制限がないのと同じなので、0からNまでの範囲を探索すればOKである。探索精度はどうか? これは難しいが、実験的には1/(N+1)くらいの精度になるまで探索すれば十分。
計算量はO(N^3 log N)だが、Dinic法は一般に最悪計算量よりも高速に動作するので、定数倍に気をつければこれで通る。

登場する典型

実装上の注意点

  • TLEに気をつける
  • xを実数にすると計算誤差が怖いので、(N+1)倍して整数演算にする

提出: #4130939 (Rust)

/**
 * Dinic's algorithm for maximum flow problem.
 * Verified by: yukicoder No.177 (http://yukicoder.me/submissions/148371)
 * Min-cut (the second element of max_flow's returned values) is not verified.
 */

#[derive(Clone)]
struct Edge<T> {
    to: usize,
    cap: T,
    rev: usize, // rev is the position of the reverse edge in graph[to]
}

struct Dinic<T> {
    graph: Vec<Vec<Edge<T>>>,
    iter: Vec<usize>,
    zero: T,
}

impl<T> Dinic<T>
    where T: Clone,
          T: Copy,
          T: Ord,
          T: std::ops::AddAssign,
          T: std::ops::SubAssign,
{
    fn bfs(&self, s: usize, level: &mut [Option<usize>]) {
        let n = level.len();
        for i in 0 .. n {
            level[i] = None;
        }
        let mut que = std::collections::VecDeque::new();
        level[s] = Some(0);
        que.push_back(s);
        while let Some(v) = que.pop_front() {
            for e in self.graph[v].iter() {
	        if e.cap > self.zero && level[e.to] == None {
	            level[e.to] = Some(level[v].unwrap() + 1);
	            que.push_back(e.to);
                }
            }
	}
    }
    /* search augment path by dfs.
     * if f == None, f is treated as infinity.
     */
    fn dfs(&mut self, v: usize, t: usize, f: Option<T>, level: &mut [Option<usize>]) -> T {
        if v == t {
            return f.unwrap();
        }
        while self.iter[v] < self.graph[v].len() {
            let i = self.iter[v];
            let e = self.graph[v][i].clone();
            if e.cap > self.zero && level[v] < level[e.to] {
                let newf = std::cmp::min(f.unwrap_or(e.cap), e.cap);
                let d = self.dfs(e.to, t, Some(newf), level);
                if d > self.zero {
                    self.graph[v][i].cap -= d;
                    self.graph[e.to][e.rev].cap += d;
                    return d;
                }
            }
            self.iter[v] += 1;
        }
        self.zero
    }
    pub fn new(n: usize, zero: T) -> Self {
        Dinic {
            graph: vec![Vec::new(); n],
            iter: vec![0; n],
            zero: zero,
        }
    }
    pub fn add_edge(&mut self, from: usize, to: usize, cap: T) {
        let added_from = Edge { to: to, cap: cap,
                            rev: self.graph[to].len() };
        let added_to = Edge { to: from, cap: self.zero,
                            rev: self.graph[from].len() };
        self.graph[from].push(added_from);
        self.graph[to].push(added_to);
    }
    pub fn max_flow(&mut self, s: usize, t: usize) -> (T, Vec<usize>) {
        let mut flow = self.zero;
        let n = self.graph.len();
        let mut level = vec![None; n];
        loop {
            self.bfs(s, &mut level);
            if level[t] == None {
                let ret = (0 .. n).filter(|&i| level[i] == None)
                    .collect();
                return (flow, ret);
            }
            self.iter.clear();
            self.iter.resize(n, 0);
            loop {
                let f = self.dfs(s, t, None, &mut level);
                if f <= self.zero { break; }
                flow += f;
            }
        }
    }
}

fn calc(n: usize, a: &[i64], b: &[i64], pairs: &[(usize, usize)], mid: i64)
        -> (i64, i64) {
    let bias = 5001; // should be > n
    let mut din = Dinic::new(2 + n, 0);
    let ni = n as i64;
    for i in 0 .. n {
        din.add_edge(0, 2 + i, (ni - b[i]) * bias);
        din.add_edge(2 + i, 1, (ni - a[i]) * bias);
        let (u, v) = pairs[i];
        din.add_edge(2 + u, 2 + v, mid);
    }
    let (_cost, t_side) = din.max_flow(0, 1);
    let mut kind = vec![0; n];
    for t in t_side {
        if t >= 2 {
            kind[t - 2] = 1;
        }
    }
    let mut used = 0;
    for i in 0 .. n {
        let (u, v) = pairs[i];
        if (kind[u], kind[v]) == (0, 1) { used += 1; }
    }
    let mut realcost = 0;
    for i in 0 .. n {
        if kind[i] == 1 {
            realcost += b[i];
        } else {
            realcost += a[i];
        }
    }
    (realcost, used)
}

fn solve() {
    let out = std::io::stdout();
    let mut out = BufWriter::new(out.lock());
    macro_rules! puts {
        ($format:expr) => (write!(out,$format).unwrap());
        ($format:expr, $($args:expr),+) => (write!(out,$format,$($args),*).unwrap())
    }
    input! {
        n: usize,
        k: usize,
        a: [i64; n],
        b: [i64; n],
    }
    let mut fail = -1;
    let mut pass: i64 = 5001 * 5001;
    let mut pairs = vec![(0, 0); n];
    for i in 0 .. n {
        pairs[a[i] as usize - 1].0 = i;
        pairs[b[i] as usize - 1].1 = i;
    }
    while pass - fail > 1 {
        let mid = (pass + fail) / 2;
        let (_cost, pair) = calc(n, &a, &b, &pairs, mid);
        if pair <= (n - k) as i64 {
            pass = mid;
        } else {
            fail = mid;
        }
    }
    let (cost, _) = calc(n, &a, &b, &pairs, pass);
    puts!("{}\n", cost);
}

まとめ

解説とはやり方が違う。この記事の解法は完全に牛刀っぽい。ラグランジュ緩和は結構面白いテクニックなので、これを読んだ人は習得するといいことがあるかもしれません。

全国統一プログラミング王決定戦予選/NIKKEI Programming Contest 2019 E - Weights on Vertices and Edges

普通に難しかった。
E - Weights on Vertices and Edges

問題

N頂点M辺の無向グラフが与えられる。頂点と辺には重みがついている。
このグラフから辺を何本か取り去って、残っている辺がすべて以下の条件を満たすようにしたい:

  • 辺の重みをYとすると、その辺を含む連結成分の頂点の重みの合計はY以上である。

条件を満たすために取り去るべき辺の本数の最小値を求めよ。

解法

辺eについて、e以下の重みの辺を繋げたときに、繋がる頂点の重みの合計がeの重み以上となる場合、eを使える辺と呼ぶことにする。
ある辺eに着目したとき、その辺を残しても良いのは以下が成り立つ場合である:

  • ある使える辺fであって、f以下の重みの辺を繋げた時にeにつながるものが存在する
    • e自体が使える辺である場合ももちろんこの条件は満たされる

このような辺を列挙したい。単純に使える辺全てに対してDFSをするとTLEしてしまう。使える辺を重みの大きい順に見ていき、すでに残せることがわかっている辺はスキップすると、合計O(N + M)時間でできる。全体の計算量はO(N + M log M)である。

登場する典型

  • 辺ごとに独立に考える
  • DFSをうまく行い、計算量の合計をO(N + M)にする

実装上の注意点

特になし。難しいので気をつける

提出: #4111631 (Rust)

fn solve() {
    let out = std::io::stdout();
    let mut out = BufWriter::new(out.lock());
    macro_rules! puts {
        ($($format:tt)*) => (write!(out,$($format)*).unwrap());
    }
    macro_rules! debug {
        ($($format:tt)*) => ();//(eprintln!($($format)*));
    }
    input! {
        n: usize,
        m: usize,
        x: [i64; n],
        aby: [(usize1, usize1, i64); m],
    }
    let mut aby = aby;
    aby.sort_by_key(|&(_, _, y)| y);
    let mut uf = UnionFind::new(n);
    let mut wei = x.clone();
    let mut usable = vec![false; m];
    for (i, &(a, b, y)) in aby.iter().enumerate() {
        if !uf.is_same_set(a, b) {
            let a = uf.root(a);
            let b = uf.root(b);
            uf.unite(a, b);
            let nr = uf.root(a);
            let oldwei = wei[a + b - nr];
            wei[a + b - nr] = -1;
            wei[nr] += oldwei;
        }
        let root = uf.root(a);
        debug!("(a, b, y) = {} {} {}", a, b, y);
        if wei[root] >= y {
            debug!("Edge {} (wei = {}) is usable", i, y);
            usable[i] = true;
        }
    }
    let mut g = vec![vec![]; n];
    for (i, &(a, b, y)) in aby.iter().enumerate() {
        g[a].push((b, y, i));
        g[b].push((a, y, i));
    }
    let mut covered = vec![false; m];
    for i in (0..m).rev() {
        let (a, _b, y) = aby[i];
        if usable[i] {
            dfs(a, &g, y, &mut covered);
        }
    }
    let mut unusable = 0;
    for i in 0..m {
        if !covered[i] {
            unusable += 1;
        }
    }
    puts!("{}\n", unusable);
}

まとめ

最初Kruskal法の構成過程を見て、最後に条件を満たした森に適当に辺を加えればOKだと思ったけど、全然ダメだった(は?)

  • 反例: [1]-3-[1]-10-[100]

ABC116 D - Various Sushi

こういう、適切な貪欲をやる問題が苦手すぎる…。
D - Various Sushi

問題

N個の寿司があり、i個目の寿司の種類はt_i、美味しさはd_iである。この中からK個選び、美味しさd_iの総和とt_iの種類数の2乗の和を最大化したい。最大値を求めよ。

解法

寿司を美味しさの順に並べたとき、各種類について一番美味しさが高いものをinitiator、それ以外のものをfollowerと呼ぶことにする。
最適な選び方を一つ固定する。これはどういう形をしているだろうか? まず、各種類について、その種類の寿司を取ったのであれば必ずinitiatorは取っている。また、選んだfollowerよりも価値の高い選んでいないinitiatorがある場合、followerをinitiatorと取り替えることによりスコアが高くなるので、そのようなことはないとして良い。
よって、最適戦略は上からinitiatorを何個か取って、そのあとにfollowerを何個か取った形をしている。上から取るinitiatorの個数を全探索すればよい。
initatorを新しく取るたびに一番美味しさの低いfollowerを捨てる操作が高速にできればよい。これは優先度キュー、vectorなどでできる。

登場する典型

  • 貪欲
  • 最適解の形を考える

実装上の注意点

  • 新しいinitiatorを追加するたびに、2 * (今まで取ったinitiatorの個数) + 1だけスコアに加算するのを忘れない!

提出: Submission #4059339 - AtCoder Beginner Contest 116 (Rust)

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: usize,
        td: [(usize1, i64); n],
    }
    let mut dt = vec![];
    for (t, d) in td {
        dt.push((d, t));
    }
    dt.sort(); dt.reverse();
    let mut que = BinaryHeap::new();
    let mut seen = vec![false; n];
    let mut tot = 0;
    let mut kind = 0;
    for i in 0 .. k {
        let (d, t) = dt[i];
        tot += d;
        if seen[t] {
            que.push((-d, t));
        } else {
            seen[t] = true;
            tot += 2 * kind + 1;
            kind += 1;
        }
    }
    let mut ma = tot;
    for i in k .. n {
        let (d, t) = dt[i];
        // No point of picking it. Skip.
        if seen[t] { continue; }
        let profit = d + 2 * kind + 1;
        if let Some((tap, _ris)) = que.pop() {
            let tap = -tap;
            tot += profit - tap;
            seen[t] = true;
            kind += 1;
        }
        ma = max(ma, tot);
    }
    puts!("{}\n", ma);
}

まとめ

最近のABCは結構定跡の把握漏れに気付かせてくれる気がする。

DISCO presents ディスカバリーチャンネル コードコンテスト2019 本戦 参加記

楽しいコンテストだった

来社

寝坊して9:24頃到着した(受付は9:00-9:50)。運良くコンセントのある席が残っていてよかった。

本戦問題コード部門

DISCO presents ディスカバリーチャンネル コードコンテスト2019 本戦 - AtCoder
A (00m00s-07m56s): 「300にしては難しくないか?」と思った。氷以外が必ず2マス以上連続で現れるのはわかっていたが、それをうまく実装の簡略化に結びつけられなかった。
B (07m56s-18m27s): idsigmaさんのツイートを以前見ていたおかげでその時の考察を再利用できた。結局大きい方から順にpushしていって転倒数を消費することを考えればよい。大きい方をpushするとオーバーしてしまう場合は小さい方をpushする。


D (18m27s-33m45s): 実家。CとDを見てどちらに行くか迷ったが、yukicoderでhamkoしゃんが似た問題(No.510 二次漸化式 - yukicoder)を出していたので解法を秒で思いついたため、最初にDを解くことにした。これが通せて暫定3位になった時点で作戦勝ちだと思った。
C (33m45s-105m23s): 幾何。当然円の周囲に触れるところで反射させるのが一番効率的だし、ある地点まで光を飛ばせるならそこより近いところにも飛ばせるので、反射で届く(柱にブロックされない)一番遠いところを計算するだけ。ただ、幾何ライブラリを整備しておらずその場で書いたため、単純なバグに長時間苦しんだ。これは負けですね…
E (105m23s-120m00s): 構築。15分しかないのでダメみたいですね…。終了です。

本戦終了時点で凍結含め最悪18位だったので、最終的に10位になれてよかった。Cに時間をかけたせいでEみたいな面白いけど時間がかかる問題に手を出せなかったのは悲しい。

本戦問題装置実装部門

無理でした おわり

コンテスト後

いつもの勢の方々とご飯に行った。kyuriさんと一緒にyosupoさんにEの解を教えてもらったりした

まとめ

反省して幾何ライブラリを整備します (少なくとも、コピペできるようなオンラインの幾何ライブラリを見繕っておきます)