Home > Archives > 2007年11月

2007年11月

SRM 379 DIV 1

もう少し頭使えよと言いたくなる結果.まあいいだろうとか考えて放っておいた部分が響いて正解なしに…

250点問題: price を最大までインクリメントしながらチェックしてたら時間オーバーだった.五千万位のループは大丈夫かと思ったけどだめみたい.馬鹿なことせずに与えられた配列内の値だけチェックすれば通っていたものを(誰かの max までは単調増加なので).

500点問題:図形っぽいからパスした.直方体を2の冪のサイズの立方体で埋める問題.正解を見てわかったけど,2の冪しかないのでな 1x1 で埋めなければならない部分を埋めて再帰的に1/2サイズにした問題を解けばよかったらしい.

1000点問題:結局のところ行列式に (-1)^(N-1) を掛けたものが答えになる.理由は以下のとおり.まず,行列に吐き出し操作をしても答えは変わらない(自明).そして,吐き出し後には,積が非ゼロになるドミノの選択は全てが対角要素である場合しかなく,その積は行列式に等しい.同時に,ドミノの連結グループ数は行列の大きさである N に等くなっているので,答えは行列式に(-1)^(N-1) を掛けたものに等しい(偶数なら符号反転).ということで,行列式を求めればおわる.幸いなことに modulo 121547 の答えでよく 121547 が素数で有限体上の計算になるので普通のLU分解とかが使える.

以上を踏まえ,有限体の逆元を求めるための拡張ユークリッドの互除法を実装し,演算を有限体の演算に置換したピボットつき LU 分解を実装して提出したのだが… 正規化のためのルーチンを次のように書いていたため TLE 喰らった.

    int norm(long a) {
        while(a < 0) a+=P;
        return (int)(a % P);
    }

掛け算してんだから数万回ループすることもあるよなぁ.もう少し進化して下のように書いたら普通に通ったし.

    int norm(long a) {
        if(a<0) a -= (a/P-1)*P;
        return (int)(a % P);
    }

あー,もったいねぇ.解けてたのに…

libstdc++ parallel mode を試す

EuroPar2007で発表のあった The Multi-Core Standard Template Library (MCSTL) が GCC に統合されている(されつつある?)らしいので,セミナーのネタにと試用してみた.GCCのオフィシャルページを見ると libstdc++ parallel mode という名前で統合中らしい.こいつは既存の STL を OpenMP による並列実装の STL に置換しましょうというもので,既存の STL で書かれたコードがそのままで並列プログラムになってくれる.

んで,試そうと思ったら最新のGCCリリースであるところの 4.2.2 にも入っていないということに GCC4.2.2 のビルドが終わった時点で気づき… しょうがないので svn のリポジトリから最新のソースを落としてきて GCC をビルドした(8コアくらいあるマシンで make -j8 とかやるとビルドがとても速い).とりあえずソースの状態で libstdc++-v3/include/parallel というディレクトリがあれば parallel mode が使える.

準備できたので次の std::sort を使ったプログラムを試してみた.

#include <iostream>
#include <algorithm>
#include <vector>
const int count = 1000000, num_tests = 10;
int main(int argc, char **argv)
{
    std::vector<int> v(count);
    for (int i = 0; i < num_tests; i++)
    {
        std::generate(v.begin(), v.end(), rand);
        std::sort(v.begin(), v.end());
        std::cout << "." << std::flush;
    }
}

コンパイルは parallel mode 用のフラグ(-D_GLIBCXX_PARALLEL -fopenmp)を立てればよいらしい.

 g++-XXX test.cpp -o testS -march=native  # sequential mode
 g++-XXX test.cpp -o testP -march=native -D_GLIBCXX_PARALLEL -fopenmp  # parallel mode

Quad の Xeon x2 という8コアで動かした結果は以下のとおり.

> OMP_NUM_THREADS=8 time ./testS   # sequential mode
..........        6.26 real         6.24 user         0.01 sys
> OMP_NUM_THREADS=8 time ./testP   # parallel mode
..........        1.39 real         5.66 user         0.75 sys

普通に4~5倍速くなってるのを確認.すげー.並列プログラミング簡単.とりあえず sort は簡単に並列に動いてくれた.

ついでに, parallel mode は他にも並列実装を持っているらしいので試してみた.

まず std::partial_sum.4.51秒が1.65秒になった.8コアで約四倍.

#include <iostream>
#include <algorithm>
#include <vector>
#include <numeric>
struct one {
  int operator()() const {
    return 1;
  }
};
const int count = 10000000, num_tests = 10;
int main(int argc, char **argv)
{
    std::vector<int> v(count);
    std::vector<int> r(count);
    std::generate(v.begin(), v.end(), one());
    for (int i = 0; i < num_tests; i++)
    {
        std::partial_sum(v.begin(), v.end(), r.begin());
        std::cout << "." << std::flush;
    }
}

次は std::accumulate.3.20秒が3.79秒に増えた.全要素の和をとるだけなので普通に考えると8コアで8倍いきそうだけど… 速くならない理由は不明.

#include <iostream>
#include <algorithm>
#include <vector>
#include <numeric>
struct one {
  int operator()() const {
    return 1;
  }
};
const int count = 10000000, num_tests = 10;
int main(int argc, char **argv)
{
    std::vector<int> v(count);
    std::generate(v.begin(), v.end(), one());
    for (int i = 0; i < num_tests; i++)
    {
        int sum = std::accumulate(v.begin(), v.end(), 0);
        std::cout << "." << std::flush;
    }
}

最後に試したのは std::for_each.3.81秒が3.75秒.全要素に独立に関数適用するだけなので8コアで8倍いきそうだけど… 速くならない理由は不明.

#include <iostream>
#include <algorithm>
#include <vector>
#include <numeric>
struct f {
  void operator()(int &x) const {
    x = x + 1;
  }
};
struct one {
  int operator()() const {
    return 1;
  }
};
const int count = 10000000, num_tests = 10;
int main(int argc, char **argv)
{
    std::vector<int> v(count);
    std::generate(v.begin(), v.end(), one());
    for (int i = 0; i < num_tests; i++)
    {
        std::for_each(v.begin(), v.end(), f());
        std::cout << "." << std::flush;
    }
}

ということで,簡単に並列化できそうな部分でいろいろと失敗した模様.使い方が悪いのか実装途中なのか? よくわからんけどうまく動いた部分に関しては効果が確認できたのでよし.

TABではまる

Fortress のプログラムでインデントに TAB 使ってたら Parse エラーが出まくった.そして原因が TAB であることに気づくまでプログラムを何度も書き直す羽目に.Spec のどこかに TAB に意味があるって書いてあったっけかな?

実験するも…

BSP実装がなんか変な挙動を示すなぁ.AlltoAllで書きなおした方がいいのだろうか?とりあえずもう一セットやってみっか.

ACM/ICPC の問題を解いてみる

寝付けないので自前で考えた解き方を試しに実装してみた.解説聞いてないので合ってるかどうかは知らんけど.

A: LinkedList で普通に要素を抜いてくだけ.最大サイズでも一瞬で終わるのでこれで問題なさそう.

B: エラトステネスのふるいで素数求めたら終わり.与えられた数から前後の素数を探索するのは前後を順に見てくだけでいい.

どうせすぐ近くに素数があるだろうから.

C: 状態遷移行列を T乗すると考えるかT回の状態遷移を計算すると考えるか… 状態遷移行列が疎なのでグラフ上で状態遷移をシミュレートするイメージの実装.スタートに戻るマスへの遷移はスタートへ足しこむ.一回休みは一回休み用のノードを作って一度そのノードへ遷移するようにする.計算量は O((N+L)T) なので時間も問題ないでしょう.

D: 面倒なので考えてない.一つの三角形を決めたら他も決まるから全部の点を試せばいいのかな?

E: これもグラフを作るまでが面倒.グラフさえできれば最短路なので楽.

F: ソートした辺のリスト上で最大重み用のポインタと最小重み用のポインタをスライドさせてく.二つのポインタの間にある辺集合で全域木が作れるなら最小のポインタを進める.作れないときは最大側のポインタをするめる.全域木ができるかの判定は連結しているかの判定でできるので適当に Unon/Find でやった.これで全体としては O(E^2) だけど… 手元では最大サイズが一秒以内なのでギリギリオッケー.

G: 状態空間のサイズが 2^24 程度なので普通に幅優先でいい気がする.ただ最大サイズのサンプル入力で4秒かかるのでもう少しコードを特化した方がいいかもしれない.

H: 再帰下降で Exception なげて帰ってくる.Hashtable とか使わないとインデックスで困るけどそれだけだと思う.

I: 面倒なのでパス.多角形を周りから削っていって最後に残ったところか?何となく島が海に沈んでくイメージ…

J: 連立方程式立てて解けば終わりなのだけど… 有理数で一次方程式を解かねばならないし(doubleとかでもいいのかな?),そもそも方程式の係数を求めるのも面倒.コード書きたくないから二乗根の場合に関して手で解いてみただけ.久々に 4x4 の一次式を解いた.

とりあえず,ABCFGH の実装に(テレビ見をながらではあるけど)5時間ほど.やっぱ9問解いたチームってのはすごいなぁ.

Home > Archives > 2007年11月

Search
Feeds

Page Top