A - Uncommon/みんプロ2018本戦

問題概要

集合A = \{a_1, a_2, ..., a_N\}が与えられる。
各整数k = 1, 2, ..., Mについて、kと互いに素であるAの要素の個数を求めよ。

制約

1 \leq N, M, a_i \leq 10^5

解法

愚直にやると少なくともO(N*M)かかってしまうのでまとめて数え上げるなどの高速化が必要になります。

kを固定したときに、kとa_iが互いに素であるというのは、gcd(k, a_i) = 1であるというのが定義ですがこの条件はまとめて数え上げようとするときには少し使いにくいので別の同値な条件に言い換えてみます。
k = p_1^{e_1}  p_2^{e_2}  ...  p_t^{e_t}
という感じにkを素因数分解したとします。(p_jは素数)
このとき、kとa_iが互いに素であることの必要十分条件は、a_iがどのp_jでも割りきれないこととなります。逆に、kとa_iが互いに素でないことは、a_iがあるp_jで割り切れることと同値です。

この事実を使ってkと互いに素でないa_iの個数を求めることにします。その個数をn(k)とします。
A_{x}をxで割り切れるa_i全体の集合と定義すると、求めたいのは
n(k) = \#(\cup_{j} A_{p_j})
となります。和集合で表されているものは一般には数え上げには使い辛いので、包除原理を使って書き直してやると

n(k) = \sum_{i} \#(A_{p_i}) - \sum_{i < j} \#(A_{p_i p_j}) + \sum_{i < j < k} \#(A_{p_i p_j p_k}) - ...

と求めることが出来ます。

後は、\#(A_{p_i})とか\#(A_{p_i p_j})とかをどうやって求めるのかとか、それをn(k)に足したり引いたりするのをどうするのかというところです。
まず初めに、エラトステネスの篩風の前処理をしておくと各整数kに含まれる異なる素因数の積やその個数などをO(MlogM)で求めて置くことができます。
それをやっておいてから、整数dを2,3,...と動かしていき、d = (dに含まれる異なる素因数の積)となっていれば上で書いたような\#(A_{p_i p_j})みたいな感じの求めるべきものなのでそこをちゃんと数えます。つまりAに含まれるdの倍数を数えます。ただし、愚直にやるとここでO(N)かかってしまうので最初にbool配列を取っておいてそこにAの要素があるないのフラグを立てておくとdの倍数ごとにスキップしながら見るということが出来るので、全体での計算量がM + M/2 + M/3 + ... = O(MlogM)程度になってくれます。そしてこれを求めたらdに含まれる素因数の個数偶奇に応じて+か-か決めて、またdの倍数ごとにスキップしながら足し引きすべきn(k)に対してそれを足したり引いたりします。

こうやってやるとO(MlogM)程度で解くことが出来てちゃんと間に合うという感じになりました。

ソースコード

適宜省略

void main() {
    int n, m;
    scan(n, m);
    auto a = iota(n).map!(i => readln.chomp.to!int).array;

    auto b = new bool[](a.reduce!max + 1);

    foreach (ai ; a) {
        b[ai] = 1;
    }

    auto pd = new int[](m + 1);
    auto pcn = new int[](m + 1);
    pd[] = 1;

    foreach (p ; 2 .. m + 1) {
        if (pd[p] == 1) {
            for (int d = p; d < m + 1; d += p) {
                pd[d] *= p;
                pcn[d]++;
            }
        }
    }

    auto cnt = new int[](m + 1);

    foreach (i ; 2 .. m + 1) {
        if (pd[i] < i) continue;
        int res = 0;

        for (int d = i; d < b.length.to!int; d += i) {
            if (b[d]) res++;
        }

        int diff = res * (-1)^^((pcn[i] & 1) ^ 1);

        for (int d = pd[i]; d < m + 1; d += pd[i]) {
            cnt[d] += diff;
        }
    }

    writeln(n);
    foreach (i ; 2 .. m + 1) {
        writeln(n - cnt[i]);
    }
}

B - だんだん強く

https://beta.atcoder.jp/contests/dwacon2018-final-open/tasks/dwacon2018_final_b

ニコ生の公式解説でちらっと触れていた解法が凄くスマートだったので復習を兼ねて書いておきます。

解法

与えられた[v_1, v_2, ..., v_n]を(K+1)倍に拡張した次のような配列を考えます。Cを十分大きな整数として、
[v_1 + K*C, v_1 + (K-1)*C, ..., v_1, v_2 + K*C, v_2 + (K-1)*C, ...]
とします。この配列に対して最長増大部分列問題(LIS)をO(NlogN)で解くおなじみのアルゴリズムを適用して終わりです。

配列のサイズが(K+1)Nになるので、計算量はO(KNlog(KN)) = O(KNlogN)になります。

なぜこれで解けるのか?

(K+1)倍に拡張した新たな配列は一体どういうものかと考えると、元の配列vを十分大きなギャップを持たせて(K+1)階の層に分けるようなことをしています。
そうすると、「増大していかなければいけない」というルールに違反することを「層を1つ上に上がる」ということに対応させることが出来ます。
例えば入力例1(K=2, v = [4,1,6,2,8,5,7,3])だと以下のようなイメージになります。

f:id:nanae1914:20180203205305p:plain

一番下の層は違反する余地が2回あって、真ん中は1回、一番上まで来るともう違反できないという感じになっています。
そして1つの解である[4,1,2,5,7,3]に対応するのが矢印で示された列になっていて確かにこれで上手くいくなということが分かります。

感想

初め問題を見たときは違反した回数ごとのセグメント木を持って何かごちゃごちゃやるんだろうなとか思って(実際そっちも1つの想定解らしいです)何かよく分からないなと思っていたのですが、発想1つでこんなにもスマートに解くことができるんだなあと感心しました。

コード

適宜省略

void main() {
    int n, k;
    int[] v;

    scan(n, k);
    v = readln.split.to!(int[]);

    immutable long c = 10L^^9 + 1;

    auto u = new long[](n*(k+1));

    foreach (i ; 0 .. n*(k+1)) {
        u[i] = v[i/(k+1)] + c*(k - i%(k+1));
    }

    immutable long inf = long.max;

    auto dp = new long[](n*(k+1) + 1);
    dp[] = inf;
    dp[0] = -1;

    long ans;

    foreach (i ; 0 .. n*(k+1)) {
        auto j = dp.assumeSorted.lowerBound(u[i]).length;
        dp[j] = u[i];
        ans = max(ans, j);
    }

    writeln(ans);
}

No.643 Two Operations No.2

No.643 Two Operations No.2 - yukicoder

考察

xとyの比を取って、p = x/yと置くとx=yにするということはp=1にするということと同値であることが分かります。
(ただし、y=0のときはp=∞であると考えることにします。また(x, y) = (0, 0)は少し特殊なので無いものとします)
このpで与えられた各操作を考えてみると、

  • 操作1:p -> 1/p
  • 操作2:p -> (p+1)/(p-1)

にすることであるということが計算によって分かります。この操作を繰り返してp=1にするゲームだと思えばいいことになります。

p=1に1ステップで行ける状態を考えてみます。操作1は1を1にするだけなので考えてもしょうがないです。操作2は(p+1)/(p-1)=1となるpを求めることになりますが本来ならそのようなpは存在しません……
ですがp=∞とすると、(∞+1)/(∞-1) = ∞/∞ = 1なのでp=∞からp=1へは1ステップで行けることが分かりました。(この計算は本当はめちゃくちゃなので、元の(x,y)で計算して確かにこれが正しいことを示す必要があります)

次にp=∞に1ステップで行ける状態を考えてみます。操作1を考えると1/0=∞なので0から1ステップで行けます。操作2を考えると分母が0になるpということなのでp=1から操作2で1ステップで行けます。(本当はめちゃくちゃ)

さらにp=0に1ステップで行ける状態。操作1では1/∞=0なので∞から0へ1ステップで。操作2ではp=-1のとき分子が0になるので、-1から0へ1ステップで。

さらにp=-1に1ステップで行ける状態。操作1では明らかに-1から-1。操作2では(p+1)/(p-1)=-1を解くと、p=0。0から-1へ1ステップ。

ここで状態がもう増えなくなったので結局どういうことになっているかをグラフにまとめると以下のようになっています。

f:id:nanae1914:20180203135735p:plain

よって、与えられた(x,y)からpを求めて、それがこのグラフに乗っていればその最短距離を返す。
これ以外の点であれば、これらの操作を繰り返してもp=1に到達することはできないので-1を返します。

感想

とても難しい★2だと思いました。コンテスト中には解けませんでしたが……楽しかったです。

この考え方は、testestestさんがまとめていてくれている「射影空間へうつす」という奴に対応しているはずでそれを使うと今回の議論が正当化できるはずです……多分。

http://sugarknri.hatenablog.com/entry/2018/02/03/021632