CODE FESTIVAL 2018 Final I - Homework

F問題は解説を見ないとわからなかったがI問題は自力で解けた。
I - Homework

問題

荷物がN個ある。i番目の荷物はコスト2^A[i]、価値B[i]である。合計価値をK以上にするために必要なコストの合計を求めよ。

制約

  • 入力は全て整数
  • 1 <= N <= 10^5
  • 0 <= A[i] <= 30
  • 1 <= B[i] <= 10^9
  • 1 <= K <= \sum B[i]

解法

二分探索をすることにして、問題を「合計コストx以下で合計価値をK以上にできるか?」という形に変形する。

ここで、xの偶奇とコスト1の荷物をどうするかに着目しよう。xが奇数であれば、少なくとも1個はコスト1の荷物を取るべきである。またxが偶数であっても奇数であっても、最初に取る荷物以外の荷物は2個ずつ取っていくべきである。これを使うと、xの各ビットを下から見て、立っていたら一番価値の高い荷物を取り、そのあとそれ以外の荷物を2個まとめてコストが2倍の荷物として扱うとよいことがわかる。
これの計算量は、ほとんどの荷物の個数が毎ターン半分になること、最大1個の荷物がそれ以外に次のターンに持ち越されることを考えると、O(N + (ターン数)) = O(N + max A[i])である。

登場する典型

  • 二分探索
  • 1, 2, 2^2, ...という等比数列の典型パターン
    • 今回は下位ビットに着目するといけた

実装上の注意点

  • 二分探索の範囲
    • 上限はN * 2^30以上にすべき
    • 上限に応じてxの大きさも変わるので、ターン数も30ではなくもっと大きくしないといけない

提出: Submission #4325140 - CODE FESTIVAL 2018 Final (Parallel) (Rust)

const B: usize = 31;
const C: usize = 50;
 
// x: max time
fn can_pack(tbl: &[Vec<i64>], k: i64, x: i64) -> bool {
    let mut rem = k;
    let mut cur_bag = tbl[0].clone();
    for i in 0..C {
        cur_bag.sort();
        if (x >> i & 1) == 1 && cur_bag.len() >= 1 {
            rem -= cur_bag.pop().unwrap();
        }
        let mut nxt = vec![];
        while let Some(mut fst) = cur_bag.pop() {
            if let Some(snd) = cur_bag.pop() {
                fst += snd;
            }
            nxt.push(fst);
        }
        if i + 1 < B {
            nxt.extend_from_slice(&&tbl[i + 1]);
        }
        cur_bag = nxt;
    }
    rem <= 0
}
 
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: i64,
        ab: [(usize, i64); n],
    }
    let mut tbl = vec![vec![]; B];
    for (a, b) in ab {
        tbl[a].push(b);
    }
    let mut pass = 1 << C;
    let mut fail = 0;
    while pass - fail > 1 {
        let mid = (pass + fail) / 2;
        if can_pack(&tbl, k, mid) {
            pass = mid;
        } else {
            fail = mid;
        }
    }
    puts!("{}\n", pass);
}

まとめ

2の冪乗系コストの問題は、たまに見ると面白い。

全国統一プログラミング王決定戦本戦 F - Flights (他人の解法を見た)

20位以内に入りたければ、A問題からE問題は自明問題として高速に処理し、この問題かG問題のどちらかを解く必要があった。
F - Flights

問題

XY平面上にN個の点がある。i番目の点の座標は(X[i], Y[i])であり、どの2点の座標も異なる。この点たちの間に、以下の規則で無向辺を設ける:

  • X[i] <= X[j], Y[i] <= Y[j]のとき、頂点iと頂点jの間にコストC[j]の無向辺を設ける。

頂点Sから頂点Tへ移動するときの最短距離を求めよ。

制約

  • 2 <= N <= 10^5
  • 0 <= 座標 <= 10^9
  • 1 <= C[i] <= 10^9

解法

最短経路がどのような形をしているか考察してみよう。X[i] <= X[j], Y[i] <= Y[j]のとき、頂点jは頂点iの右上にあるということにする。明らかに、頂点jが頂点iの右上にあり頂点kが頂点jの右上にあるときはi -> j -> kという道を通る意味はない。このことから、最短経路はジグザグになっている、つまりS -> a_1 -> a_2 -> ... -> Tを最短経路としたときにa_1はSとa_2の右上にあり、a_3はa_2とa_4の右上にあり、…ということが成り立つ。(最初に他の頂点の右上になるのがa_1ではなくSの場合もある。)
a_1, a_2, ...として選ぶ意味がある頂点はどのような頂点だろうか? a_kがa_{k-1}の右上にあると仮定すると、一般性を失わず元の点集合にa_{k-1}の左下にある点はないと仮定してよい。なぜなら、そのような点があれば、それを代わりにa_{k-1}とすればよいからである。これを使うと、最適戦略においては以下の2種類の辺しか使われないことがわかる:

  • 各点vについて、vの左下にある頂点wであって、それより左下の点が存在しないような点wへの辺(v, w)
  • SまたはTの右上か左下にある点vへの辺(S, v)あるいは(T, v)

後者の辺はO(N)本しかないのでよい。前者について、もともと頂点集合を座標の辞書順にソートしておけば、辺を伸ばすべき頂点の列はならしO(1)で管理できる。しかし、頂点集合の大きさはO(N)程度であるため、このままではO(N^2)本の辺ができてしまう。これは以下のようにして解決できる。
一般性を失わず(Sの座標) < (Tの座標)とする。最適戦略はジグザグに右に進んでいく戦略である。このとき、辺を伸ばすべき頂点の列をKとして、各点vに対してKのどの点へ辺を伸ばせるかを考える。Kは左上から右下へと進んでいく点列で、座標でソートしている以上Kにはv以下のX座標を持つ点しか登場しないので、vから辺を伸ばせる頂点はY座標がvのY座標以下の点たち、つまりKの途中から最後まで、あるいは空列である。この列の先頭と最後をそれぞれst, enと呼ぶことにし、有向辺st -> v, v -> en を張り、enからstへ向かってKの頂点の間に逆向きに大きさ0の有向辺を張ることにする。(座標空間内では右下から左上に辺が伸びる。) こうすると、上で考慮した辺を張るのと同等であることがわかる。大きさ0の有向辺は別のvに対しても共有できるので、合計で張られる辺の総数はO(N)である。
こうしてできたグラフの上でダイクストラ法をすれば答えが計算できる。計算量はO(N log N)である。

登場する典型

  • 最適戦略の形を考える

実装上の注意点

  • 問題のグラフは無向グラフだが、長さ0の有向辺を追加するのでSとTの順番には気をつける必要がある。

提出: Submission #4316925 - 全国統一プログラミング王決定戦本戦 (Rust)

// Dijkstra省略

fn calc(xyc: &[(i64, i64, i64)],
        pool: &[(i64, i64, i64, usize)], s: usize, t: usize)
        -> i64 {
    let n = pool.len();
    let mut edges = vec![];
    let mut dedges = vec![];
    let mut st: Vec<(i64, usize)> = vec![];
    for &(_x, y, cost, curi) in pool {
        let mut added = true;
        if let Some(&(top, _idx)) = st.last() {
            if top <= y {
                added = false;
            }
        }
        if added {
            st.push((y, curi));
            if st.len() >= 2 {
                let other = st[st.len() - 2].1;
                dedges.push((curi, other, 0)); // reverse edge
            }
        } else {
            let mut fail = -1;
            let mut pass = st.len() as i64 - 1;
            while pass - fail > 1 {
                let mid = ((pass + fail) / 2) as usize;
                let (ym, _idxm) = st[mid];
                if ym <= y {
                    pass = mid as i64;
                } else {
                    fail = mid as i64;
                }
            }
            let pass = pass as usize;
            dedges.push((st[pass].1, curi, cost));
            dedges.push((curi, st[st.len() - 1].1, cost));
        }
    }
    for &v in &[s, t] {
        for i in 0..n {
            if i == v { continue; }
            if xyc[i].0 <= xyc[v].0 && xyc[i].1 <= xyc[v].1 {
                edges.push((i, v, xyc[v].2));
            }
            if xyc[i].0 >= xyc[v].0 && xyc[i].1 >= xyc[v].1 {
                edges.push((i, v, xyc[i].2));
            }
        }
    }
    let mut dijk = Dijkstra::new(n);
    for (u, v, x) in edges {
        dijk.add_edge(u, v, x);
        dijk.add_edge(v, u, x);
    }
    for (u, v, x) in dedges {
        dijk.add_edge(u, v, x);
    }
    let ans = dijk.solve(s, t, 1 << 57);
    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,
        s: usize1,
        t: usize1,
        xyc: [(i64, i64, i64); n]
    }
    let mut s = s;
    let mut t = t;
    // s comes before t in the lexicographical order
    if xyc[t] < xyc[s] {
        std::mem::swap(&mut s, &mut t);
    }
    let mut pool = vec![];
    for i in 0..n {
        let (x, y, c) = xyc[i];
        pool.push((x, y, c, i));
    }
    pool.sort();
    let ans = calc(&xyc, &pool, s, t);
    puts!("{}\n", ans);
}

まとめ

本番重み0の辺を張ればよさそうなことまでは気付けたが、逆向きの有向辺にすることを思いつかず爆死した。kanasii
この解法はcatupperさんのツイートを参考にして書いた。

全国統一プログラミング王決定戦本戦 D - Deforestation

本番は平面走査で通したけど、セグメント木で殴る解法にもちゃんと気付くべきだった。
D - Deforestation

問題

N本の竹がある。各竹の時刻0における高さは0で、毎秒1ずつ伸びる。
以下の行動をi = 1, ..., NのN回行うとき、総得点を求めよ。

  • 時刻T[i]に、L[i]番目からR[i]番目までの竹を根元から切る。それらの竹の長さの合計を得点とする。切られた竹も、その後伸び続ける。

解法

数列の上の以下のようなクエリを捌ければよい。

  • 区間和クエリ (L番目からR番目までの和を求める)
  • 区間ゼロ初期化クエリ (L番目からR番目までを0にする)
  • 区間加算クエリ (1番目からN番目までに1を加える)

これは以下のような遅延セグメント木で処理できる。

  • 要素: i64
  • 更新: (i64, i64)、(a, b)は要素xをax + bに変更するクエリを表す。

単位元および演算は以下の通り。

  • 要素の単位元: 0
  • 更新の単位元: (1, 0) (x |-> 1x + 0が恒等写像であるため)
  • 要素の演算: (+)
  • 更新の作用: ノードの高さ(根からの距離)をhとすると、xに(a,b)を作用させた結果はax + b * 2^h (理由: そのノードは2^h個の要素の和が格納されているため)
  • 更新の演算: (a, b) * (c, d) = (ac, bc + d) (理由: yにx |-> ax + bとx |-> cx + dをこの順で適用すると、acy + bc + dになるため)

以上を遅延セグメント木に落とし込むとできる。これらの演算を書いた遅延セグメント木を書くのは大変なので、演算部分を除いて抽象化したものをライブラリにして、演算はtraitの実装という形で与えるのがよいだろう。

登場する典型

  • 遅延セグメント木

実装上の注意点

  • 更新の演算でバグらせないように気をつける。今回の場合、「ゼロクリア」と「区間加算」を同時に処理する構造を考えようとするとバグらせるに決まっているので、両方を含む代数構造を考えるとよい。

提出: Submission #4309587 - 全国統一プログラミング王決定戦本戦 (Rust)

/**
 * Lazy Segment Tree. This data structure is useful for fast folding and updating on intervals of an array
 * whose elements are elements of monoid T. Note that constructing this tree requires the identity
 * element of T and the operation of T. This is monomorphised, because of efficiency. T := i64, biop = max, upop = (+)
 * Reference: http://d.hatena.ne.jp/kyuridenamida/20121114/1352835261
 * Verified by https://codeforces.com/contest/1114/submission/49759034
 */
pub trait ActionRing {
    type T: Clone + Copy; // data
    type U: Clone + Copy + PartialEq + Eq; // action
    fn biop(Self::T, Self::T) -> Self::T;
    fn update(Self::T, Self::U, height: usize) -> Self::T;
    fn upop(Self::U, Self::U) -> Self::U;
    fn e() -> Self::T;
    fn upe() -> Self::U; // identity for upop
}
pub struct LazySegTree<R: ActionRing> {
    n: usize,
    dep: usize,
    dat: Vec<R::T>,
    lazy: Vec<R::U>,
}

impl<R: ActionRing> LazySegTree<R> {
    pub fn new(n_: usize) -> Self {
        let mut n = 1;
        let mut dep = 0;
        while n < n_ { n *= 2; dep += 1; } // n is a power of 2
        LazySegTree {
            n: n,
            dep: dep,
            dat: vec![R::e(); 2 * n - 1],
            lazy: vec![R::upe(); 2 * n - 1]
        }
    }
    #[inline]
    fn lazy_evaluate_node(&mut self, k: usize, height: usize) {
        if self.lazy[k] == R::upe() { return; }
        self.dat[k] = R::update(self.dat[k], self.lazy[k], height);
        if k < self.n - 1 {
            self.lazy[2 * k + 1] = R::upop(self.lazy[2 * k + 1], self.lazy[k]);
            self.lazy[2 * k + 2] = R::upop(self.lazy[2 * k + 2], self.lazy[k]);
        }
        self.lazy[k] = R::upe(); // identity for upop
    }
    #[inline]
    fn update_node(&mut self, k: usize) {
        self.dat[k] = R::biop(self.dat[2 * k + 1], self.dat[2 * k + 2]);
    }
    fn update_sub(&mut self, a: usize, b: usize, v: R::U, k: usize, height: usize, l: usize, r: usize) {
        self.lazy_evaluate_node(k, height);

        // [a,b) and  [l,r) intersects?
        if r <= a || b <= l {return;}
        if a <= l && r <= b {
            self.lazy[k] = R::upop(self.lazy[k], v);
            self.lazy_evaluate_node(k, height);
            return;
        }

        self.update_sub(a, b, v, 2 * k + 1, height - 1, l, (l + r) / 2);
        self.update_sub(a, b, v, 2 * k + 2, height - 1, (l + r) / 2, r);
        self.update_node(k);
    }
    /* ary[i] = upop(ary[i], v) for i in [a, b) (half-inclusive) */
    #[inline]
    pub fn update(&mut self, a: usize, b: usize, v: R::U) {
        let n = self.n;
        let dep = self.dep;
        self.update_sub(a, b, v, 0, dep, 0, n);
    }
    /* l,r are for simplicity */
    fn query_sub(&mut self, a: usize, b: usize, k: usize, height: usize, l: usize, r: usize) -> R::T {
        self.lazy_evaluate_node(k, height);

        // [a,b) and  [l,r) intersect?
        if r <= a || b <= l {return R::e();}
        if a <= l && r <= b {return self.dat[k];}
        let vl = self.query_sub(a, b, 2 * k + 1, height - 1, l, (l + r) / 2);
        let vr = self.query_sub(a, b, 2 * k + 2, height - 1, (l + r) / 2, r);
        self.update_node(k);
        R::biop(vl, vr)
    }
    /* [a, b) (note: half-inclusive) */
    #[inline]
    pub fn query(&mut self, a: usize, b: usize) -> R::T {
        let n = self.n;
        let dep = self.dep;
        self.query_sub(a, b, 0, dep, 0, n)
    }
}

struct Monoid {}
impl ActionRing for Monoid {
    type T = i64;
    type U = (i64, i64);
    fn e() -> i64 { 0 }
    fn upe() -> Self::U { (1, 0) }
    fn biop(a: i64, b: i64) -> i64 {
        a + b
    }
    fn update(a: Self::T, (b, c): Self::U, height: usize) -> i64 {
        a * b + (c << height)
    }
    fn upop((a, b): Self::U, (c, d): Self::U) -> Self::U {
        (a * c, b * c + d)
    }
}

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,
        tlr: [(i64, usize1, usize)],
    }
    let mut st = LazySegTree::<Monoid>::new(n);
    let mut last_t = 0;
    let mut tot = 0;
    for (t, l, r) in tlr {
        let diff = t - last_t;
        st.update(0, n, (1, diff));
        tot += st.query(l, r);
        st.update(l, r, (0, 0));
        last_t = t;
    }
    puts!("{}\n", tot);
}

まとめ

遅延セグメントツリーは実装が大変なので、うまい実装を書けるかどうかはライブラリ作成者の腕が問われる気がする。

NJPC2017 F - ダブルス (解説を見た)

何もわからなかったので解説を見た。
F - ダブルス

問題

無限に伸びる数直線上に2人の人間が立っている。2人の初期位置は0である。この2人は協力して以下の条件を満たしたい:

  • 時刻0以降に散発的に光点が現れるので、2人のうちのどちらかがその光点を叩く。光点は合計N回現れ、i番目の光点は時刻T[i]のときに場所X[i]に現れる。2人は速度V以下で動くことができる。

この条件を満たせるようなVの最小値を求めよ。

制約

  • 1 <= N <= 10^5
  • 1 <= T[i] <= 10^9
  • -10^6 <= X[i] <= 10^6
  • T[i]は狭義単調増加

解法

Vについての二分探索で判定問題に変換する。(「速度V以下で条件を満たせるか?」という問題に変換できる。)
dp[i][j] = (2人がi, jにいることがあり得るかどうか)というDPでこの問題を解くことはできるが、O(N^2)時間かかる。
そこで、状態のうち一つを圧縮して、dp[i] = (i番目までの光点が現れ、一人がiにいる場合、その時もう一人が存在し得る範囲)とするとうまく行くことが証明できる。遷移は以下の通り(dp[i] = [l, r]とし、D = V(T[i + 1] - T[i])を、時刻T[i]からT[i+1]の間に2人が移動可能な距離とする):

  • iにいる人がi+1に行く場合、dp[i] <- [l - D, r + D]
  • iにいない方の人がi+1に行く場合、dp[i] <- [X[i] - D, X[i] + D]

どちらか一方しか実現しない場合はそちらの区間を、両方実現する場合は両方の区間のunionをdp[i + 1]とすればよい。(unionが実際に区間になることの証明は解説を参考にしてください)
遷移はO(1)なので、この問題はO(N * log(V))で解ける。

登場する典型

  • 二分探索で最大化問題を判定問題に変換する
  • bool値のDPの状態の一つを値に持って行く

実装上の注意点

  • 2分探索のループ回数に気をつける
    • 時間に余裕があるので100回くらいやればよい
  • 最初時刻0のとき位置0にいる
    • 番兵などを使うと実装しやすい


提出: Submission #4272588 - NJPC2017 (C++)

// This solution is made after the author read the editorial.
bool ok(int n, const vector<double> &t, const vector<double> &x, double v) {
  vector<pair<double, double> > dp(n + 1);
  const double INF = 1e18;
  dp[0] = make_pair(0.0, 0.0);
  REP(i, 0, n) {
    double dist = v * (t[i + 1] - t[i]);
    dp[i + 1] = make_pair(INF, -INF);
    if (abs(x[i + 1] - x[i]) <= dist) {
      dp[i + 1].first = min(dp[i + 1].first, dp[i].first - dist);
      dp[i + 1].second = max(dp[i + 1].second, dp[i].second + dist);
    }
    if (dp[i].first - dist <= x[i + 1] && x[i + 1] <= dp[i].second + dist) {
      dp[i + 1].first = min(dp[i + 1].first, x[i] - dist);
      dp[i + 1].second = max(dp[i + 1].second, x[i] + dist);
    }
    if (dp[i + 1].first > dp[i + 1].second) return false;
  }
  return true;
}

int main(void) {
  ios::sync_with_stdio(false);
  cin.tie(0);
  int n;
  cin >> n;
  vector<double> t(n + 1), x(n + 1);
  REP(i, 0, n) cin >> t[i + 1] >> x[i + 1];
  double pass = 1e7, fail = 0;
  REP(i, 0, 100) {
    double mid = (pass + fail) / 2;
    if (ok(n, t, x, mid)) {
      pass = mid;
    } else {
      fail = mid;
    }
  }
  cout << setprecision(15) << pass << endl;
}

まとめ

最初実家DP + CHTかなと思ったが、CHTは大嘘で、ちゃんと問題の性質を調べる必要がある問題だった…。

「みんなのプロコン 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));
}

まとめ

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