AtCoder Regular Contest 090: F - Number of Digits

https://atcoder.jp/contests/arc090/tasks/arc090_d

問題概要

正整数nに対してf(n)をnの10進表記での桁数と定義する。与えられる整数Sに対してf(l)+f(l+1)+...+f(r)=Sを満たすような(l, r)の組が何通り存在するか求めよ。

S <= 108

解法

まず桁数が少しでも大きくなると「ある桁全体を覆うような区間」はそれだけで簡単にSを超えてしまって解になりえない。具体的には8桁以上になると桁全体を覆う区間はSの上限108を超える( (108 - 107) * 8 ≒ 7×108 )。よってf(l)>=8のケースでは区間がある桁をまるまるまたぐということがありえないため、f(l)とf(r)の差はたかだか1にしかならない。これを元に以下のように場合分けをする。

(1) f(l) < 8 のとき

とりうる区間が結構面倒な形になりうるが、ここまでの大きさなら数列を陽に持った上で尺取りをやる形の全探索ができる。rの方は8桁の方に少し飛び出す可能性があることに注意する(厳密にやらずとも適当に5×107くらいまで持っておけばまあ足りる)。

(2) f(l) >= 8 のとき

区間の長さに注目すると、まずありうる区間長の範囲は1~S/8となる (f(l)の下限が8 => 長さ1の区間の最小値が8)。ここで区間長kを定めたとき、上述のとおりf(l)とf(r)の差はたかだか1であることから、f(l), f(l+1), ..., f(r) の並びは [S/k], [S/k], [S/k], [S/k]+1, [S/k]+1 のような形になる([]はここでは切り捨ての意)。つまり後半の(S%k)個が[S/k]+1, 前半の(K-S%K)個が[S/k]という形である。Sがkで割り切れないとき、このようなfの並びはただ一通りに定まる。よって1<=k<=S/8かつSの約数でないkの数だけ答えに+1が加わる。一方でSがkで割り切れるときはすべてが[S/k]となる。これはつまりf(l)=f(r)となるケースで、このときはその桁の中で好きに区間をとることができる。なのでこのケースをカバーするためにはSの約数を全部求めてf(l)を列挙すればよい。それぞれのf(l)について具体的な数は (l桁の数の個数 - 区間長 + 1) となる。このとき区間長が上述の範囲に収まっているかチェックする必要があることに注意する。

感想

説明ができなすぎる 解説放送を見て通したのでわかりたい人はそっちをみてくれ 小さいものだけ全探索で大きいときサボるといえば結構あるあるに聞こえるがサボり方がなんか自分にとってかなり非自明だった

コード (D言語)

import std.stdio, std.array, std.string, std.conv, std.algorithm;
import std.typecons, std.range, std.random, std.math, std.container;
import std.numeric, std.bigint, core.bitop, core.stdc.string;

immutable long MOD = 10^^9 + 7;

long powmod(long a, long x, long m) {
    long ret = 1;
    while (x) {
        if (x % 2) ret = ret * a % m;
        a = a * a % m;
        x /= 2;
    }
    return ret;
}

long count(int x) {
    return ((powmod(10, x, MOD) - powmod(10, x-1, MOD)) % MOD + MOD) % MOD;
}

void main() {
    auto S = readln.chomp.to!int;
    long ans = 0;

    // f(l) <= 7
    {
        auto A = new int[](10^^7*5);
        for (int k = 1; k <= 8; ++k) {
            for (int i = 10^^(k-1); i < 10^^k && i < 10^^7*5; ++i) {
                A[i] = k;
            }
        }

        for (int l = 1, r = 1, cnt = 0; l < 10^^7; cnt -= A[l], l += 1) {
            if (r < l) r = l, cnt = 0;
            while (cnt < S) cnt += A[r], r += 1;
            if (cnt == S) (ans += 1) %= MOD;
        }
    }

    // f(l) >= 8
    {
        int[] factors;
        int T = S / 8;

        for (int i = 1; i * i <= S; ++i) {
            if (S % i != 0) continue;
            if (i * i != S) factors ~= S / i;
            factors ~= i;
        }

        foreach (f; factors) {
            if (S / f < 8) continue;
            (ans += count(S / f) - f) %= MOD;
            ans = (ans + MOD) % MOD;
        }

        (ans += T) %= MOD;
    }

    ans.writeln;
}

Codeforces Round #562 (Div. 1) : B. Good Triple

https://codeforces.com/contest/1168/problem/B

問題概要

各文字が0または1のどちらかからなる長さNの文字列Sが与えられる。1 <= l <= r <= N を満たす整数の組(l, r)のうち、Sの区間[l, r]の中に同じ文字が3文字以上等間隔で並んでいるものの数を求めよ。

N <= 3×105

解法

なんかこの条件を満たさないのはすごい難しいんじゃないかと考えて小さいNで実験コードを書いてみると10文字以上くらいになると必ず条件を満たす3文字が存在してしまうことがわかる。よって長さ10程度までの区間を全探索して条件を満たさないものを全選び方の総数から引けばよい。

感想

本番中、実験まではやっていたのになぜかそこから全探索にいかず(かなしいね)

コード (D言語)

import std.stdio, std.array, std.string, std.conv, std.algorithm;
import std.typecons, std.range, std.random, std.math, std.container;
import std.numeric, std.bigint, core.bitop, core.stdc.string;

void main(){
    auto S = readln.chomp;
    auto N = S.length.to!int;
    long ans = 1L * N * (N + 1) / 2;

    foreach (l; 0..N) foreach (r; l..min(N, l+10)) {
        bool ok = true;
        foreach (i; l..r+1) for (int j = 1; i - j >= l && i + j <= r; ++j) {
            if (S[i-j] == S[i] && S[i] == S[i+j]) ok = false;
        }
        ans -= ok;
    }

    ans.writeln;
}

Educational Codeforces Round 64: D. 0-1-Tree

https://codeforces.com/problemset/problem/1156/D

問題概要

N頂点の木が与えられる。木の各辺は0または1のコストを持つ。単純パスで、かつ辺が「0個以上の0コスト辺」→「0個以上の1コスト辺」の順序で並んでいるパスをvalidと呼ぶ。頂点x->yへの単純パスがvalidとなるような頂点の組(x, y)の数を求めよ。

N <= 2×105

解法

UnionFindを2つ作って、「0コスト辺だけを見たときの連結成分」と「1コスト辺だけを見たときの連結成分」を別々に作る。このとき同じ連結成分に属している頂点同士のペアは明らかにvalidであるので、連結成分ごとにこれをカウントする。単純な掛け算だが、ペアの順番はどちらもとれることに注意する(<x,y>も<y,x>も取れる)。次に各頂点ごとに「その頂点が属す0コスト辺での連結成分」と「その頂点が属す1コスト辺での連結成分」を見ると、前者から後者へのパスは「今見ている頂点を境に使う辺が0から1へと切り替わる」ようなvalidパスとなり、これも集合サイズの単純な掛け算でカウントできる(今見ている頂点自身は数に含めないことに注意する)。このような数え方は明らかに漏れ・ダブリがない。よってこれらを合計したものが答え。

感想

なんか木DPを考え始めちゃってだめだった それでなくてもUFを2つ持つという発想ができたかどうか、、

参考にした記事

https://cinnamo-coder.hatenablog.com/entry/2019/05/02/021544

コード (D言語)

import std.stdio, std.array, std.string, std.conv, std.algorithm;
import std.typecons, std.range, std.random, std.math, std.container;
import std.numeric, std.bigint, core.bitop, core.stdc.string;

void main() {
    auto N = readln.chomp.to!int;
    auto G = new Tuple!(int, int)[][](N);
    foreach (i; 0..N-1) {
        auto s = readln.split.map!(to!int);
        G[s[0]-1] ~= tuple(s[1]-1, s[2]);
        G[s[1]-1] ~= tuple(s[0]-1, s[2]);
    }
    
    auto uf0 = new UnionFind(N);
    auto uf1 = new UnionFind(N);
    foreach (i; 0..N) foreach (to; G[i]) (to[1] == 0 ? uf0 : uf1).unite(i, to[0]);

    long ans = 0;
    foreach (i; 0..N) if (uf0.find(i) == i) ans += 1L * -uf0.table[i] * (-uf0.table[i]-1);
    foreach (i; 0..N) if (uf1.find(i) == i) ans += 1L * -uf1.table[i] * (-uf1.table[i]-1);
    foreach (i; 0..N) ans += 1L * (-uf0.table[uf0.find(i)] - 1) * (-uf1.table[uf1.find(i)] - 1);

    ans.writeln;
}


class UnionFind {
    int N;
    int[] table;

    this(int n) {
        N = n;
        table = new int[](N);
        fill(table, -1);
    }

    int find(int x) {
        return table[x] < 0 ? x : (table[x] = find(table[x]));
    }

    void unite(int x, int y) {
        x = find(x);
        y = find(y);
        if (x == y) return;
        if (table[x] > table[y]) swap(x, y);
        table[x] += table[y];
        table[y] = x;
    }
}

AtCoder Regular Contest 040: D - カクカク塗り

https://atcoder.jp/contests/arc040/tasks/arc040_d

問題概要

N行N列のグリッドが与えられる。グリッドの各マスは床もしくは障害物である。指定されたマスからスタートして「現在の向きに直進し、90度どちらかに向きを変える」という操作を繰り返してすべての床マスを訪れることが可能かどうかを判定せよ。ただし向きは上下左右のいずれかであり、開始時の向きは自由に決められる。また同じマスを2度以上訪れてはならず、グリッドの外や障害物への移動は不可とする。

N <= 400

解法

最終的な経路を想像すると、スタート・ゴールを除く各床マスからは上下どちらかに1本、左右どちらかに1本、の計2本の辺が生えている(スタート・ゴールからは上下左右どこかに1本)。この数に合うよう辺を張ったうえでグラフが連結であれば題意を満たす経路が存在することになる。

また辺の張り方に関しては自由度がほぼない。たとえば#....#のような行があった場合、全部の床に左右辺を生やすには「2マス目と3マス目」「4マス目と5マス目」というように端から貪欲にペアを組んでいくしかない。

よって「開始時の向き」「ゴールの位置」「ゴールの向き」を決め打つと、あとは自動的に上下・左右のそれぞれの辺の生やし方が決定するため、そのとおりに辺を張ってグラフが連結になるかをチェックすれば問題を解くことができる。ゴールの位置を全探索すると決め打ちにO(N2), グラフの構築・連結チェックにO(N2)のO(N4)かかって、これで部分点は通る。

満点解法ではゴールの位置を枝刈りする。まず前提として、ある行や列に存在する床マスの数は基本的には偶数でなければならない(2つずつペアにして辺を張っていくので)。ただしスタート・ゴールマスのある行/列は例外であり、たとえばスタートマスが左向きの場合上下の辺は出ないので、スタートマスは列に関しては障害物マスと同様の扱いになって、実質的にその列のマス数がマイナス1される。ゴールマスも同様である。つまりすべての行/列の床マス数は偶数でなければならないが、スタートマス・ゴールマスには行もしくは列どちらかひとつの床マス数を1減らす効果がある、ということになる。なのでスタートマスの向きを決め打った時点でゴールマスを置くべきなのは「床マスが奇数の行」「床マスが奇数の列」のどちらかであり、しかもそれら以外の行・列はすべて偶数になっている必要がある。これでゴールの位置が「1行のどこか」もしくは「1列のどこか」に絞れたのでNがひとつ落ちてO(N3)になる。

感想

説明が難しすぎる 公式解説を見たほうが図もわかりやすいし全然早いですすいません、あと実装もなんかこまごましたところがめちゃくちゃつらくてつらかった(gotoまで使ってしまった)

コード (C++)

#include <bits/stdc++.h>
using namespace std;
#define REP(i,n) for (int i=0;i<(n);i++)
#define REP2(i,m,n) for (int i=m;i<(n);i++)
typedef long long ll;

const int dr[4] = {0, 0, -1, 1}; // LRUD
const int dc[4] = {-1, 1, 0, 0}; // LRUD

int N;
string A[404];
int row_parity[404];
int col_parity[404];
int rp = 0, cp = 0;
int sr, sc;
int empty_count;

class UnionFind {
public:
    vector<int> table;
    UnionFind(int n) {
        table = vector<int> (n, -1);
    }

    void unite(int x, int y) {
        x = find(x);
        y = find(y);
        if (x == y) return;
        if (table[x] >= table[y]) {
            table[y] += table[x];
            table[x] = y;
        } else {
            table[x] += table[y];
            table[y] = x;
        }
    }

    int find(int x) {
        if (table[x] < 0) return x;
        return table[x] = find(table[x]);
    }
};

bool check(int sd, int gr, int gc, int gd) {
    UnionFind uf = UnionFind(N*N);
    REP(i, N) REP(j, N) if (A[i][j] != '#') {
        if (i == sr && j == sc && sd != 1) continue;
        if (i == gr && j == gc && gd != 1) continue;
        if (j == N - 1) return false;
        if (A[i][j+1] == '#') return false;
        if (i == sr && j + 1 == sc && sd != 0) return false;
        if (i == gr && j + 1 == gc && gd != 0) return false;
        uf.unite(i*N+j, i*N+j+1), j += 1;
    }
    REP(j, N) REP(i, N) if (A[i][j] != '#') {
        if (i == sr && j == sc && sd != 3) continue;
        if (i == gr && j == gc && gd != 3) continue;
        if (i == N - 1) return false;
        if (A[i+1][j] == '#') return false;
        if (i + 1 == sr && j == sc && sd != 2) return false;
        if (i + 1 == gr && j == gc && gd != 2) return false;
        uf.unite(i*N+j, (i+1)*N+j), i += 1;
    }

    REP(i, N) REP(j, N) if (A[i][j] != '#') return -uf.table[uf.find(i*N+j)] == empty_count;
    return false;
}

void solve() {
    cin >> N;
    REP(i, N) cin >> A[i];

    REP(i, N) REP(j, N) empty_count += A[i][j] != '#';
    REP(i, N) REP(j, N) if (A[i][j] == 's') sr = i, sc = j;
    REP(i, N) REP(j, N) if (A[i][j] != '#') row_parity[i] ^= 1, col_parity[j] ^= 1;
    REP(i, N) rp += row_parity[i], cp += col_parity[i];

    REP(sd, 4) {
        if (sd < 2) {
            cp += col_parity[sc] ? -1 : 1;
            col_parity[sc] ^= 1;
        } else {
            rp += row_parity[sr] ? -1 : 1;
            row_parity[sr] ^= 1;
        }
        if (rp + cp != 1) goto finally;

        REP(i, N) if (row_parity[i]) REP(j, N) if (A[i][j] == '.') REP2(k, 2, 4) if (check(sd, i, j, k)) {
            cout << "POSSIBLE" << endl;
            return;
        }

        REP(j, N) if (col_parity[j]) REP(i, N) if (A[i][j] == '.') REP(k, 2) if (check(sd, i, j, k)) {
            cout << "POSSIBLE" << endl;
            return;
        }

        finally:
        if (sd < 2) {
            col_parity[sc] ^= 1;
            cp -= col_parity[sc] ? -1 : 1;
        } else {
            row_parity[sr] ^= 1;
            rp -= row_parity[sr] ? -1 : 1;
        }        
    }

    cout << "IMPOSSIBLE" << endl;
}

int main() {
    cin.tie(0);
    ios::sync_with_stdio(false);
    solve(); 
}

京都大学プログラミングコンテスト2012 practice: D - A mul B Problem

https://atcoder.jp/contests/kupc2012pr/tasks/kupc2012pr_4

問題概要

N行N列の行列A, B, Cが与えられる。AB = Cであるか判定せよ。

N <= 1000

解法

普通に掛け算するとO(N3)だが、検算だけなら各要素が0か1をランダムでとるN要素のベクトルrを使って ABr = Cr かを見るというO(N2)の乱択アルゴリズムがある(詳しくは以下の記事参照)。

https://research.preferred.jp/2011/01/matrix-multiplication-and-polynomial-identity/

これをやると AB=C なら必ず ABr = Cr で、AB!=C なら1/2で ABr != Cr となるので、100回ほど回して一度でも ABr != Cr となるかどうかを見れば十分な速度・精度で答えを得られる(上の記事の「AB≠C の時は確率(1/2)kでNoと出力する」は「確率1-(1/2)kでNoと出力する」の打ち間違い?)。

感想

なるほどね

コード

import std.stdio, std.array, std.string, std.conv, std.algorithm;
import std.typecons, std.range, std.random, std.math, std.container;
import std.numeric, std.bigint, core.bitop, core.stdc.string;

void main() {
    auto N = readln.chomp.to!int;
    auto A = iota(N).map!(_ => readln.split.map!(to!long).array).array;
    auto B = iota(N).map!(_ => readln.split.map!(to!long).array).array;
    auto C = iota(N).map!(_ => readln.split.map!(to!long).array).array;

    int yes = 0, no = 0;

    foreach (_; 0..100) {
        auto v = iota(N).map!(_ => uniform(0, 2).to!long).array;
        auto t = v.dup;
        auto u = new long[](N);

        foreach (i; 0..N) foreach (j; 0..N) {
            u[i] += B[i][j] * v[j];
        }
        v = u.dup;
        u[] = 0;

        foreach (i; 0..N) foreach (j; 0..N) {
            u[i] += A[i][j] * v[j];
        }
        v = u.dup;
        u[] = 0;

        foreach (i; 0..N) foreach (j; 0..N) {
            u[i] += C[i][j] * t[j];
        }
        t = u.dup;

        (v == t ? yes : no) += 1;
    }

    writeln(no ? "NO" : "YES");
}

第3回 ドワンゴからの挑戦状 本選: B - ニワンゴくんの約数

https://atcoder.jp/contests/dwacon2017-honsen/tasks/dwango2017final_b

問題概要

N要素の正整数の数列Xが与えられる。Q個の区間[L, R]が与えられるので、それぞれについて「その区間に含まれるすべてのXの要素の積をとったとき、その数の約数は何個か」という問いに答えよ。

N, Q <= 105

max(Xi) <= 105

解法

約数の個数は各素因数について(出現回数+1)の積をとることで求められる。なので問題で要求されている操作そのものはシンプルで、愚直にやるならばXの要素を素因数分解したうえでそれぞれの区間において各素数の出現回数の和を取ってかけ合わせればよい。

そう考えると各素数の出現回数テーブルを持って累積和をすればよいのではと一瞬思えるが、105までの素数は104個くらいあるのでそれでは計算量的に厳しい。

ということで別の道を探してmax(Xi)の小ささに注目してみる。max(Xi)がたかだか105なので、あるひとつのXiにおいてsqrt(105)より大きい素数はたかだか1回しか現れない(もし2回以上現れたら明らかに105を越えてしまう)。また105以下の素数は104個くらいあるがsqrt(105)以下の素数は70個くらいしかない。まとめると以下のような性質の違いがsqrt(105)を境に現れるといえる。

  1. sqrt(105)以下の素因数 → 種類は少ないがひとつのXiにおいて複数種・複数回現れる可能性がある
  2. sqrt(105)より大きい素因数 → 種類は多いがひとつのXiにおいてたかだか一種類・一回までしか現れない

以上の性質の違いからこれらを場合分けして独立に解くことにするとうまくいく。

sqrt(105)以下の素因数に対して

上述のとおりこのような素因数は70個くらいしかないので単純な累積和(全部の素数の出現回数をカウントするテーブルを作って累積和)を適用できる。

sqrt(105)より大きい素因数に対して

ひとつのXiにおいてたかだか一種類・一回までしか現れないという性質から、もしいまある区間の答えがわかっているならば、その区間の端をひとつだけずらしたときの答えというのが高速に計算できる(たとえば[L, R]の右端をひとつ伸ばして[L, R+1]を見たとき、X_(R+1)の「大きい素因数」の出現回数が1増える。もしもともとの出現回数が3回だったとするとこれによって答えは「元の答え / 3 × 4」に更新できる)。

このように区間をひとつずらしたときの答えの差分更新が高速にできるとき、Mo's algorithm が使える可能性がある。このアルゴリズムの詳細については以下の記事がとてもわかりやすかった。

具体的な話はこの記事を読んでもらうとして自分の理解をざっくりまとめておくと、Mo's algorithmは「区間クエリのいい感じの並び替え方」についてのアルゴリズムである。具体的には「区間の右端か左端をひとつずつずらして差分更新していって最終的にクエリで飛んでくる全部の区間の答えを求めたい、というとき、Mo's algorithm にしたがって区間を並べ替えるとずらしの回数が(N+Q)sqrt(N)に抑えられる」というもの(Nは数列の長さ、Qはクエリ数)。

このケースはまさにMoに適した形の構造をしている。各素因数が何回出てきたかカウントする用の105のテーブルひとつと現在の答えを保持する変数ひとつを持ったうえでMoが返してくるとおりの順番で更新を行っていけばよい。

感想

うしさんありがとう

コード (C++)

Mo の部分は上の記事のものをそのまま貼っただけ(うしさんありがとう)

#include <bits/stdc++.h>
using namespace std;
#define REP(i,n) for (int i=0;i<(n);i++)
#define REP2(i,m,n) for (int i=m;i<(n);i++)
typedef long long ll;

const ll MOD = 1000000007;
const int SQ = 350;
int N, Q;
int A[101010];
int B[101010][100];
int L[101010];
int R[101010];
ll ans[101010];
vector<int> P;
ll tmp = 1;
ll cnt[101010];
ll inv[101010];

ll powmod(ll a, ll x) {
    ll ret = 1;
    while (x) {
        if (x % 2) ret = ret * a % MOD;
        a = a * a % MOD;
        x /= 2;
    }
    return ret;
}

// from https://ei1333.hateblo.jp/entry/2017/09/11/211011
struct Mo
{
    vector< int > left, right, order;
    vector< bool > v;
    int width;
    int nl, nr, ptr;

    Mo(int n) : width((int) sqrt(n)), nl(0), nr(0), ptr(0), v(n) {}

    void insert(int l, int r) /* [l, r) */
    {
        left.push_back(l);
        right.push_back(r);
    }

    /* ソート */
    void build()
    {
        order.resize(left.size());
        iota(begin(order), end(order), 0);
        sort(begin(order), end(order), [&](int a, int b)
        {
            if(left[a] / width != left[b] / width) return left[a] < left[b];
            return right[a] < right[b];
        });
    }

    /* クエリを 1 つぶんすすめて, クエリのidを返す */
    int process()
    {
        if(ptr == order.size()) return (-1);
        const auto id = order[ptr];
        while(nl > left[id]) distribute(--nl);
        while(nr < right[id]) distribute(nr++);
        while(nl < left[id]) distribute(nl++);
        while(nr > right[id]) distribute(--nr);
        return (order[ptr++]);
    }

    inline void distribute(int idx)
    {
        v[idx].flip();
        if(v[idx]) add(idx);
        else del(idx);
    }

    void add(int idx) {
        if (A[idx] == 1) return;
        tmp = tmp * inv[cnt[A[idx]] + 1] % MOD;
        cnt[A[idx]] += 1;
        tmp = tmp * (cnt[A[idx]] + 1) % MOD;
    }

    void del(int idx) {
        if (A[idx] == 1) return;
        tmp = tmp * inv[cnt[A[idx]] + 1] % MOD;
        cnt[A[idx]] -= 1;
        tmp = tmp * (cnt[A[idx]] + 1) % MOD;
    }
};

void enum_small_primes() {
    for (int i = 2; i < SQ; ++i) {
        bool ok = true;
        for (auto p: P) if (i % p == 0) {
            ok = false;
            break;
        }
        if (ok) {
            P.push_back(i);
        }
    }
}

void solve_small() {
    REP(i, N) REP(j, (int)P.size()) while (A[i] % P[j] == 0) {
        A[i] /= P[j];
        B[i+1][j] += 1;
    }
    REP(i, N) REP(j, (int)P.size()) {
        B[i+1][j] += B[i][j];
    }
    REP(i, Q) REP(j, (int)P.size()) {
        ans[i] = ans[i] * (B[R[i]+1][j] - B[L[i]][j] + 1) % MOD;
    }
}

void solve_large() {
    Mo mo = Mo(N);
    REP(i, Q) {
        mo.insert(L[i], R[i] + 1);
    }
    mo.build();
    REP(i, Q) {
        int idx = mo.process();
        ans[idx] = ans[idx] * tmp % MOD;
    }
}

void solve() {
    cin >> N >> Q;
    REP(i, N) cin >> A[i];
    REP(i, Q) cin >> L[i] >> R[i], --L[i], --R[i];
    REP(i, Q) ans[i] = 1;
    REP(i, 101010) inv[i] = powmod(i, MOD-2);

    enum_small_primes();
    solve_small();
    solve_large();

    REP(i, Q) cout << ans[i] << "\n";
}

int main() {
    cin.tie(0);
    ios::sync_with_stdio(false);
    solve(); 
}

早稲田大学プログラミングコンテスト2019: F - RPG

https://atcoder.jp/contests/wupc2019/tasks/wupc2019_f

問題概要

N頂点M辺の有向無閉路グラフが与えられる。グラフの各頂点は必ず「戦闘」もしくは「空き地」のいずれかに分類される。また空き地の頂点iには整数のコストCiを支払うことで回復所を設置することができる。頂点1から頂点Nまで移動するとき、どのような経路を通ったとしても2つの戦闘頂点間を移動する間には必ずひとつ以上の回復所を通過するように回復所を設置するとき、設置に必要なコスト合計の最小値を求めよ。

N <= 500

M <= 1000

解法

フローをやる。辺の張り方はいろいろあると思うが、自分は次のようにやった。

  • 頂点を倍加して各頂点に「入頂点」「出頂点」をつくる
  • 戦闘頂点ではsourceから出頂点へ、入頂点からsinkへそれぞれ容量INFで辺を張る
  • 空き地頂点では自分の入頂点から自分の出頂点に対して容量Ciで辺を張る
  • あとは与えられたグラフどおりに容量INFで出頂点から入頂点への辺を張っていく

あとはこのグラフにフローを流して最大流を求めれば答えが出る。この問題で求められているような回復所の設置の仕方はすべての戦闘頂点の出頂点がsource側、入頂点がsink側になるようグラフをカットするものと捉えることができて、INFの辺は実質カットできないものと見なすとあとは空き地頂点の容量Ciの辺を切る(=そこに回復所を設置する)しかないので、結果最小カット(=最大流)が答えとなる、という感じ(たぶん)

感想

カットで頂点をsource側sink側の2種類に分けるという考え方重要っぽい フローよくわかってないのでなんか変なこと言ってたら教えてください

こいつ最近F問題の記事ばっか書いてるな

コード (D言語)

import std.stdio, std.array, std.string, std.conv, std.algorithm;
import std.typecons, std.range, std.random, std.math, std.container;
import std.numeric, std.bigint, core.bitop;
 
immutable long MOD = 10^^9 + 7;
immutable long INF = 10L^^15;

void main() {
    auto s = readln.split.map!(to!int);
    auto N = s[0];
    auto M = s[1];
    auto C = 0 ~ readln.split.map!(to!(long)).array ~ 0;
    auto G = new int[][](N);
    foreach (i; 0..M) {
        s = readln.split.map!(to!int);
        G[s[0]-1] ~= s[1]-1;
    }
    
    int source = 0;
    int sink = N - 1;
    auto ff = new FordFulkerson!long(N*2, source, sink);

    foreach (i; 1..N-1) {
        if (C[i] == -1) {
            ff.add_edge(source, i, INF);
            ff.add_edge(i+N, sink, INF);
        } else {
            ff.add_edge(i+N, i, C[i]);
        }

        foreach (j; G[i]) {
            ff.add_edge(i, j+N, INF);
        }
    }

    long ans = ff.run;
    writeln( ans < INF ? ans : -1 );
}

class FordFulkerson(T) {
    int N, source, sink;
    int[][] adj;
    T[][] flow;
    bool[] used;

    this(int n, int s, int t) {
        N = n;
        source = s;
        sink = t;
        assert (s >= 0 && s < N && t >= 0 && t < N);
        adj = new int[][](N);
        flow = new T[][](N, N);
        used = new bool[](N);
    }

    void add_edge(int from, int to, T cap) {
        adj[from] ~= to;
        adj[to] ~= from;
        flow[from][to] = cap;
    }

    T dfs(int v, T min_cap) {
        if (v == sink)
            return min_cap;
        if (used[v])
            return 0;
        used[v] = true;
        foreach (to; adj[v]) {
            if (!used[to] && flow[v][to] > 0) {
                auto bottleneck = dfs(to, min(min_cap, flow[v][to]));
                if (bottleneck == 0) continue;
                flow[v][to] -= bottleneck;
                flow[to][v] += bottleneck;
                return bottleneck;
            }
        }
        return 0;
    }

    T run() {
        T ret = 0;
        while (true) {
            foreach (i; 0..N) used[i] = false;
            T f = dfs(source, T.max);
            if (f > 0)
                ret += f;
            else
                return ret;
        }
    }
}