Expected diameter of a tree | Codeforces #411

コメントつきコード

// 頂点の数、辺の数、連結成分のID、最も遠い点までの距離、連結成分の直径
int N, M, Q, cmp[100001], far[100001], nc, dia[100001];
// グラフ、連結成分ごとに最も遠い点までの距離をすべてもってソートしたもの
vi G[100001], F[100001];
// その部分和
vll S[100001];
// メモ
map<pii, ll> DS;

int bfs(int root, int c) {
    queue<tuple<int,int,int> > q;
    q.push({ root,-1, 0});
    int ma = -1, fa = 0;
    while (sz(q)) {
        int u, p, d;
        tie(u, p, d) = q.top();
        q.pop();
        cmp[u] = c;
        if (d > ma) {
            fa = u;
            ma = d;
        }
        /*
        頂点uから最も遠い点までの距離を求める。
        */
        smax(far[u], d);
        each(v, G[u])if (v != p) {
            q.push({ v,u,d + 1 });
        }
    }
    return fa;
}

ll solve(int U, int V) {
    if (DS.count({ U,V }))return DS[{U, V}];
    int madi = max(dia[U], dia[V]);
    ll &res = DS[{U, V}];
    /*
    ここの最悪ケースを考えてみよう。
    すべての連結成分の大きさが√nのとき
    √n個のUについてxは√N個あり、相手のVがc√N個
    中で二部探索しているので
    O(n^(3/2)log(n))
    **/
    each(x, F[U]) {
        /*
        madi > x + y
        連結しても直径はmadiのまま
        madi <= x + yのとき
        連結すると直径は
        x + y + 1
        y ∈ F[V]
        最小のyを求める
        madi - x <= y
        */
        int k = (int)distance(F[V].begin(), lower_bound(all(F[V]), madi - x));
        res += (ll)k*madi;
        // madi<=x+y
        // x+y+1のx+1の部分を足す
        res += (ll)(x + 1)*(sz(F[V]) - k);
        // x+y+1のyを足す
        res += S[V].back() - S[V][k];
    }
    return res;
}

int main(){
    cin >> N >> M >> Q;
    rep(i, M) {
        int u, v;
        scanf("%d%d", &u, &v);
        --u; --v;
        G[u].push_back(v);
        G[v].push_back(u);
    }
    MEM(cmp, -1);

    /*
    各木について直径を求める。
    また、頂点vから最も遠い点までの距離をすべて求める。
    */
    rep(i, N) if (cmp[i] == -1) {
        // iから最も遠い頂点はx
        int x = bfs(i, nc);
        // xから最も遠い頂点はy
        int y = bfs(x, nc);
        // (x, y)の距離が直径
        dia[nc] = far[y];
        // 任意の頂点についてxまたはyが最も遠い頂点の1つである。
        bfs(y, nc);
        nc++;
    }
    
    /*
    最も遠い点までの距離たちを連結成分ごとにまとめてからソートする
    */
    rep(i, N)F[cmp[i]].push_back(far[i]);
    rep(i, nc) {
        sort(all(F[i]));
        S[i].resize(sz(F[i]) + 1);
        rep(j, sz(F[i])) {
            S[i][j + 1] = F[i][j] + S[i][j];
        }
    }

    while (Q--) {
        int u, v;
        scanf("%d%d", &u, &v);
        --u;
        --v;
        int U = cmp[u], V = cmp[v];
        if (U == V) {
            printf("%d\n", -1);
            continue;
        }
        if (sz(F[U]) > sz(F[V]))swap(U, V);
        ll x = solve(U, V);
        double ans = (double)x / sz(F[U]) / sz(F[V]);
        printf("%0.10f\n", ans);
    }
}

Find Amir | Codeforces #411

2番目の入力例n=10を考えてみよう。

とりあえず、(i+j) mod (n+1) = 0

となるような辺(i, j)はコスト0なので全部張ろう。

1-10

2-9

3-8

4-7

5-6

あとはコスト1以上の辺を貼るしかない。

実はすべてコスト1の辺で繋げられる。

10+2≡1

9+3≡1

8+4≡1

7+5≡1

よってコストは4。

奇数の場合も同様に考えることができる。したがって解は

floor((n-1)/2)

 

クラスカル法っぽくやる

Minimum number of steps | Codeforces #411 (Div. 1)

abのような形が残らないので、操作していくと最終的に

bbb....baa...a

のような形になる。

とりあえず実験してみる。

ab

→bba

bはaを飛び越えるときに1個から2個になった。

abに対する操作は1回。

aab

→a<ab>

abba

→<ab>ba

→bbaba

→bb<ab>a

→bbbbaa

aを2個飛び越えることでbは4個になった。

abに対する操作は1+2回。

...

k>=1のとき

(aがk個)b→(bが2^k個)(aがk個)

abに対する操作は1+2+...+2^(k-1) 回

 

あとは、すべてのbについて、そのbより左にあるaを数えて、この操作回数の和を求めればいい。

Ice cream coloring | Codeforces #411 (Div. 1)

コメント付きコード

// 頂点の数、色の数、頂点vに含まれるアイスの数
int N, M, S[300001];
// グラフ, 頂点vに含まれるアイス
vi G[300001], C[300001];
// 解, 色iを使ったかどうか
int X[300001], use[300001];

int main(){
    scanf("%d%d", &N, &M);
    rep(i, N) {
        scanf("%d", &S[i]);
        C[i].resize(S[i]);
        rep(j, S[i])scanf("%d", &C[i][j]), --C[i][j];
    }
    rep(i, N - 1) {
        int u, v;
        scanf("%d%d", &u, &v);
        --u; --v;
        /*
        S[u] > 0 かつ S[v] > 0の場合だけ辺を張る。
        こうすると元の木Tの部分木の集合(Tの分割)が得られる。
        アイスは連結した部分グラフになっているので、どのアイスもたかだか1個の部分木にしか現れない。
        */
        if (S[u] && S[v]) {
            G[u].push_back(v);
            G[v].push_back(u);
        }
    }

    int ans = 1;
    queue<pii> q;
    rep(i, N) {
        /*
        頂点iの含む部分木に含まれるアイスの色をすべて決める。
        */
        if (S[i] && !X[C[i][0]]) {
            /*
            部分木を頂点ごとに探索していく。スタックオーバーフローを避けるためにDFSはしない。
            */
            q.push(mp(i, -1));
            while (sz(q)) {
                int u, pre;
                tie(u, pre) = q.front();
                q.pop();
                /*
                すでに色が確定しているアイスをチェック
                */
                rep(j, S[u]) {
                    int ice = C[u][j];
                    if (X[ice]) {
                        use[X[ice]] = 1;
                    }
                }
                /*
                1から順に、まだ使われていない色を探していく。
                */
                int cur = 1;
                rep(j, S[u]) {
                    int ice = C[u][j];
                    if (!X[ice]) {
                        // 色curは他のアイスに使われている。
                        while (use[cur])cur++;
                        X[ice] = cur;
                        use[cur] = 1;
                    }
                }
                /*
                curが最大値を取るとき、
                色1, 2, 3, 4, 5, ... , cur
                はすべて使われている。
                逆にこの時、curを一色でも減らすと、必ずどこかで同色が隣接してしまう。
                */
                smax(ans, cur);
                rep(j, S[u])use[X[C[u][j]]] = 0;
                each(v, G[u]) if (v != pre) {
                    q.push(mp(v, u));
                }
            }
        }
    }

    /*
    問題文には
    "empty set of vertices form a connected subgraph in this problem"
    とあるので、アイスが空の部分木になっている場合がある。
    このアイスの色は任意なので1をふっておく。
    人々が落ちてるのはここ?
    */
    rep(i, M)if (!X[i])X[i] = 1;

    printf("%d\n", ans);
    rep(i, M) {
        printf("%d%c", X[i], i != M - 1 ? ' ' : '\n');
    }
}

Subsequence Queries | CS Academy #24

コメントつきコード

/*
解法
漸化式で解を表して行列を作ってみる。
行列は添字ごとに異なるので累乗では解けない。
[l, r)の積
A[r-1]A[r-2]...A[l]
の求め方

(1) セグメント木を使って求める
http://yukicoder.me/problems/no/510
の解法
今回の問題ではTLEを食らった
(2)
逆行列を使う
A[r-1]A[r-2]...A[l]
= A[r-1]A[r-2]...A[l]...A[0]inv(A[0])inv(A[1])...inv(A[l-1])
ただし、inv(A[i])は行列A[i]の逆行列とする。
*/

/*
行列Bの左から行列Aを掛けた積を返す
*/
const int MOD = (int)1e9 + 7;
vector<vector<int> > mulMatMod(const vector<vector<int> > &A, const vector<vector<int> > &B) {
    auto n = A.size(), m = A[0].size(), p = B[0].size();
    vector<vector<int> > res(n, vector<int>(p));
    for (int i = 0; i < n; ++i)for (int j = 0; j < p; ++j) for (int k = 0; k < m; ++k)
        res[i][j] = (int)((res[i][j] + (long long)A[i][k] * B[k][j]) % MOD);
    return res;
}

typedef vector<vector<int> > mat;
string S;
int Q;

int main(){
    ios::sync_with_stdio(false);
    cin.tie(0);
    cin >> S >> Q;
    int N = sz(S);
    mat E(10, vi(10));
    rep(i, 10)E[i][i] = 1;
    vector<mat> AA(N+1, E), BB(N+1, E);
    rep(i, N) {
        int a = S[i] - 'a';
        mat A = E;
        // B=inv(A)
        mat B = E;
        rep(j, 10)A[a][j] = 1;
        // 掛ける順番に注意
        // A[j]A[j-1]...A[0]
        AA[i + 1] = mulMatMod(A, AA[i]);
        /*
        1 0 0 0 0
        1 1 1 1 1
        0 0 1 0 0
        0 0 0 1 0
        0 0 0 0 1
        のように漸化式から作った行列が特殊な形をしているので
        逆行列を簡単に求めることができる。
        */
        rep(r, 10)if (r != a) {
            B[a][r] = (B[a][r] + MOD - 1) % MOD;
        }
        // B[0]B[1]...B[j]
        BB[i + 1] = mulMatMod(BB[i], B);
    }

    mat u(10, vi(1));
    u[9][0] = 1;
    while (Q--) {
        int l, r;
        cin >> l >> r;
        --l;
        // A[l, r) = A[r-1]...A[0]inv(A[0])inv(A[1])...inv(A[l-1])
        mat Alr = mulMatMod(AA[r], BB[l]);
        ll ans = 0;
        rep(i, 9)ans += Alr[i][9];
        cout << ans%MOD << endl;
    }
}

Counting Perfect Subsequences | HourRank 20

sに含まれる文字xの数をn(x)とする。
c, dが無いものと見做してsがa, bだけから成り立つと考えてみよう。
f[i]を文字a, bをちょうどi個ずつ含む部分列の数とする。
i>min(n(a), n(b))のとき
aまたはbの数が足りないから
f[i] = 0
i<=min(n(a), n(b))のとき
n(a)個のaのうちi個の選び方はC(n(a), i)
n(b)個のbのうちi個の選び方はC(n(b), i)
よって
f[i] = C(n(a), i)*C(n(b), i)

同様にc, dだけを考えてg[j]をc, dをちょうどi個含む部分列の数とすれば
g[j] = C(n(c), j)*C(n(d), j)

あとは、空の部分列になる(0, 0)以外のすべての(i, j)の組について考えれば良いので
ans
= Σ(f[i]g[j])-1
= Σ(f[i]) * Σ(g[i]) - 1
で求まった。

この数式変形はよくある。例えば、
1*4+1*5+1*6 + 2*4+2*5+2*6 + 3*4+3*5+3*6
= (1+2+3)*(4+5+6)

Birjik and Nicole's Tree Game | HourRank 20

復習用コメント付きコード

/*
思いつき方
頂点を黒く塗る
その黒い頂点を含む部分木は?
根まで行く距離が長過ぎる
HL分解する
*/

/*
HL分解
Heavy-Light Decomposition
木の構築 O(|V|+|E|)
あらかじめ与えられた木について、頂点u,v間のパスをセグメント木などで扱える半開区間の集合に変換する。区間の数はO(log(|V|))個。
ひとつの区間は連結した頂点たちにIDをふったもので、頂点の深さは全て異なる。その区間内では、(深さの値の差)=(IDの値の差) になっている.
1クエリ O(log(|V|))
スタックオーバーフロー回避版
*/
class HLDecomposition {
    int cur = 0;
    void bfs(const vector<vector<int> > &G) {
        queue<tuple<int, int, int> > que;
        vector<int> order;
        que.push(make_tuple(0, -1, 0));
        while (!que.empty()) {
            int u, pre, d;
            tie(u, pre, d) = que.front();
            que.pop();
            parent[u] = pre;
            depth[u] = d;
            order.push_back(u);
            for (int v : G[u])if (v != pre) {
                que.push(make_tuple(v, u, d + 1));
            }
        }
        reverse(order.begin(), order.end());
        for (int u : order) {
            sub[u] = 1;
            for (int v : G[u])if (v != parent[u])sub[u] += sub[v];
        }
    }

    void dfs_stk(const vector<vector<int> > & G) {
        stack<pair<int, int> > stk;
        stk.push(make_pair(0, 0));
        while (!stk.empty()) {
            int u, h, pre;
            tie(u, h) = stk.top();
            stk.pop();
            head[u] = h;
            id[u] = cur++;
            pre = parent[u];
            int heavy = -1, maxi = 0;
            for (int v : G[u]) {
                if (v != pre && maxi < sub[v]) {
                    maxi = sub[v];
                    heavy = v;
                }
            }
            for (int v : G[u]) {
                if (v != pre&&v != heavy) {
                    stk.push(make_pair(v, v));
                }
            }
            if (heavy != -1)stk.push(make_pair(heavy, h));
        }
    }
public:
    vector<int> parent, depth, sub, id, head;
    
    HLDecomposition(const vector<vector<int> > &G) {
        parent = depth = sub = id = head = vector<int>(G.size());
        bfs(G);
        dfs_stk(G);
    }

    /*
    パス(u, v)を半開区間の集合に変換する。
    O(log(|V|))
    */
    vector<pair<int, int> > goUpToLCA(int u, int v) {
        vector<pair<int, int> > res;
        while (true) {
            if (head[u] == head[v]) {
                if (depth[u] > depth[v])swap(u, v);
                res.emplace_back(id[u], id[v] + 1);
                break;
            }
            else {
                if (depth[head[u]] > depth[head[v]])swap(u, v);
                res.emplace_back(id[head[v]], id[v] + 1);
                v = parent[head[v]];
            }
        }
        return res;
    }
};

int N, Q, K;
vector<vi> G;
int vs[300001], ans[300001];

int main() {
    scanf("%d", &N);
    G.resize(N);
    rep(i, N - 1) {
        int u, v;
        scanf("%d%d", &u, &v);
        // 0-indexedとする。
        --u;
        --v;
        G[u].push_back(v);
        G[v].push_back(u);
    }

    HLDecomposition hld(G);
    // sgs[l]はlをheadとするセグメント[l, R)に対して
    // l<r<=R, r∈sgs[l]
    // を満たすような多重集合
    unordered_map<int, vi> sgs;

    scanf("%d", &Q);
    while (Q--) {
        sgs.clear();
        scanf("%d", &K);
        rep(i, K) {
            scanf("%d", &vs[i]);
            --vs[i];
            /*
            頂点vs[i]を含む部分木は
            vs[i], vs[i]の親、vs[i]の親の親、、、根
            つまりvs[i]の先祖すべてを根とする部分木すべて。
            これをすべて得るにはvs[i]から根までのパスを見るだけでいいが、
            O(|V|)かかる。
            かわりに、HL分解によってO(log|V|)個のパスを得る。
            */
            // log(|V|)個のパスに変換
            auto s = hld.goUpToLCA(0, vs[i]);
            each(p, s) {
                // ここが最も深い
                // O(log(N)ΣK)
                sgs[p.first].push_back(p.second);
            }
        }
        rep(i, K + 1)ans[i] = 0;

        each(p, sgs) {
            /*
            headのidをlとするようなパスについて考える。
            IDがlである頂点を根とする部分木は、黒い頂点をcn個含む。
            */
            int l = p.first, cn = sz(p.second);
            // rを昇順に見る。
            // より深い頂点へ向かっていく。
            sort(all(p.second));
            each(r, p.second) {
                /*
                r-1に対応する頂点が黒いので、
                rまで見たら黒色が1つ減る。
                [l, r)に対応するr-l個の頂点はcn個の黒い頂点を持つ。
                */
                ans[cn] += r - l;
                l = r;
                // 黒い頂点を1個減らす。(その黒い頂点よりも下へ行く)
                cn--;
            }
        }
        ans[0] = N;
        for (int i = 1; i <= K; ++i)ans[0] -= ans[i];
        rep(i, K + 1)printf("%d%c", ans[i], i != K ? ' ' : '\n');
    }
}