少し前に、非常に簡潔な言葉遣いで興味深い競争問題を解決し最適化することについての記事
「 grey_wolfs "パリンドロームを求めて」を読んだ。
「10進数585は2進数で1001001001です。 それは両方のベースでパリンドロームです。 n番目の回文数を見つけます。 または、ロシア語で:「2進数システムの10進数585は1001001001のように見えます。両方の番号システムの回文です。 n番目の回文を見つけます。
著者は、
1から
Nまでのすべての数値と計算の複雑さ
O(N * log(N))を並べ替えてチェックすることにより、最も簡単なソリューションから魅力的なストーリーを始めました
。Nはチェックする最大数です。
ログ(N)係数が必要な理由は チェックされた番号ごとに、その桁数に応じていくつかのアクションが実行されます。
最初の作業の最適化、つまり、すべての数値の列挙を10進パリンドロームのみの列挙に置き換えると、計算される数が
O(N 1/2 * log(N))に劇的に改善されました。
) また、アルゴリズムの複雑さによるいくらかの損失にもかかわらず、十分に大きい
Nの場合、実行時間は桁違いに予測可能に改善されました。
いくつかの小さな最適化を行った後、著者はランタイムを
3倍改善し、この良い結果で停止することにしました。
まだ最後まで読んでいないので、同じ計算の複雑さで、
O(N 1/2 * log(N))はおそらく文字列ではなく、直接数値ではるかに速く動作すると思いました。 つまり、算術演算ではなく算術演算を使用してパリンドロームのシーケンスを生成します。
そして、同じ長さのパリンドローム数のシーケンスは、
BASE 、
BASE 2 、
BASE 3 、...要素ごとに調整する必要がある一定のステップを伴う算術的進行に似ているため、まったく難しくないことが判明しました。
たとえば、幅が
5の 10進数回文の
場合、メインステップは
+100になります。
10001, 10101, 10201, 10301, 10401, 10501, 10601, 10701, 10801, 10901, 11001
シーケンスの最後の要素はパリンドロームではないため、
+10から
11011
への修正が必要です
11011, 11111, 11211, 11311, 11411, 11511, 11611, 11711, 11811, 11911, 12011
シーケンスの要素の最後の要素には、再び
+10から
12021
への修正が必要です。
したがって、
99番目と
100番目の要素を取得します:
19991, 20091
100番目の要素に必要な補正
+ 10-99 =
-89の結果、
20002
なり、
99999
に達するまで続行します。
パリンドロームの算術生成が非常に高速になったため(平均して、数個のアセンブラーコマンドのみ)、あるベースでのパリンドロームの生成+別のベースでのパリンドロームのチェックは、両方のベースでのパリンドロームの生成と比較よりも遅いと判断しました。
その結果、次のC ++コードが取得されました。
必要な基礎度を備えた補助構造:
#undef POWER #define POWER(exp) m_powers[exp] template<typename IntT> struct BaseData { static const size_t MAX_WIDTH = sizeof(IntT) * CHAR_BIT; BaseData( size_t base, IntT maxValue) : m_base(base) { POWER(0) = IntT(1); for (size_t i = 1; i < MAX_WIDTH; ++i) { POWER(i) = POWER(i - 1) * IntT(base); if (POWER(i) >= maxValue) { m_maxWidth = i - 1; break; } } } size_t m_base; size_t m_maxWidth; IntT m_powers[MAX_WIDTH]; };
パリンドロームイテレーター:
template<typename IntT> class Iterator { static const size_t MAX_WIDTH = sizeof(IntT) * CHAR_BIT; private: struct IncrementData { size_t m_counter;
そして最後にメイン:
int _tmain(int argc, _TCHAR* argv[]) { int64 limit = 18279440804497281; BaseData<int64> base0(2, limit); BaseData<int64> base1(10, limit); Iterator<int64> it0(base0.m_base, base0.m_powers); Iterator<int64> it1(base1.m_base, base1.m_powers); while (*it0 <= limit) { if (*it0 < *it1) ++it0; else if (*it0 > *it1) ++it1; else { std::cout << *it0 << std::endl; ++it0; ++it1; } } return 0; }
18279440804497281に相当する
56番目のパリンドロームは、
1.85秒後に取得されました。これは、以前の結果よりも約
150倍高速です(
grey_wolfsコンピューターのIntel Core i7-3770 CPU @ 3.40GHzと同等の計算能力があると仮定)。 もちろん、私の利点の大部分はPHPからC ++への移行によって生じましたが、私はまだ満足して手をこすりました:アルゴリズムはすでに1秒あたりほぼ
300,000,000回のパリンドロームを整理しており、さらに2枚の切り札がありました:奇数の10進パリンドロームのみを生成します(
- 20-25 %)およびマルチスレッドを使用(8スレッドで
-70-85 %)...
そして、私はちょうど私を「殺した」というコメントを見ました:
@hellman
少し前まで、codechefコンテストの1つでも同じ問題が発生していました。 2から16までの微積分システムのベースのみが任意であり、2 ^ 60より小さい最初の1000回の回文が必要でした。 制限時間は8秒です(ただし、入力には5つのテストがある場合があります)。 社説があります。
私のアルゴリズムは、
15秒で
2 60までのすべての回文を見つけました。 最悪の場合、マルチスレッドを使用しても制限時間に収まりません。 しかし、エディトリアルには、「なぜこの質問の制限時間は8秒と非常に長いのですか?質問でこのような高い制限が見られないのはなぜですか?」という。笑的なコメントもありました。
満足度はすぐに失望に変わりました。計算の複雑さ
O(N 1/2 * log(N))の列挙の私の実装は、理論上の最小限界に近かったのですが、それでもまだ十分ではありませんでした。 エディトリアルで説明されている理論的な解決策を見つけようとしましたが、コードを見ても、
O(N 1/4 * log(N))付近の計算の複雑さで問題を解決できることに初めて気付きました。
この時点で、タスクの作業を停止しましたが、頭から出ませんでした。 そして数日後、私は彼女に戻りました。
継続する 。