このページではCodeIQ*1で結城浩先生から出題されたクロッシング問題について解説しています。

はじめに

  • 今回、締め切りの時間をすっかり忘れていて、結城先生には最低限の回答だけを提出して終わってしまったことをお詫びします。
  • 提出しようと思った解説文、感想を、ココに記して公開します。

問題を読んで

  • 今回の実行時間を測定する問題は、必ずオーダ計算の問題に突き当たるだろうと予想していました。
  • クロッシング問題は、今までの様に問題を解くだけでなく、実際に実行時間を測定することも、かなり実世界よりな問題になったと思います。

解法

  • 交点の数は、光線の本数が少ない状態から順番に推定していきました。
    • 光線が2本しか無い時は、交わるか交わらないかの2種類だけです。
    • 光線が3本ある時は、交点は最大3点になります。これは_3C_2で求まります。
    • 光線が3本あって、交点の数が最大になる場合は、既に2本の光線が交わった上で、その両方と3本目の光線が交わる状態です。
    • 今回の問題では光線は発射される時点で左から順番に数字が大きなっていきますから、一番大きい数の光線は一番右側から発されます。
    • この光線が一番左側に到着するときに、全ての光線と交わり、交点の数が最大になります。
  • さて、今回の問題は最大ではなく、交点がいくつか数える問題です。
  • そのために、最大からいくつ足りないか、を数え上げることにしました。

数え上げの理論

  • 光線が、ある数nを持ってるとして、こいつが交わる可能性がある光線は、
    • 自分より左側に到達する、nより大きい数を持つ光線
    • 自分より右側に到達する、nより小さい数を持つ光線
  • の2種類に分けることができます。
  • ここで気をつけないと行けないのはダブりでカウントしてしまう事態です。
    • 直線AとBが交わって交点ができるとして、 交点は1つなのに、直線Aで1点、直線Bでもう1点と、計2点としてカウントしてしまうと、重複してカウントしてしまいます。
  • 今回はそれを避けるために、到達した光線の内、左側から順番にカウントしていくことにしました。
  • 感覚としては、1本1本、光線をほどいていくイメージです(もしくは皮を剥いでいくイメージなのですが、光線が1次元なので、ほどいていくと表現しました)
  • 到達した光線のうち左側から1本ずつ光線をほどいていくと、どんどん交点の数は減っていきます。
  • つまり、重複をカウントしてしまうことがありません。

実際の手続き

  • さて、交点の数を数えるためには、左側から順番に
    • 自身より右側に到達する、自身より小さい数を持つ光線
  • の本数を数えれば良いことになります。
  • これは数式で書けば以下のようになります。
    • 交わるかどうかを判定する関数を以下のように定義します
      f\left(i, j\right) = \begin{array}\left{1:P_i > P_j\\0: else\right}\end{array}
  • なお、Pは数列を表し、ここでは到着した光線の番号の集合です。
  • 自身より右側に到達する、自身より小さい数を持つ光線の本数は、f(i,j)を使って、
    S(i) = \sum_{j=i+1}^N f(i, j)
  • と表せます。
  • i番目の光線より右側に到達する光線の本数がS(i)で表されるわけです。
  • 今回の問題では、この総和を求めますので、S(i)の総和を求めれば良いことになります。
    \sum_{i=1}^N S(i) = \sum_{i=1}^N \sum_{j=i+1}^N f(i, j)
  • これだけで求めることができます。

数学的な議論から実行時間の話へ。

  • 薄々、オーダ量の問題だろうと思いながら、ここまでコーディングしました。
  • いざcrossing.txtの処理にかかると、これが応答がなかなか返ってこない
  • デバッグをしてみると、計算量が膨大なのが原因で、iの添字が1秒間に数百〜千程度しか増えていません。
  • 問題文は約30万の光線がありますので、数千秒のオーダがかかります。(1時間程度)
  • オーダ量の観点から行けば当然の帰結で、二重に総和を求めているので、計算量のオーダはn^2だけかかります。
  • これを削減する必要があります。

数え上げから2分木の構築へ

  • 計算量がn^2になってしまっているのは、二重ループがあるせいです。
  • この二重ループはかなり無駄です。
  • ある数nがあったとして、これより右側に存在する、nより小さい数を探すわけで、毎回30万件を探し当てるのはかなりバカバカしいです。
  • 今回の問題はもうひとつ特徴がありました。
    • それは光線の番号が必ず1つずつ増え、光線の数と、光線が持つ最大の数が必ず一致する、ということです。
    • つまりある数nより、小さい数を持つ光線は必ずn-1本存在することが問題で保証されています。
  • 自身より小さい数を持つ光線は、自身の左側か右側のどちらかにしか到達しません。
  • 左側に到達した光線の数は、走査する過程でアクセスしますので、アクセスした数をもとに、2分木を作成しました
  • 2分木を使うことで、自身より小さい数が、2分木内に幾つ含まれているかをカウントしました。

子の要素数を持つ2分木

  • ある要素が、いくつ子を持っているか2分木では通常数え上げる必要があります。
  • しかし、今回は計算時間をさらに短縮するため、2分木にデータを挿入する際に、親ノードへ通知する仕組みを作りました。
  • ある要素が追加された時、追加された要素は自身の親、先祖の要素全てで、カウンタを1つずつ足していきます。
  • 今回のソースコードで言えば88行目−93行目のwhileループ内がそれです。
  • 一つずつ自身の親を手繰りながら、カウンタを1つずつ足していきます。
  • これをすることで、全ての要素は、自身を含めて自身以下にいくつ子要素が存在するか、常に把握してることになります。
  • 一方で、今まで現れた数より小さい数の個数を調べるためには、二分木から、一番近い数を探し、その際のノードの要素を数え上げることで実現されます。
  • 2分木での探索はO(\log n)で表わされるので、全体の計算量はO(n \log n)となり、O(n^2)より小さくなります。

実行時間に付いて

  • 今回はWindows.hにあるclock()という関数を使いました。
  • この関数の分解能は1E-3秒(1ms)であり、それによればcrossing.txt は 400ms 程度で計算が完了しました。
  • うち、読み込みに260ms、処理に140ms 程度でした。

計算時間の検討

  • 本当にO(n^2)で計算時間が増えるのか、nの値を変えて乱数を発生させながら、実際プロットしてみたのが、以下の図
    crossingComputationCost.png
  • 両対数軸になってるのがミソ
  • 参考までに緑色でn\log nを重ねて描いてみたが、増え方が線型探索と段違いなのが一目瞭然である。

ソースコード

  • 偉そうに解説を書いておきながら、unsigned int だとオーバーフローすることに気づいておらず、必要だったのは、unsigned long long int。
  • 結果残念賞。
  • 悔やんでも悔やみきれぬ。
  • 次回でリベンジ!
    1. #include <iostream>
    2. #include <vector>
    3. #include <fstream>
    4. #include <time.h>
    5. #include <Windows.h>
    6. #include <algorithm>
    7. #include <cstdlib>
    8.  
    9. //下記defineを有効にすると、結果をミリ秒から秒単位に丸め(切り捨て)て表示する
    10. #define OUTPUT_TIME_IN_ROUNDED_SECOND
    11. //下記defineを有効にすると、線型探索でも同じ配列をカウントし、数が合っているか、出力する
    12. //#define VERIFY_ALGORITHM
    13.  
    14. const int DEFAULT_NUMBER_OF_DATA = 1000;
    15.  
    16. int main(int argc, char **argv);
    17. int readData(std::vector<int>& arrivedLightNumbers);
    18.  
    19. // 2分木の要素
    20. // キーの他に、
    21. // ・自身より小さい数を持つ子のインデックス
    22. // ・自身より大きい数を持つ子のインデックス
    23. // ・自身の親のインデックス
    24. // ・自身以下の子の要素の総数 (自身含む)
    25. // を変数として持つ
    26. struct _binaryTreeElement{
    27. public:
    28. _binaryTreeElement(int key):
    29. m_key(key),
    30. m_left(-1), 
    31. m_right(-1), 
    32. m_parent(-1), 
    33. m_count(1){
    34. }
    35. _binaryTreeElement(int key, int parent):
    36. m_key(key),
    37. m_left(-1), 
    38. m_right(-1), 
    39. m_parent(parent), 
    40. m_count(1){
    41. }
    42. // キー
    43. int m_key;
    44. // 左の子のポインタ(自身より小さい数を持つ)
    45. int m_left;
    46. // 右の子のポインタ(自身より大きい数を持つ)
    47. int m_right;
    48. // 親のポインタ
    49. int m_parent;
    50. // 自身以下の子の要素の総数 (自身を含む)
    51. int m_count;
    52. };
    53.  
    54. typedef struct _binaryTreeElement binaryTreeElement;
    55.  
    56. // 2分木のクラス
    57. class CBinaryTree
    58. {
    59. public:
    60. CBinaryTree() {};
    61.  
    62. // 要素を追加する
    63. void insert( int key ){
    64. if(m_binaryTree.size() == 0){
    65. // 最初の要素
    66. m_binaryTree.push_back(binaryTreeElement(key));
    67. }else{
    68. // 同じ要素を探す
    69. int index = searchIndex( key );
    70. if(m_binaryTree[index].m_key == key){
    71. // 同じ要素が見つかった場合
    72. // 今回の出題では見つからないはずなので、
    73. // exitして通知する
    74. std::cerr << "Duplicate key found in CBinaryTree class" << std::endl;
    75. std::cerr << "key :" << key << std::endl;
    76. exit( -1);
    77. }else if(key < m_binaryTree[index].m_key){
    78. // 追加しようとしているキーが現在のキーより小さい場合
    79. m_binaryTree.push_back(binaryTreeElement(key, index));
    80. m_binaryTree[index].m_left = m_binaryTree.size() - 1;
    81. index = m_binaryTree[index].m_left;
    82. }else{
    83. // 追加しようとしているキーが現在のキーより大きい場合
    84. m_binaryTree.push_back(binaryTreeElement(key, index));
    85. m_binaryTree[index].m_right = m_binaryTree.size() - 1;
    86. index = m_binaryTree[index].m_right;
    87. }
    88. while(index != 0){
    89. // カウンタを増やしながら親を順に手繰る
    90. index = m_binaryTree[index].m_parent;
    91. m_binaryTree[index].m_count++;
    92. }
    93. }
    94. };
    95.  
    96. // 指定されたキーが2分木内に存在するか否かを返す
    97. bool doesInclude( int key ) const {
    98. int index = searchIndex(key);
    99. if(index == -1){
    100. return false;
    101. }
    102. if(0 <= index && index < (int)m_binaryTree.size() && 
    103. (m_binaryTree[index].m_key == key)){
    104. return true;
    105. }else{
    106. return false;
    107. }
    108. }
    109.  
    110. // 指定されたキーより、小さいキーを持つノードの数を数える
    111. int countSmallerThan( int key ) const {
    112. int index = 0;
    113. int count = 0;
    114. if(m_binaryTree.empty() == true){
    115. // 空っぽなら必ず0
    116. return 0;
    117. }
    118. while(1){
    119. if(key == m_binaryTree[index].m_key){
    120. if(m_binaryTree[index].m_left != -1){
    121. count += m_binaryTree[m_binaryTree[index].m_left].m_count;
    122. }
    123. return count;
    124. }else if(key < m_binaryTree[index].m_key){
    125. if(m_binaryTree[index].m_left == -1){
    126. return count;
    127. }else{
    128. index = m_binaryTree[index].m_left;
    129. }
    130. }else{
    131. if(m_binaryTree[index].m_left != -1){
    132. count += m_binaryTree[m_binaryTree[index].m_left].m_count;
    133. }
    134. count++;
    135. if(m_binaryTree[index].m_right == -1){
    136. return count;
    137. }else{
    138. index = m_binaryTree[index].m_right;
    139. }
    140. }
    141. }
    142. };
    143.  
    144. // 2分木に存在するノードの数を数える
    145. unsigned int getNodeCount() const {
    146. return (unsigned int)m_binaryTree[0].m_count;
    147. }
    148.  
    149.  
    150. private:
    151. // 指定したキーを持つノードを探す
    152. // 返された値は、実体を持つ vector 内での位置を表す
    153. int searchIndex( int key ) const {
    154. int index = 0;
    155. if(m_binaryTree.size() == 0){
    156. // 要素が1つも無い場合
    157. return -1;
    158. }
    159. while(1){
    160. if(key == m_binaryTree[index].m_key){
    161. return index;
    162. }else if(key < m_binaryTree[index].m_key){
    163. if(m_binaryTree[index].m_left == -1){
    164. return index;
    165. }else{
    166. index = m_binaryTree[index].m_left;
    167. }
    168. }else{
    169. if(m_binaryTree[index].m_right == -1){
    170. return index;
    171. }else{
    172. index = m_binaryTree[index].m_right;
    173. }
    174. }
    175. }
    176. };
    177.  
    178. // 2分木の実体、vector
    179. std::vector<binaryTreeElement> m_binaryTree;
    180. };
    181.  
    182. #ifdef VERIFY_ALGORITHM
    183. class Random
    184. {
    185. public:
    186. // コンストラクタ
    187. Random()
    188. {
    189. srand( static_cast<unsigned int>( time(NULL) ) );
    190. }
    191.  
    192. // 関数オブジェクト
    193. unsigned int operator()(unsigned int max)
    194. {
    195. double tmp = static_cast<double>( rand() ) / static_cast<double>( RAND_MAX );
    196. return static_cast<unsigned int>( tmp * max );
    197. }
    198. };
    199.  
    200. // N件の乱数データを生成する
    201. void generateData(int argc, char** argv, std::vector<int>& arrivedLightNumbers){
    202.  
    203. int numberOfData;
    204.  
    205. if(argc <= 1){
    206. numberOfData = DEFAULT_NUMBER_OF_DATA;
    207. }else{
    208. numberOfData = max(atoi(argv[1]), 2);
    209. }
    210.  
    211. for(int i = 0;i < numberOfData;i++){
    212. arrivedLightNumbers.push_back(i+1);
    213. }
    214.  
    215. Random r;
    216. random_shuffle( arrivedLightNumbers.begin(), arrivedLightNumbers.end(), r );  // ランダムシャッフル
    217. }
    218. #endif //VERIFY_ALGORITHM
    219.  
    220. // ファイルからデータを読み込む
    221. int readData(int argc, char** argv, std::vector<int>& arrivedLightNumbers){
    222. char *dataFileName;
    223. if(argc <= 1){
    224. // ファイル名が指定されなかった場合
    225. std::cerr << "ファイル名が指定されていません" << std::endl;
    226. std::cerr << argv[0] << " [データファイル名] として使用して下さい" << std::endl;
    227. return -1;
    228. }else{
    229. dataFileName = argv[1];
    230. }
    231.  
    232. std::fstream input(dataFileName);
    233. if(input.is_open() == false){
    234. // オープンに失敗した場合
    235. std::cerr << "ファイルオープンに失敗" << std::endl;
    236. std::cerr << dataFileName << std::endl;
    237. std::cerr << argv[0] << " [データファイル名] として使用して下さい" << std::endl;
    238. return -2;
    239. }
    240.  
    241. // 読み込んだデータ内で最大の数字
    242. int maxNumber = 0;
    243. int readBuffer;
    244. while(input >> readBuffer, !input.eof()){
    245. // vector 内に追加
    246. arrivedLightNumbers.push_back(readBuffer);
    247. if(readBuffer > maxNumber){
    248. maxNumber = readBuffer;
    249. }
    250. }
    251. if(maxNumber != arrivedLightNumbers.size()){
    252. // 読み込んだ値の内、最大の数と、読み込んだデータの数が一致しない場合
    253. // (=データ内容と件数が不整合の場合)
    254. std::cerr << "最大の番号が" << maxNumber << "で、データの件数が" << arrivedLightNumbers.size() << "件です。" << std::endl;
    255. std::cerr << "データが壊れています" << std::endl;
    256. return -3;
    257. }
    258.  
    259. return arrivedLightNumbers.size();
    260. }
    261.  
    262.  
    263. int main(int argc, char **argv){
    264.  
    265. using namespace std;
    266.  
    267. // 到着した光線の番号を保持する
    268. vector<int> arrivedLightNumbers;
    269. // 時刻測定用
    270. clock_t begin, end;
    271. // 交差点の総数
    272. // unsigned int totalNumberOfCrossPoint = 0; // ← unsigned int だと桁不足
    273. unsigned int totalNumberOfCrossPoint = 0;
    274. // 光線の本数
    275. int maximumNumberOfLight = 0;
    276. // ある数より小さい数が現れた回数
    277. int countOfSmallerNumber = 0;
    278. // 処理に必要な2分木クラス
    279. CBinaryTree binaryTree;
    280.  
    281. // 計測開始
    282. begin = clock();
    283. #ifdef VERIFY_ALGORITHM
    284. // 線型探索の場合の結果
    285. unsigned int totalNumberOfCrossPointInLinear = 0;
    286. // 線型探索の場合の所要時間
    287. clock_t endLinear;
    288. // 乱数のデータを生成する
    289. generateData(argc, argv, arrivedLightNumbers);
    290. #else
    291. // データをファイルから読み込む
    292. readData(argc, argv, arrivedLightNumbers);
    293. #endif // VERIFY_ALGORITHM
    294.  
    295. for(unsigned int i = 0;i < arrivedLightNumbers.size();i++){
    296. // 自身より小さい数が既に何回現れたか調べる
    297. countOfSmallerNumber = binaryTree.countSmallerThan(arrivedLightNumbers[i]);
    298. // 1本の光線は「自身の右側に自身より小さい数がいくつ存在するか」で表される
    299. // 光線の番号は順番に振られているので「自身の数-1-自身より左側に存在する自身より小さい数」が等価である
    300. totalNumberOfCrossPoint += arrivedLightNumbers[i] - 1 - countOfSmallerNumber;
    301. // 自身の数を、現れた数として2分木に追加
    302. binaryTree.insert(arrivedLightNumbers[i]);
    303. }
    304. // 計測終了
    305. end = clock();
    306. #ifdef VERIFY_ALGORITHM
    307. for(unsigned int i = 0;i < arrivedLightNumbers.size();i++){
    308. for(unsigned int j = i;j < arrivedLightNumbers.size();j++){
    309. if(arrivedLightNumbers[j] < arrivedLightNumbers[i]){
    310. totalNumberOfCrossPointInLinear++;
    311. }
    312. }
    313. }
    314. endLinear = clock();
    315. cout << arrivedLightNumbers.size() << ',' << endLinear - end + 1 << ',' << totalNumberOfCrossPointInLinear << ',' << end - begin + 1 << ',' << totalNumberOfCrossPoint << endl;
    316. #else
    317. #ifdef OUTPUT_TIME_IN_ROUNDED_SECOND
    318. cout << totalNumberOfCrossPoint << ',' << (int)((double)(end - begin) / 1000) << endl;
    319. #else
    320. cout << totalNumberOfCrossPoint << ',' << end - begin << endl;
    321. #endif //OUTPUT_TIME_IN_ROUNDED_SECOND
    322. cout << "ENV: C++" << endl << endl;
    323. #endif //VERIFY_ALGORITHM
    324.  
    325. }

*1  CodeIQ|ITエンジニアのための実務スキル評価サービス
*2  CodeIQの問題に挑戦しよう!

添付ファイル: filecrossingComputationCost.png 517件 [詳細]

トップ   編集 凍結 差分 バックアップ 添付 複製 名前変更 リロード   新規 一覧 単語検索 最終更新   ヘルプ   最終更新のRSS
Last-modified: 2013-09-03 (火) 20:16:04 (2208d)