na_o_ysのブログ

プログラミングなど

SRM 627 Div1 Med, GraphInversions

問題

http://community.topcoder.com/stat?c=problem_statement&pm=13275

N 頂点, N 辺 (3 <= N <= 1000) の連結な無向グラフが与えられる. 各頂点は属性として値 v_i (1 <= v_i <= 1000) を持つ. 長さ K 以上の全ての単純路について, それぞれ v_i の系列で表した時に, 最も小さい転倒数 (inversion) を求めよ.

解法

それぞれの頂点に対して, 深さ K まで dfs する. 探索の計算量は N2 におさまる.
dfs しながら同時に, inversion を BIT で求める.
O(N2 log(N)).

実装

#define loop(n, i) for(int i=0;i<n;i++)

class GraphInversions
{
public:
    vector<int> G[1001];
    vector<int> V;
    int used[1001] = {};
    BIT *bit;

    int getMinimumInversions (vector <int> A, vector <int> B, vector <int> V, int K)
    {
        int N = A.size();
        this->V = V;
        loop (N, i) {
            G[A[i]].push_back(B[i]);
            G[B[i]].push_back(A[i]);
        }

        int ans = INF;
        loop (N, i) {
            bit = new BIT(1001);
            ans = min(ans, dfs(i, K));
            delete bit;
        }
        if (ans == INF) return -1;
        return ans;
    }

    int dfs(int i, int k) {
        if (used[i]) return INF;

        int crr = bit->sum(1000) - bit->sum(V[i]);
        if (k == 1) return crr;

        used[i] = 1;
        bit->add(V[i], 1);
        int rest = INF;
        for (int j : G[i]) if (!used[j]) {
            rest = min(rest, dfs(j, k-1));
        }
        used[i] = 0;
        bit->add(V[i], -1);

        if (rest == INF) return INF;
        return rest+crr;
    }
};

Binary Indexed Tree (BIT, Fenwick Tree)

概要

整数列  a_1, a_2, ..., a_n に対して, 以下のクエリを O(log(n)) で実現するデータ構造.

  1.  {\rm sum}(i):  a_1 + a_2 + ... + a_i を求める
  2.  {\rm add}(i, x):  a_i x を加える

特徴

Segment Tree の機能縮小版で, 実装がむちゃくちゃ簡単.

構造

深さ log(n) の二分木であり, 各ノードは以下の法則で値を保持する.

  1. :  a_1,\ a_3,\ a_5,\ a_7, \dots
  2. 葉の親:  a_1+a_2,\ a_5+a_6,\ a_9+a_{10}, \dots
  3. 葉の親の親: [tex: a_1+a_2+a_3+a_4,\ a_9+a{10}+a{11}+a_{12}, \dots]

クエリ処理は, 葉から根へ高々 log(n) 回の計算で完了する.
例えば,  {\rm sum}(7) (a_7) + (a_5+a_6) + (a_1+a_2+a_3+a_4) で求められる.

用途

転倒数 (inversion) を求めるなど

実装に関する補足

木を配列としてうまく持つことで実装がものすごく簡易になる. 詳細は下記ページを参照.

TopCoder Algorithm Tutorial: http://community.topcoder.com/tc?module=Static&d1=tutorials&d2=binaryIndexedTrees
わかりやすいスライド: http://hos.ac/slides/20140319_bit.pdf

実装

class BIT
{
public:
    vector<int> bit;
    int M;

    BIT(int M):
        bit(vector<int>(M+1, 0)), M(M) {}

    int sum(int i) {
        if (!i) return 0;
        return bit[i] + sum(i-(i&-i));
    }

    void add(int i, int x) {
        if (i > M) return;
        bit[i] += x;
        add(i+(i&-i), x);
    }
};

暗唱のポイント

  • 1-indexed
  • (i&-i) で rightmost set bit だけ取り出せる
  • について, sum は重ならない方向(左), add は重なる方向(右)

互いに素な3つ組の総数

問題

http://www.codechef.com/LTIME13/problems/COPRIME3
整数 N (0 < N < 105) と, N 個の整数 a_0, a_1, ..., a_[n-1] (0 < a_i < 106) が与えられる.
0 < i < j < k <= N として, a_i, a_j, a_k が互いに素となるような (i, j, k) の個数を出力せよ.
(制限時間1秒)

解答

gcd(a_i, a_j, a_k) = m なる (i, j, k) の個数に対する動的計画法で解く.
dp[m] は gcd() = m なる (i, j, k) の個数を表すとして, dp[106] から順に計算していく.
dp[1] が答えとなる.
計算量オーダーは, a_i の最大値を M (=106) として, O(Mlog(M)).

#include <iostream>
#include <vector>

using namespace std;
using ll = long long;
const int MAX = 1000000;

int main(int argc, char const* argv[])
{
    int N; cin >> N;
    int cnt[MAX+10] = {};
    for (int i = 0; i < N; i++) {
        int a; cin >> a;
        cnt[a]++;
    }

    vector<ll> dp(MAX+10);
    for (int i = MAX; i; i--) {
        // 入力値のうち i で割り切れるものの個数
        int mults = 0;
        for (int j = i; j <= MAX; j += i) mults += cnt[j];

        // multsから3つ選ぶ組み合わせ (mults C 3)
        dp[i] = (ll)mults*(mults-1)*(mults-2)/6;

        // dp[i*2], dp[i*3], ... を除く
        for (int j = i*2; j <= MAX; j += i) dp[i] -= dp[j];
    }

    cout << dp[1] << endl;
    return 0;
}

この方法だと簡単にnつ組に拡張できる.