hogecoder

tsutaj 競技プログラミングの記録

大学合宿の作問を卒業した & HUPC2020 Day1 の講評とか

9/14〜9/16 に HUPC2020 が開催され、9/14 は 北大セット でした。元々は RUPC2020 の北大セットとして出す予定だったので自分もまだ作問に携わっていて、今回で大学合宿の作問は卒業ということになりました。思い返してみれば、RUPC2017 からセット提供するごとに毎回作問しているのでなんだかんだ 3 年半くらい関わったことになります (しかも HUPC 主催で作問機会を自ら増やしているという・・・)。そう考えるとなんとなく結構やったのかもという気持ちになりますね。

今回もセットを作るにあたって相当いろいろやったので、振り返ってみます (なんかこういうのやりたくなっちゃうんですよね、半分ポエムだけど許してください)

続きを読む

2018-2019 ACM-ICPC Pacific Northwest Regional Contest (Div. 1) B: Coprime Integers

この辺の (海外の ICPC 問題の) 記事書く人いなさそうなので書きます

問題概要

原文 (PDF): https://codeforces.com/gym/101982/attachments/download/7897/20182019-acmicpc-pacific-northwest-regional-contest-div-1-en.pdf

整数  A, B, C, D が与えられる。 A \leq x \leq B, C \leq y \leq D を満たす整数の組  (x, y) であって  x, y が互いに素であるものの個数を求めよ。

  •  1 \leq A \leq B \leq 10^{7}
  •  1 \leq C \leq D \leq 10^{7}
  • TL 1 sec

解法

※ 計算量を書くために、 N = \max(B, D) としています。

お察しの通り  O(N \log N) を書いても落ちると思います。それよりも時間計算量が良いものを書く必要があります。

 f(p, q) を、「 1 \leq x \leq p, 1 \leq y \leq q を満たす組  (x, y) であって  x, y が互いに素であるものの個数」とします。元の問題で要求されていることとは微妙に異なりますが、この関数を組み合わせることで元の問題を表現することができます。具体的には  f(B, D) - f(A-1, D) - f(B, C-1) + f(A-1, C-1) を考えれば良いです (二次元累積和を実装したことがあるならば、それがイメージの助けになるかと思います)

では、この関数  f はどのようにすれば高速に計算できるでしょうか?整数の組  (x, y) 1 以外で最小の公約数が  d であるとして、包除原理のように考えると以下のようになります。

  •  d に含まれる素数が奇数個ならば、引かなければならないので引く
  •  d に含まれる素数が偶数個ならば、引きすぎたぶんを足す
  • ただし、 d が平方数で割り切れる (つまりある素数  p があって  d p^{2} で割り切れる) 場合は他の結果に包含されるので考慮しない

ここで「引く」「足す」「考慮しない」など、それぞれの場合の数に対して  -1, +1, 0 といった係数がかかることになりますが、この係数はまさにメビウス関数 (Mobius function) そのものです。この関数について詳しく知りたい方は メビウスの反転公式の証明と応用 | 高校数学の美しい物語 を見てください。メビウス関数はエラトステネスのふるいを動かす際に一緒に計算できるので、元の問題は  O(N \log \log N) で解けます。

ソースコード

const int MAXN = 10000010;
int mu[MAXN + 5];
bool is_prime[MAXN + 5];

int main() {
    fill(is_prime + 2, is_prime + MAXN + 1, true);
    fill(mu, mu + MAXN + 1, 1);
    for(int i=2; i<=MAXN; i++) {
        if(!is_prime[i]) continue;
        mu[i] = -1;
        for(int k=i<<1; k<=MAXN; k+=i) {
            is_prime[k] = false;
            if((k/i) % i == 0) mu[k] = 0;
            else mu[k] = -mu[k/i];
        }
    }

    int A, B, C, D; scanf("%d%d%d%d", &A, &B, &C, &D);

    auto calc = [&](int x, int y) {
        ll res = 0;
        for(int i=1; i<=min(x, y); i++) {
            res += 1LL * (x/i) * (y/i) * mu[i];
        }
        return res;
    };

    ll ans = calc(B, D) - calc(A-1, D) - calc(B, C-1) + calc(A-1, C-1);
    printf("%lld\n", ans);
    return 0;
}

第三回アルゴリズム実技検定 O 問題: 輪投げ

問題概要

原文はこちら

atcoder.jp

棒が  N 本ある。これらのうち異なる  M 本に輪を投げ入れることを  3 回行う。投げ入れ方によって以下のように得点が定められる。

  •  i 回目に  j 番目の棒に輪を投げ入れたとき、 A_{j} \left( B_j \right)^{i} \bmod R_{i} 点を得る。これらの合計を  X とする。
  •  j 番目の棒について、全 3 回のうち  i 回その棒に輪を投げ入れたとき、 A_{j} \left( B_j \right)^{i} 点を失う。これらの合計を  Y とする。
  • 総得点は  X - Y 点である。

得られる得点を最大化せよ。最大化した得点が負になることもある。

制約

  •  1 \leq M \leq N \leq 500
  •  2 \leq A_{i}, B_{i} \leq 1000
  •  2 \leq R_{i} \leq 10^{5}

解説

おそらくどう考えても動的計画法のアプローチでは無理だと思います。他のアプローチを考えましょう。満たすべき制約がそれなりに多いので、フローを考えてみることにします。必要そうな頂点や辺を順に考えていきます。

  1. それぞれの回において、ある  M 本を選んでそこに投げなければならないので、流量  M だけ通過できるような頂点が  3 つ欲しい
  2. ある棒に関しては一度に複数回投げられないので、1. で用意した各頂点から流量  1 ずつ、棒を表す全頂点に張りたい
  3. 失点を表現できるように頂点を作りたい (後述)

符号を反転させて最小化問題として捉え、最小費用流で考えてみると、途中までは以下のようにグラフを作ると良くなります。辺に書いてある  (a, b) とは、流量が  a でコストが  b であることを表します。

※図における  A_i はすべて  A_j に置き換えて読んでください

f:id:tsutaj:20200606180236j:plain

さて、実際には失点を表現できるようにグラフを作らなければならないですが、後半のグラフはどう作ればよいでしょうか?ここで超重要なのが、 B_{i} \geq 2 であるので 失点は指数的に増える ということです。より具体的に言うと、 x \lt y ならば  \left( B^{x} - B^{x-1} \right) \lt \left( B^{y} - B^{y-1} \right) であることが重要です。

 B の差分がどんどん広がっていくわけですので、以下のように辺を張ると、上の頂点から貪欲にいくつか使用したもののみが最小費用流の解となりえることがわかるので、このように張ると良いです。

f:id:tsutaj:20200606180249j:plain

ソースコード

// フロー 略

int main() {
    int N, M; cin >> N >> M;
    vector<ll> A(N), B(N);
    for(int i=0; i<N; i++) cin >> A[i];
    for(int i=0; i<N; i++) cin >> B[i];
    vector<ll> R(3);
    for(int i=0; i<3; i++) cin >> R[i];

    Flow<ll, ll> fl(2 + 3 + N + 3*N, LONGINF, LONGINF, LONGINF);
    int source = 3 + N + 3*N, sink = source + 1;
    for(int i=0; i<3; i++) {
        fl.add_edge(source, i, M, 0);
    }
    for(int r=0; r<3; r++) {
        for(int i=0; i<N; i++) {
            ll val = A[i];
            for(int j=0; j<=r; j++) (val *= B[i]) %= R[r];
            fl.add_edge(r, 3+i, 1, -val);
        }
    }
    for(int i=0; i<N; i++) {
        ll acc = 0;
        for(int r=0; r<3; r++) {
            ll val = A[i];
            for(int j=0; j<=r; j++) (val *= B[i]);
            fl.add_edge(3+i, 3+N+(3*i+r), 1, val - acc);
            acc += val - acc;
        }
    }
    for(int i=0; i<N; i++) {
        for(int r=0; r<3; r++) {
            fl.add_edge(3+N+(3*i+r), sink, 1, 0);
        }
    }

    ll ans = fl.mincost_flow(source, sink, 3*M);
    assert(ans != LONGINF);
    printf("%lld\n", -ans);
    return 0;
}

検定の問題としては要求知識がコアだな・・・と少し思いましたが、過去問の傾向見るとこんなもんかなという気もしますね。