Tag Archive for 'ProgrammingPearls'

ProgrammingPearls::ex3-8:7セグメント表示機

よくあるLED数字表示器のようなものを書くという問題です。ビット演算をちょこちょこやれば書けるんじゃないかなと思いました。

コード

/*
 * Programming Pearls
 * ISBN:4-89471-236-9
 *
 */

vector<char> split_decimal5(int n) {
   vector<char> ret(5);
   int div = 10;

   for (int i=1; i<=5; i++) {
      char r = n % div;
      ret[5-i] = r;
      n /= 10;
   }

   return ret;
}

#define SEG2BIT(x) (1 << (x))

char int27seg(char ch) {
   const static char char2segment[] = {
      /* 0 */ SEG2BIT(0) | SEG2BIT(6) | SEG2BIT(4) | SEG2BIT(2) | SEG2BIT(3) | SEG2BIT(5),
      /* 1 */ SEG2BIT(4) | SEG2BIT(6),
      /* 2 */ SEG2BIT(2) | SEG2BIT(4) | SEG2BIT(1) | SEG2BIT(5) | SEG2BIT(0),
      /* 3 */ SEG2BIT(2) | SEG2BIT(4) | SEG2BIT(1) | SEG2BIT(6) | SEG2BIT(0),
      /* 4 */ SEG2BIT(3) | SEG2BIT(1) | SEG2BIT(4) | SEG2BIT(6),
      /* 5 */ SEG2BIT(2) | SEG2BIT(3) | SEG2BIT(1) | SEG2BIT(6) | SEG2BIT(0),
      /* 6 */ SEG2BIT(2) | SEG2BIT(3) | SEG2BIT(5) | SEG2BIT(0) | SEG2BIT(6) | SEG2BIT(1),
      /* 7 */ SEG2BIT(2) | SEG2BIT(4) | SEG2BIT(6),
      /* 8 */ SEG2BIT(0) | SEG2BIT(1) | SEG2BIT(2) | SEG2BIT(3) | SEG2BIT(4) | SEG2BIT(5) | SEG2BIT(6),
      /* 9 */ SEG2BIT(2) | SEG2BIT(3) | SEG2BIT(1) | SEG2BIT(4) | SEG2BIT(6)
   };

   return char2segment[ch];
}

void printSeg(char seg) {
   char rows[][3] = {
      {3,1,4},
      {5,0,6}
   };

   if (seg & (SEG2BIT(2))) {
      cout << " _ ";
   }
   cout << endl;

   if (seg & (SEG2BIT(3))) {
      cout << "|";
   } else {
      cout << " ";
   }
   if (seg & (SEG2BIT(1))) {
      cout << "_";
   } else {
      cout << " ";
   }
   if (seg & (SEG2BIT(4))) {
      cout << "|";
   }
   cout << endl;

   if (seg & (SEG2BIT(5))) {
      cout << "|";
   } else {
      cout << " ";
   }
   if (seg & (SEG2BIT(0))) {
      cout << "_";
   } else {
      cout << " ";
   }
   if (seg & (SEG2BIT(6))) {
      cout << "|";
   }
   cout << endl;
}

int main(int argc, char **argv) {
   assert(argc == 2);

   vector<char> splitted = split_decimal5(atoi(argv[1]));
   vector<char>::iterator i;

   for (i = splitted.begin(); i != splitted.end(); i++) {
      printf("%x ", *i);
   }
   cout << endl;

   for (i = splitted.begin(); i != splitted.end(); i++) {
      printSeg(int27seg(*i));
   }

   return 0;
}

実行例

% ./ex3-8 12345
1 2 3 4 5

  |
  |
 _
 _|
|_
 _
 _|
 _|

|_|
  |
 _
|_
 _|

所感

printseg関数がまだ冗長に思えます。うまく配列をつかったらもっとコンパクトに書けそう。

ProgrammingPearls::ex3-5:英単語をハイフンで区切る

問題3.5より。

英単語をハイフンで分ける方法のごく一部を考えましょう。 … 与えられた単語をハイフンで区切る関数をどのように書けばよいでしょうか。

なんとなく解き方はすぐ頭にイメージできました。でもコードにするのが一苦労でした。世の中のたいていのことは、思ったよりも難しいものなのです。

作戦

まず、入力の全行を逆転させておいて、そこから木をつくれば探索にはO(logn)しかかからないんじゃないかと考えました。

逆転

入力の全行を逆転させるために、Rubyでワンライナーを書きます。

ruby -p -l -e '$_.reverse!' hyphen.list > hyphen.reversed

in:

et-ic
d-ic
s-tic
i-ac
....

out:

ci-te
ci-d
cit-s
ca-i
----

各ノードに[文字, ハイフンの位置, 子供のリスト] を持たせた木を考えます。

ex3-5

でもこれだと探索するときに子供を線形探索することになってしまうなぁ。

コード

  1  // ------------------------------------------------------------
  2  // Hyphnate tree
  3  //
  4  class Node;
  5
  6  bool hasChar(const char ch, const Node& n);
  7
  8  class Node {
  9  public:
 10     char ch;
 11     int pos; // position of hyphen from tail
 12     list<Node> children;
 13
 14     Node(char c, int p = -1) : ch(c), pos(p) {};
 15
 16     Node* add(const char c) {
 17        // XXX: linear search
 18        list<Node>::iterator found = find_if(
 19              children.begin(), children.end(),
 20              boost::bind( ptr_fun(hasChar), c, _1));
 21
 22        if (found == children.end()) { // not found
 23           // cout << "Node::add : " << c << endl;
 24           Node n(c);
 25           children.push_front(n);
 26           return &children.front();
 27        }
 28
 29        return &*found;
 30     }
 31
 32  }; // class Node
 33
 34  bool hasChar(const char ch, const Node& n) {
 35     return n.ch == ch;
 36  }
 37
 38
 39  Node root('c');
 40
 41  void addNode(string& pf) {
 42     Node *n = &root;
 43     int pos = -1;
 44     for (string::iterator i = pf.begin() + 1; i != pf.end(); i++) {
 45        if ('-' == *i) {
 46           pos = i - pf.begin() -1;
 47           continue;
 48        }
 49        n = n->add(*i);
 50     }
 51     n->pos = pos;
 52  }
 53
 54  string hyphenate(string& str) {
 55     Node *n = &root;
 56     string ret = str;
 57     int pos = -1;
 58
 59     for (string::reverse_iterator i = str.rbegin()+1; i != str.rend(); i++) {
 60        // cout << "HY: " << *i << endl;
 61        if (n->children.empty()) {
 62           // cout << "hyphenate : " << n->pos << endl;
 63           pos = n->pos;
 64           break;
 65        }
 66
 67        list<Node>::iterator found = find_if(
 68              n->children.begin(), n->children.end(),
 69              boost::bind( ptr_fun(hasChar), *i, _1));
 70        if (found == n->children.end()) { // not found
 71           if (n->pos != -1) {
 72              pos = n->pos;
 73              break;
 74           }
 75           cout << "hyphenate : not found" << endl;
 76           return "";
 77        }
 78
 79        n = &*found;
 80     }
 81
 82     cout << pos << " " << ret.size() << endl;
 83     // ret.resize(ret.length() + 1);
 84     ret.insert(ret.begin() + ret.length() - pos - 1, '-');
 85
 86     return ret;
 87  }
 88
 89  // ------------------------------------------------------------
 90  // utility
 91  //
 92  void showTree_rec(Node *n, int indent) {
 93     for(list<Node>::iterator i = n->children.begin();
 94           i != n->children.end();
 95           i++) {
 96        for (int j=0; j<indent; j++) {
 97           cout << " ";
 98        }
 99        cout << i->ch << " ";
100        if (i->children.empty() || i->pos != -1) {
101           cout << i->pos;
102        }
103        cout << endl;
104
105
106        showTree_rec(&*i, indent + 3);
107     }
108  }
109
110
111  void showTree() {
112     showTree_rec(&root, 0);
113  }
114
115  // ------------------------------------------------------------
116  // test
117  //
118  int main(int argc, char **argv) {
119     assert(argc == 2);
120
121     // build tree
122     string postfix;
123     ifstream ifs(argv[1]);
124     while (!ifs.eof()) {
125        ifs >> postfix;
126        addNode(postfix);
127     }
128
129     showTree();
130
131     // hyphenate
132     string input;
133     while (true) {
134        // read line
135        cin >> input;
136
137        if (cin.eof()) {
138           break;
139        }
140
141        cout << hyphenate(input) << endl;
142     }
143
144     return 0;
145  }

実行例

a
   i 1
   m 2
i
   a 1
   b 2
   d 1
   f 1
   h 1
   l 1
      c 3
      b 2
      l 2
   m 1
   n 1
      h 2
   t
      a 1
      c 2
      n 2
         a 2
      o 1
      y
         l 5
      p 2
      s 2
         i
            l
               a 5
      e 1
c

palistic
pa-listic

actic
ac-tic

mantic
man-tic

gigantic
gigan-tic

critic
hyphenate : not found

所感

できたーと思って問題文をもう一度見返してみたら、ハイフンは一カ所だけとは限らないと。 このコードはハイフンが一カ所しかないことを前提にしているので、正しくないです。otz

ProgrammingPearls::ex2-1 anagram generator

辞書ファイルを入力として、すべてのアナグラムを見つけるプログラムを書きます。 辞書ファイルには一行につき一単語が書かれてあるものとします。

その1

sign, sort はそのままなので省略します。squashをどう書くか。

一番始めの処理と、アナグラムを格納するコンテナがいるかなと思って書いたコードが次のものです。

 1  int main() {
 2     string key, val;
 3     string prev;
 4     set<string> grp;
 5
 6     // first time
 7     cin >> key >> val;
 8
 9     prev = key;
10     grp.insert(val);
11
12     do {
13        // read line
14        cin >> key >> val;
15
16        if (prev == key) {
17           grp.insert(val);
18        } else {
19           // print anagrams
20           for (set<string>::iterator i = grp.begin(); i != grp.end(); i++) {
21              cout << *i << " ";
22           }
23           cout << endl;
24
25           grp.clear();
26
27           prev = key;
28           grp.insert(val);
29        }
30     } while (!cin.eof());
31
32     // finish
33
34     if (!grp.empty()) {
35        // print anagrams
36        for (set<string>::iterator i = grp.begin(); i != grp.end(); i++) {
37           cout << *i << ", ";
38        }
39        cout << endl;
40     }
41
42     return 0;
43  }
44

とりあえず、出力は正しいことを確認できました。でもなんだかきれいなコードではないように見えます。

その2

2.8の解答を読んで、なるほどコンテナは必要ないのだということに気づいて書き直したのが次のものです。

 1  int main() {
 2     string key, val;
 3     string prev = "";
 4
 5     while (true) {
 6        // read line
 7        cin >> key >> val;
 8
 9        if (cin.eof()) {
10           break;
11        }
12
13        if (key != prev && "" != prev) {
14           cout << endl;
15        }
16
17        cout << val << " ";
18
19        prev = key;
20     }
21
22     // finish
23     cout << endl;
24
25     return 0;
26  }

だいぶすっきりしました。

所感

やはり先人のコードというのは簡潔で美しいものだと思います。見習わねば。

ProgrammingPearls::ex2-8. 部分配列の和がtを超えないか

ex 2-8より。

n要素の実数の配列と実数tと整数kが与えられたとき、要素がk個の部分配列で、要素の和がtを超えないようなものがあるかどうか、すばやく判定するにはどうしたらよいでしょうか。

3分考えてアルゴリズムをひねり出してみました。

部分配列の和をincrementalに求める作戦

ex 2-8

部分配列をずらしながら、和をincrementalに求めれば、速く計算できるのではないかと考えました。 計算量は O(n) で収まるはず。

コード

 1  bool subarr_sum_less(VI &a, int t, int k) {
 2     // init
 3     int sum = 0;
 4     for (int i=0; i<k; i++) {
 5        sum += a[i];
 6     }
 7
 8     if (sum < t) {
 9        return true;
10     }
11
12     for (int i=1; i<a.size()-k+1; i++) {
13        sum -= a[i-1];
14        sum += a[i+k-1];
15
16        if (sum < t) {
17           return true;
18        }
19     }
20
21     return false;
22  }

実行例

% ./ex2-8 100 -100 3

-19, -97, 212, -161, -245, 181, 114, 121, 123, 118, 229, -284, -12, 213, 276, -90, -67,
-200, -36, -130, -62, -185, 56, -69, -235, 177, -101, -197, 270, 242, 141, 97, -272,
 292, 111, -93, 186, 102, -36, -173, 111, 240, -68, 78, 0, -89, 104, 101, -236, -188,
-216, 24, -11, 96, -102, -123, -123, -230, 253, -282, -80, 146, -9, -13, -93, -96, -235,
 267, 0, 98, 136, -120, 37, 151, -110, 47, -256, -74, -245, 15, -41, 151, 284, 274, -233,
 -191, -47, 80, -165, -127, 168, 217, -172, -194, -183, 155, 72, -252, 228, -93, 1

=> 1

結果はあってそうです。

所感

解答を読んだのですが、なんか全然やり方違う… 部分配列って、元の配列の連続した部分列のことじゃないのかな?

ProgrammingPearls::ex2-5. abc -> cba

2.5 問題より。

配列の回転はabという配列をbaに変えます。abcをcbaにするにはどうしたらよいでしょう。(これは大きさの違うメモリブロックの交換の問題です。)

2つ書いてみました。

memmove

ぱっと思いつくのは、a,cのいずれかを一時的に退避しておいてbをmemmoveするというもの。

  1. a, cのうち、サイズの大きい方(L)を一時バッファに置いて
  2. bをmemmove
  3. a, cのうち、サイズの小さい方(S)をLの方にmemcpy
  4. LをSの方にmemcpy

 1  /*
 2   * abc -> cba
 3   * requires extra memory
 4   *
 5   * left : start of b
 6   * right : end of b
 7   */
 8  void swap_abc(VI& a, int left, int right) {
 9     int size_l = left;
10     int size_r = a.size() - right - 1;
11
12     if (size_l > size_r) {
13        VI tmp(size_l);
14        copy(a.begin(), a.begin() + left, tmp.begin());
15
16        // memmove
17        int pos = size_r;
18        for (int i=left; i<right+1; i++) {
19           a[pos] = a[i];
20           pos++;
21        }
22
23        copy(a.begin() + right+1, a.end(), a.begin());
24        copy(tmp.begin(), tmp.end(), a.begin() + size_r + right - left + 1);
25     } else {
26        VI tmp(size_r);
27        copy(a.begin() + right + 1, a.end(), tmp.begin());
28
29        // memmove
30        int pos = size_r + right - left;
31        for (int i=right; i>=left; i--) {
32           a[pos] = a[i];
33           pos--;
34        }
35
36        copy(a.begin(), a.begin() + left, a.begin() + size_r + right - left + 1);
37        copy(tmp.begin(), tmp.end(), a.begin());
38     }
39  }

しかしこれではメモリを余分に使ってしまいます。ではどうするか。

reverse

ProgrammingPearls::配列のrotateのエレガントな解法を使えばできるんじゃないかと思いました。

  1. reverseを部分的に行って
  2. 全体をreverse

ためしに紙で手書きしながらやってみたら、うまくいきました。

 1  /*
 2   * abc -> cba
 3   * by rotation
 4   *
 5   * left : start of b
 6   * right : end of b
 7   */
 8  void swap_abc_reverse(VI& a, int left, int right) {
 9     reverse(a.begin(), a.begin() + left);
10     reverse(a.begin() + left, a.begin() + right+1);
11     reverse(a.begin() + right+1, a.end());
12     reverse(a.begin(), a.end());
13  }

できました。

所感

memmoveする方は、コード化もややこしいし余分にメモリも食います。 reverseの方は、コードにするのも簡単ですし、効率も良いといういいことづくめに見えます。

書く前にアルゴリズムをよく考えてみることが、結局はバグの少なくて速いコードを書く秘訣なのだとわかります。

ProgrammingPearls::8. 配列中の最大部文和を求める

8 . アルゴリズムデザインのテクニックより。

n要素の配列xを入力とし、xのすべての部分配列の和のうち、最大のものを出力する

で紹介されている5つのアルゴリズムを実装してみました。

自分で考えてみる

部分配列の最大和問題

  1. 配列には正と負の値 (とゼロ) の2種類のものしか入っていないので、それぞれをまとめたら何かうまい方法が見つかるんじゃないかと思ってやってみました。これにかかる時間は、O(n) です。
  2. 次に、小さくなった配列について、隣同士の和を求めたら、何かの役に立つのではないかと思って和の配列をつくってみました。これにかかる時間も、 O(n) になります。定数項は元の配列のサイズ n よりもだいぶ小さくなっているはず。
  3. さて、これからどうしたものか…

けっきょく問題を小さくしただけで、解くためには元の問題と同じアルゴリズムを使う必要があるのではないでしょうか。otz

O(n^3)

いちばん単純なアルゴリズム。愚直にすべての部分和を先頭から求めて最大のものを探します。

 1  int max_subarr_O3(VI &array) {
 2     int n = array.size();
 3     int maxsofar = 0;
 4
 5     for (int i=0; i<n; i++) {
 6        for (int j=i; j<n; j++) {
 7           int sum = 0;
 8
 9           for (int k=i; k<=j; k++) {
10              sum += array[k];
11           }
12           maxsofar = max(sum, maxsofar);
13        }
14     }
15
16     return maxsofar;
17  }

実装で悩んだのは、i, j, kの範囲で境界を入れるかどうかというところ。

O(n^2)

O(n^3) のアルゴリズムで、部分和をキャッシュするもの。

 1  int max_subarr_O2_sum_cache(VI &array) {
 2     int n = array.size();
 3     int maxsofar = 0;
 4
 5     for (int i=0; i<n; i++) {
 6        int sum = 0;
 7
 8        for (int j=i; j<n; j++) {
 9           sum += array[j];
10
11           maxsofar = max(sum, maxsofar);
12        }
13     }
14
15     return maxsofar;
16  }


部分和を求めるループを外に出しておくもの:

 1  int max_subarr_O2_precalc(VI &array) {
 2     int n = array.size();
 3     int maxsofar = 0;
 4
 5     VI cumarr(n+1);
 6     cumarr[0] = 0;
 7
 8     for (int i=0; i<n; i++) {
 9        cumarr[i+1] = cumarr[i] + array[i];
10     }
11
12     for (int i=0; i<n; i++) {
13        for (int j=i; j<n; j++) {
14           int sum = cumarr[j+1] - cumarr[i];
15
16           maxsofar = max(sum, maxsofar);
17        }
18     }
19
20     return maxsofar;
21  }

いずれも境界をループに含めるかどうかを迷ってしまいました。

O(n logn)

分割統治戦略で。これはちょっと考えつきにくいなぁ。

 1  int max3(int a, int b, int c) {
 2     int m = max(a, b);
 3     return max(m, c);
 4  }
 5
 6  // devide-conquer and O(n log(n) )
 7  int max_subarr_Ologn_rec(VI &array, int l, int r) {
 8     // no elements
 9     if (l > r) {
10        return 0;
11     }
12     // one element
13     if (l == r) {
14        return array[l] > 0 ? array[l] : 0;
15     }
16
17     int m = (l+r)/2;
18     int sum, lmax, rmax;
19     sum = lmax = rmax = 0;
20
21     for (int i = m; i>=l; i--) {
22        sum += array[i];
23        lmax = max(sum, lmax);
24     }
25
26     sum = 0;
27     for (int i=m+1; i<r; i++) {
28        sum += array[i];
29        rmax = max(sum, rmax);
30     }
31
32     return max3(lmax+rmax, max_subarr_Ologn_rec(array, l, m), max_subarr_Ologn_rec(array, m+1, r));
33  }

2分探索みたいな書き方になります。2分探索もそうだけど、境界の扱いに気を使います。

O(n)

とどめは、 O(n) のアルゴリズム。コードも一番シンプルになります。

 1  int max_subarr_On_scan(VI &array) {
 2     int maxsofar = 0;
 3     int maxendinghere = 0;
 4
 5     for (size_t i=0; i<array.size(); i++) {
 6        maxendinghere = max(maxendinghere + array[i], 0);
 7        maxsofar = max(maxendinghere, maxsofar);
 8     }
 9
10     return maxsofar;
11  }

境界も難しくありません。でもこれは思いつけない…

所感

O(n logn) くらいまでは自力で導きたいなぁと。 境界を入れるかどうかは、ループを書くときにいつも悩むところです。 きちんとした方法を編み出さないとです。

実行時間

g++のコンパイルオプションを -O3 -fast -mcpu=7450 -g にして実行時間を計ってみました。単位はμ秒です。

n              100      1000        10000
-----------------------------------------
simple        1881   1375914    540120837
sum cache       93      5552       566072
pre calc        71      8267       597084
divide conq     25       236         2747
scan            16        34          306

歴然とした差になりました。 O()記法って便利だなぁ。

ProgrammingPearls::配列のrotate (2)

ProgrammingPearls::配列のrotate の続き。

トリッキーなお手玉 アルゴリズムがやっと実装できました。

コード

 1  // tricky rotation, requires O(n)
 2  void rotate_tricky(VI& array, size_t x) {
 3     int tmp = array[0];
 4     size_t size = array.size();
 5     size_t prev = 0;             // index of element to be stored
 6     size_t next = 0;             // index of element to store
 7     size_t offset = 0;
 8
 9     for (size_t i = 0; i < size; i++) {
10        assert(i >= 0 && i < size);
11
12        prev  = next;
13        next += x;
14
15        if (next >= size) {
16           next = next % size;
17        }
18
19        if (offset == next) {
20           array[prev] = tmp;
21
22           next = ++offset;
23           tmp = array[next];
24        }
25        else {
26           array[prev] = array[next];
27        }
28     }
29  }

所感

19        if (offset == next) {

を、

19        if (0 == next) {

としていて、そこのバグがなかなかとれませんでした。

0にしていたのは本に書いてあったからなのですが、それが適用されるのは初期の場合だけということに気づきませんでした。

どうやって気づいたかというと、テストで失敗したときに実行過程をダンプさせ、手で確認し、期待される振る舞いと比較したら、「あぁ」となったのでした。

実行時間

実装できた3つのアルゴリズムについて、 10000個の要素を持つ配列を3つ左に回転させたときの実行時間を計ってみました。単位はμ秒です。

  • NI (simple) : 33749
  • TRICKY (お手玉) : 7259
  • REVERSE (手のひらを返す) : 4725

STLのstd::reverseを使ったコードが一番速かったのでした。

巻末の解答にもありますが、要素を移動する回数が少ないからお手玉の方がREVERSEよりも速いんじゃないの? と思いがちです。 実際にはお手玉はメモリをとびとびにアクセスすることになり、キャッシュを活かしきれないため、性能が出ないのだろうということでした。なるほど。


本当は、

  1. N = [10,10000)、i = [0, N) で実行時間を計測
  2. 結果をRubyでCSVに変換
  3. NeoOfficeでグラフ化

とするつもりでした。 1と2はできたのですが、3のグラフ化がうまくいきませんでした。NeoOfficeはPowerBook 12″には負荷が高すぎるようで…

ProgrammingPearls::配列のrotate

Programming Pearls 2.1.Bより。

要素がn個ある配列を左方向にi要素分回転させるにはどうすればよいでしょう。… 単純には、n個の作業用配列を作ってnステップで仕事をすればよいのですが、メモリを数十バイトしか使わずに実行時間もnに比例するだけのように回転させられますか。

をやってみます。

その1

一番単純なやり方。実装も素直で間違えませんでした。

 1  // simple rotation, but requires O(n x i)
 2  void rotate_ni(VI& array, size_t x) {
 3     int tmp;
 4     size_t size = array.size();
 5
 6     for (size_t i = 0; i < x; i++) {
 7        // left edge
 8        tmp = array[0];
 9
10        for (size_t j=0; j < size-1; j++) {
11           array[j] = array[j+1];
12        }
13
14        array[size-1] = tmp;
15     }
16  }
17

その2

難しい… まだテストが通りません。 あとで書きます。

その3

これは思いつかないなぁと。 でも確かにプログラムはとてもシンプル。しかも効率が良いというすごいものです。

1  void rotate_reverse(VI& array, size_t x) {
2     reverse(array.begin(), array.begin() + x);
3     reverse(array.begin() + x, array.end());
4     reverse(array.begin(), array.end());
5  }

所感

問題を解くときには、いろいろな見方をすることが大切だということが良くわかります。まずは安直に書いてみて、それをどう改善するかというアプローチもいいと思いますが、その3みたいに全然違う方向から光を当てないと出てこないようなアイデアもあるし。

そのためには、やっぱりたくさんの問題を自力で解くことで、引き出しを増やしていくしかないのかなと思います。

ProgrammingPearls::find non-existant number

2.1 (A) の補題 p.p.15 (訳注):

0から15までのどれかの整数が10個あるとき、この10個の中に登場しない整数を1つ見つける

を解いてみます。

アルゴリズム

p.p.15 の本文中ほどに書かれてあるものを素直に実装してみました。といっても、正しいらしいものに至るまで1時間ほどもかかってしまって… otz

  1  /*
  2   * find a non-existant element
  3   *
  4   * Programming Pearls 2.1 (a)
  5   * $Id: find_nonexistant.cc 493 2006-10-25 15:16:07Z takayama $
  6   */
  7
  8  #include <iostream>
  9  #include <string>
 10  #include <cassert>
 11  #include <vector>
 12  #include <set>
 13  #include <algorithm>
 14  #include <cstdlib>
 15  #include <cassert>
 16  #include <ctime>
 17
 18  using namespace std;
 19
 20  typedef vector<int> VI;
 21  typedef set<int> SI;
 22
 23  void print(int n) {
 24     cout << n << ", ";
 25  }
 26
 27  int find_nonexistant(const VI& array, size_t l, size_t r) {
 28
 29     VI less, greater;
 30     int m = (l+r)/2;
 31
 32     cout << "================================================" << endl;
 33     cout << "(l, r, m) = (" << l << ", " << r << ", " << m << ")" << endl;
 34     for_each(array.begin(), array.end(), print);
 35     cout << endl;
 36
 37     if (array.empty()) { // found
 38        return m;
 39     }
 40
 41     for (VI::const_iterator i = array.begin(); i != array.end(); i++) {
 42        if (*i < m) {
 43           less.push_back(*i);
 44        } else {
 45           greater.push_back(*i);
 46        }
 47     }
 48
 49     cout << "less : ";
 50     for_each(less.begin(), less.end(), print);
 51     cout << endl;
 52
 53     cout << "greater : ";
 54     for_each(greater.begin(), greater.end(), print);
 55     cout << endl;
 56
 57     if (less.size() < greater.size()) {
 58        return find_nonexistant(less, l, m);
 59     }
 60     return find_nonexistant(greater, m, r);
 61  }
 62
 63
 64  // -------------------------------------------------------------------
 65  // test
 66  //
 67  int main(int argc, char **argv) {
 68     assert(3 == argc);
 69
 70     size_t N = atoi(argv[1]);
 71     size_t M = atoi(argv[2]);
 72
 73     SI int_set;
 74     VI array;
 75
 76     ::srandom(::time(NULL));
 77
 78     // make N random numbers from [0..M)
 79     while (N != int_set.size())  {
 80        int_set.insert(random() % M);
 81     }
 82
 83     // set -> vector
 84     for (SI::iterator si = int_set.begin(); si != int_set.end(); si++) {
 85        array.push_back(*si);
 86     }
 87
 88     random_shuffle(array.begin(), array.end());
 89
 90     // find non-existant number
 91     int result = find_nonexistant(array, 0, M);
 92
 93     cout << result << endl;
 94
 95     // check result
 96     VI::iterator found = find(array.begin(), array.end(), result);
 97     assert(found == array.end());
 98
 99     return found == array.end();
100  }

実行結果

% ./find_nonexistant 10 15
================================================
(l, r, m) = (0, 15, 7)
11, 5, 2, 14, 7, 4, 10, 1, 13, 6,
less : 5, 2, 4, 1, 6,
greater : 11, 14, 7, 10, 13,
================================================
(l, r, m) = (7, 15, 11)
11, 14, 7, 10, 13,
less : 7, 10,
greater : 11, 14, 13,
================================================
(l, r, m) = (7, 11, 9)
7, 10,
less : 7,
greater : 10,
================================================
(l, r, m) = (9, 11, 10)
10,
less :
greater : 10,
================================================
(l, r, m) = (9, 10, 9)

9

所感

終了条件が何なのかを掴むのに時間がかかりました。というか、未だにこれでちゃんと終わるかどうか定かではないのですが。

ProgrammingPearls::ex1-3

3 . 実行時間を短くすることはデザイン上の重要な目標の1つですが、このコラムのプログラムは十分速いものでした。自分のシステム上でビット列のソートを実装し、その実行時間を計測してみてください。システム付属のソートや問題1の解のソートと比べるとどうですか。ここでnは10,000,000とし、入力ファイルには1,000,000個の整数があるとして実行してみましょう。

ex1-4でつくったデータを基に、実行時間を測ってみました。そしたら悲しい結果に。

入力

1,000,000個の整数が1行1個あるファイル

計測方法

time exec < numbers.txt > sorted.txt

計測項目

  • sort -n : システムにあるsort(1) を使って、ファイルをソートするのみ。
  • Ex1-1 : setを使ったシンプルな実装
  • Ex1-3 : Bitstreamを使った実装

  • O0 : コンパイラの最適化をなしに

  • O3 : -O3 -fast -mcpu=7450

結果

ex1-3

sort(1)の圧勝となってしまいました…

課題

  • なぜ sort(1) に勝てないのか?
  • Ex1-3は最適化してもさほど性能が上がっていないが、それはなぜなのか?
  • Cで書いてみる

追記 (10/01 14:05)

ex1-1を、Cで書いてみました。qsort(3)を使います。 それで得られた結果を、さっきのグラフに付け加えました。もうちょっとまじめにグラフを描いてみたらどうなるかというと、

ex1-3 (revised)

こうなりました。Cはやっっ

こうしてみると、C++はSystem時間が大きいようです。iostreamのせいなのかな?

追記 (10/01 16:49)

解答を見て。

  • そうか、マスクは剰余のために使うのだった
  • intの配列でやると速いのかな

なんてことを考え、

  • Cによるビット列ソート
  • C++によるint配列 + 割り算と剰余をシフト演算で

を書いてみました。計測してみたところ、Cによるビット列ソートはかなり速くなりました。しかし、C++の方はさほど速くなりませんでした。

C++では、O3くらいにすると2のべき乗の割り算や剰余はコンパイラがシフト演算に最適化してくれるような覚えがあります。アセンブラを見たら分かるはず。

PPCアーキテクチャなので、アライメントの都合によって、1バイトごとの操作よりも4バイトまとめて操作できるint配列にしたら効果があるんじゃないかと期待しましたが、さほどの効果はないようです。

やはり、C++は生Cに比べるとてきめんに遅いという結果になるのでしょうか。