web-dev-qa-db-ja.com

整数の範囲の各桁を数える方法は?

家、ロッカーのドア、ホテルの部屋などに番号を付けるために使用される金属製の数字を販売するとします。顧客がドア/家に番号を付ける必要がある場合は、出荷する各数字の数を見つける必要があります。

  • 1から100
  • 51から300
  • 左側にゼロがある1〜2,000

明白な解決策は、最初の数から最後の数までループを実行し、カウンターを左にゼロがあるまたはない文字列に変換し、各桁を抽出し、それをインデックスとして使用して10の整数の配列をインクリメントすることです。

整数の範囲全体をループする必要なしに、これを解決するより良い方法があるのだろうか。

任意の言語または疑似コードのソリューションを歓迎します。


編集:

回答のレビュー
John at CashCommonsおよびWayne Conradは、私の現在のアプローチは十分で高速であるとコメントしています。ばかげた類推をさせてください:チェス盤の正方形を1分未満で数えるタスクが与えられた場合、正方形を1つずつ数えることでタスクを完了することができますが、の方が優れています解決策は、建物のタイルを数えるように後で求められることがあるため、辺を数えて乗算を行うことです。
Alex Reisnerは、非常に興味深い数学的法則を示していますが、残念ながら、この問題には関係がないようです。
Andresは、私が使用しているのと同じアルゴリズムを提案していますが、部分文字列ではなく%10演算で数字を抽出しています。
John at CashCommonsおよびphordは、必要な数字を事前に計算し、ルックアップテーブルに格納することを提案します。または、生の速度のために、配列。これは、絶対的な、移動できない、完全な整数の最大整数値が設定されている場合に適切な解決策になる可能性があります。私はそれらの1つを見たことがありません。
高性能マークおよびストレーナーは、さまざまな範囲に必要な数字を計算しました。 1つのミロンの結果は比率があることを示しているようですが、他の数の結果は比率が異なります。
strainerは、10の累乗である数値の数字をカウントするために使用できるいくつかの式を見つけました。 Robert Harveyは、MathOverflowに質問を投稿する非常に興味深い経験をしました。数学の男の1人が、数学的表記を使用してソリューションを作成しました。
Aaronaughtは、数学を使用してソリューションを開発およびテストしました。それを投稿した後、彼はMath Overflowに由来する数式をレビューし、その中に欠陥を発見しました(Stackoverflowをポイントする:)。
noahlavineはアルゴリズムを開発し、疑似コードで提示しました。

新しいソリューション
すべての回答を読み、いくつかの実験を行った後、1〜10の整数の範囲で-1:

  • 1から9桁の場合、n * 10(n-1) ピースが必要です
  • 数字0の場合、先行ゼロを使用しない場合、n * 10n-1 -((10-1)/ 9)が必要
  • 数字0の場合、先行ゼロを使用すると、n * 10n-1 -nが必要です

最初の式はstrainerによって(そしておそらく他の人によって)発見され、私は試行錯誤によって他の2つを発見しました(ただし、他の回答に含まれている可能性があります)。

たとえば、n = 6の場合、範囲は1〜999,999です。

  • 1から9の数字には6 * 10が必要です5 =それぞれ600,000
  • 先行ゼロなしの数字0の場合、6 * 10が必要です5 –(106-1)/ 9 = 600,000-111,111 = 488,889
  • 数字の0の場合、先行ゼロを使用して、6 * 10が必要です5 – 6 = 599,994

これらの数値は、High-Performance Markの結果を使用して確認できます。

これらの式を使用して、元のアルゴリズムを改善しました。それでも、整数の範囲の最初の数から最後の数までループしますが、10の累乗の数を見つけた場合、数式を使用して、1から9までの全範囲の数量を数字に追加します。または1から99または1から999など。疑似コードのアルゴリズムは次のとおりです。

integer First、Last //範囲内の最初と最後の数値
 integer Number //ループ内の現在の数値
 integer Power // Powerは、数式の10 ^ nのnです
 integer Nines // Ninesは10 ^ n-1、10 ^ 5-1 = 99999の結果です。
 integer Prefix //数値の最初の桁。 14,200の場合、プレフィックスは142 
 array 0..9 Digits //すべての数字のカウントを保持します
 
 FOR番号=最初から最後まで
 CALL TallyDigitsForOneNumber WITH Number、1 //各桁の数を集計します
 //数値を1増やします
 //最適化の開始。コメントは、数値= 1,000および最終= 8,000に対するものです。
電力=数値の終わりのゼロ// 1,000の場合、電力= 3 
 IF電力> 0 //数値は0 00 000で終わりますetc 
 Nines = 10 ^ Power-1 // Nines = 10 ^ 3-1 = 1000-1 = 999 
 IF Number + Nines <= Last // If 1,000 + 999 <8,000、addフルセット
数字[0-9] + = Power * 10 ^(Power-1)// 3 * 10 ^(3-1)= 300を数字0〜9に追加
数字[0]-= -Power //数字0を調整(先頭のゼロの数式)
接頭辞=数値の最初の桁// 1000の場合、接頭辞は1 
 CALL TallyDigitsForOneNumber WITH Prefix、Nines // Tally各
のカウント//プレフィックス内の数字、
 // 999だけインクリメント
 Number + = Nines //ループカウンターを999サイクルインクリメント
 ENDIF 
 ENDIF 
 //オプティの終了mization 
 ENDFOR 
 
 SUBROUTINE TallyDigitsForOneNumber PARAMS Number、Count 
 REPEAT 
 Digits [Number%10] + = Count 
 Number =数値/ 10 
 UNTIL数値= 0 

たとえば、範囲が786から3,021の場合、カウンタは増分されます。

  • 786から790まで1ずつ(5サイクル)
  • 9まで790から799(1サイクル)
  • 799から800まで1
  • 99までに800から899まで
  • 899から900まで1ずつ
  • 99までに900から999
  • 999から1000まで1ずつ
  • 999年までに1000から1999
  • 1999年から2000年まで1ずつ
  • 2000年から2999年まで999年
  • 2999から3000まで1
  • 3000から3010まで1ずつ(10サイクル)
  • 3010から3019まで9ずつ(1サイクル)
  • 3019から3021まで1ずつ(2サイクル)

合計:28サイクル最適化なし:2,235サイクル

このアルゴリズムは、先行ゼロなしで問題を解決することに注意してください。先行ゼロでそれを使用するには、ハックを使用しました:

先行ゼロのある700から1,000の範囲が必要な場合は、10,700から11,000のアルゴリズムを使用して、桁1のカウントから1,000-700 = 300を引きます。

ベンチマークとソースコード

私は元のアプローチをテストしましたが、%10を使用した同じアプローチといくつかの大きな範囲の新しいソリューションをテストし、次の結果が得られました。

元の104.78秒
%10の場合83.66 
 10の累乗0.07の場合

ベンチマークアプリケーションのスクリーンショット:
alt text
(ソース: clarion.sca.mx

完全なソースコードを確認するか、ベンチマークを実行する場合は、次のリンクを使用してください。

承認された回答

noahlavineソリューションは正しいかもしれませんが、疑似コードを追跡できませんでした。いくつかの詳細が欠落しているか、完全に説明されていないと思います。

Aaronaughtソリューションは正しいようですが、コードが私の好みには複雑すぎます。

strainerの答えを受け入れました。彼の考えがこの新しいソリューションを開発するように導いてくれたからです。

52

数字から数字を引き出すには、modを実行できなかった場合にのみ、コストのかかる文字列変換を実行するだけで済みます。数字は、次のように最もすばやくプッシュできます。

feed=number;
do
{ digit=feed%10;
  feed/=10; 
  //use digit... eg. digitTally[digit]++;
  }
while(feed>0)

そのループは非常に高速である必要があり、数字を集計する最も簡単な方法として、開始番号から終了番号のループ内に配置できます。

より速く、より広い数値の範囲で、0から数値* 10 ^有意性までのすべての数字を集計する最適化された方法を探しています(最初から最後まで私を驚かせます)

これは、いくつかの単一の有効数字の数字の集計を示す表です。これらは0を含みますが、上位の値自体ではありません。これは見落としでしたが、パターンが少しわかりやすくなっています(上位の値の数字がここにありません)。これらの集計には後続ゼロは含まれませんが、

  1 10 100 1000 10000 2 20 30 40 60 90 200 600 2000  6000

0 1 1  10  190  2890  1  2  3  4  6  9  30 110  490  1690
1 0 1  20  300  4000  1 12 13 14 16 19 140 220 1600  2800
2 0 1  20  300  4000  0  2 13 14 16 19  40 220  600  2800
3 0 1  20  300  4000  0  2  3 14 16 19  40 220  600  2800
4 0 1  20  300  4000  0  2  3  4 16 19  40 220  600  2800
5 0 1  20  300  4000  0  2  3  4 16 19  40 220  600  2800
6 0 1  20  300  4000  0  2  3  4  6 19  40 120  600  1800
7 0 1  20  300  4000  0  2  3  4  6 19  40 120  600  1800
8 0 1  20  300  4000  0  2  3  4  6 19  40 120  600  1800
9 0 1  20  300  4000  0  2  3  4  6  9  40 120  600  1800

編集:私の元の考えを片付ける:

0(含まれている)からpoweroTen(notinc)までの合計を示すブルートフォーステーブルから、tenpowerの主桁が次のように見えることがわかります。

increments tally[0 to 9] by md*tp*10^(tp-1)
increments tally[1 to md-1] by 10^tp
decrements tally[0] by (10^tp - 10) 
(to remove leading 0s if tp>leadingzeros)
can increment tally[moresignificantdigits] by self(md*10^tp) 
(to complete an effect)

これらの集計調整が各有効桁に適用された場合、集計は0からend-1までカウントされるように変更する必要があります。

調整を反転して、前の範囲(開始番号)を削除できます

テスト済みの完全な回答を提供してくれたAaronaughtに感謝します。

5
strainer

このような問題に対する明確な数学的解決策があります。値に最大桁数までゼロが埋め込まれていると仮定しましょう(そうではありませんが、後で補正します)。

  • 0から9までは、各桁が1回出現します
  • 0から99までの各桁は20回発生します(位置1で10倍、位置2で10倍)
  • 0-999から、各桁は300回発生します(P1で100x、P2で100x、P3で100x)

範囲が0から10の累乗である場合、特定の数字の明白なパターンはN * 10N-1、ここで[〜#〜] n [〜#〜]は10の累乗です。

範囲が10の累乗でない場合はどうなりますか?最小の10のパワーから始めて、次はワークアップします。処理する最も簡単なケースは399のような最大値です。100の倍数ごとに、各桁が少なくとも 20回発生することはわかっていますが、それがに表示される回数を補正する必要があります最上位桁の位置。0から3の数字では正確に100になり、他のすべての数字では正確にゼロになります。具体的には、追加する追加量は10です。N 関連する数字。

これを数式に入れると、上限が10の累乗の倍数よりも1小さい場合(つまり、399、6999など)、次のようになります。M * N * 10N-1 + iif(d <= M、10N、0)

これで、残りの部分(これを[〜#〜] r [〜#〜]と呼びます)を処理する必要があります。例として445を取り上げます。これは、399の結果に加えて、400〜445の範囲の結果です。この範囲では、MSDが発生します[〜#〜] r [〜#〜]より多くの回数であり、すべての桁(MSDを含む)も、範囲[0-- [〜#〜] r [〜#〜]]。

ここで、先行ゼロを補正する必要があります。このパターンは簡単です-それはただ:

10N + 10N-1 + 10N-2 + ... + ** 10

pdate:このバージョンでは、「パディングゼロ」が正しく考慮されます。つまり、残りを処理するときに中間位置のゼロ([4 0、4 1、4 2、...])。パディングゼロを計算するのは少し見苦しいですが、改訂されたコード(Cスタイルの疑似コード)がそれを処理します。

function countdigits(int d, int low, int high) {
    return countdigits(d, low, high, false);
}

function countdigits(int d, int low, int high, bool inner) {
    if (high == 0)
        return (d == 0) ? 1 : 0;

    if (low > 0)
        return countdigits(d, 0, high) - countdigits(d, 0, low);

    int n = floor(log10(high));
    int m = floor((high + 1) / pow(10, n));
    int r = high - m * pow(10, n);
    return
        (max(m, 1) * n * pow(10, n-1)) +                             // (1)
        ((d < m) ? pow(10, n) : 0) +                                 // (2)
        (((r >= 0) && (n > 0)) ? countdigits(d, 0, r, true) : 0) +   // (3)
        (((r >= 0) && (d == m)) ? (r + 1) : 0) +                     // (4)
        (((r >= 0) && (d == 0)) ? countpaddingzeros(n, r) : 0) -     // (5)
        (((d == 0) && !inner) ? countleadingzeros(n) : 0);           // (6)
}

function countleadingzeros(int n) {
      int tmp= 0;
      do{
         tmp= pow(10, n)+tmp;
         --n;
         }while(n>0);
         return tmp;
         }

function countpaddingzeros(int n, int r) {
    return (r + 1) * max(0, n - max(0, floor(log10(r))) - 1);
}

ご覧のように、少し見栄えが悪くなりますが、O(log n)時間で実行されるため、数十億の数値を処理する必要がある場合でも、すぐに結果が得られます。 :-)そして、もしそれを[0-1000000]の範囲で実行すると、ハイパフォーマンスマークによって投稿されたものとまったく同じ分布が得られるので、私はそれが正しいとほぼ確信しています。

参考までに、inner変数の理由は、先行ゼロ関数がすでに再帰的であるため、countdigitsの最初の実行でのみカウントできるためです。

pdate 2:コードが読みにくい場合のために、countdigits returnステートメントの各行が何を意味するかのリファレンスを以下に示します(インラインコメントを試しましたが、コードをさらに難しくしました読んだ):

  1. 最高10の累乗までの任意の数字の頻度(0〜99など)
  2. 最高の10の累乗の倍数を超えるMSDの周波数(100-399)
  3. 残りの桁の頻度(400-445、R = 45)
  4. 残りのMSDの追加頻度
  5. 残りの範囲(404、405 ...)の中間位置にあるゼロをカウントします
  6. 先行ゼロを1回だけ減算します(最も外側のループ上)
10
Aaronaught

私は、数値が範囲内にあり、開始番号と終了番号があるソリューションを必要としていると想定しています。開始番号から始めて、終了番号に達するまで数えることを想像してみてください-うまくいくでしょうが、遅くなるでしょう。高速アルゴリズムの秘訣は、10 ^ xの場所で1桁上に移動し、他のすべてを同じに保つために、その前にすべての桁を使用する必要があることを理解することです。 -9 10 ^(x-1)回。 (あなたのカウントがx桁を超えた桁上げを含んでいた可能性があることを除いて-私はこれを以下で修正します。)

ここに例があります。あなたが523から1004まで数えているとしましょう。

  • 最初に、523から524まで数えます。これは、数字5、2、および4をそれぞれ1回使用します。
  • 次に、524から604まで数えます。右端の数字はすべての数字を6サイクル循環するため、各数字のコピーが6つ必要です。 2番目の数字は、2から0までの数字をそれぞれ10回通過します。 3桁目は6 5回と5 100-24回です。
  • 3番目に、604から1004までカウントします。右端の数字は40サイクルなので、各数字のコピーを40ずつ追加します。右から2番目は4サイクル実行するため、各数字のコピーを4つ追加します。左端の数字は、7、8、9のそれぞれに100を加え、さらに5の0と100-5の6を行います。最後の数字は1 5回です。

最後のビットを高速化するには、右端の2か所の部分を見てください。各桁を10 + 1回使用します。一般に、1 + 10 + ... + 10 ^ n =(10 ^(n + 1)-1)/ 9です。これを使用すると、さらにカウントを高速化できます。

私のアルゴリズムは、(base-10カウントを使用して)開始番号から終了番号までカウントアップすることですが、上記の事実を使用してすばやく実行します。開始番号の数字を最下位から最上位まで反復し、各場所でカウントアップして、その数字が終了番号の数字と同じになるようにします。各ポイントで、nはキャリーに到達する前に行う必要があるアップカウントの数、mは後でキャリーする必要がある数です。

次に、疑似コードが言語としてカウントされると仮定します。ここで、私は何をしますか:

開始番号と終了番号を数字配列start []とend [] 
に変換します。10個の要素を持つ配列counts []を作成します。必要
 
開始番号を右から左に繰り返します。 i桁目、
 dを、この桁数
から最後の番号のi桁目までに数える必要がある桁数とします。 (つまり、同等の
桁mod 10を減算します)
 countの各エントリにd *(10 ^ i-1)/ 9を追加します。
 mをすべての数値にしますこの数字の右側の数字、
 nは10 ^ i-m。
です。開始数字の左側から
 i-までの各数字e th桁、その桁のカウントにnを追加します。
は、j in 1からd 
まで、i桁を1ずつインクリメントします。開始番号の左から
までのi番目の数字、開始番号の左側から最大までの各桁eのその桁のカウントに10 ^ iを追加します
 
 i番目の数字を含めて、その数字のカウントにmを追加します。
開始番号のi番目の数字を末尾のi番目の数字に設定します
番号。

ああ、そしてiの値は毎回1ずつ増加するので、毎回べき乗するのではなく、古い10 ^ iを追跡し、10を掛けて新しい値を取得するだけです。

8
Noah Lavine

これは非常に悪い答えです。投稿するのは恥ずかしいです。 Mathematicaに、1から1,000,000までのすべての数字で使用される数字を数えるように依頼しました。ここに私が得たものがあります:

0   488895
1   600001
2   600000
3   600000
4   600000
5   600000
6   600000
7   600000
8   600000
9   600000

次回、ハードウェアストアで販売するスティッキーディジットを注文するときは、これらの比率で注文すれば、間違いはありません。

私は Math Overflowでこの質問をしました 、そしてそのような単純な質問をしているとたたかれました。ユーザーの1人が私を同情し、私がそれを The Art of Problem Solving に投稿した場合、彼はそれに答えると言った。そうした。

ここに彼が投稿した答えがあります:
http://www.artofproblemsolving.com/Forum/viewtopic.php?p=1741600#17416

恥ずかしいことに、私の数学は彼が投稿したものを理解するには不十分です(その人は19歳です...それはとても憂鬱です)。私は本当にいくつかの数学のクラスを取る必要があります。

明るい面では、方程式は再帰的であるため、数学を理解している人が数行のコードで再帰関数に変換するのは簡単なことです。

5
Robert Harvey

あなたのアプローチは結構です。なぜあなたがあなたが説明したものよりも速く何かを必要とするのかは分かりません。

または、これは瞬時に解決策を提供します。実際に必要になる前に、必要なものを1から最大数まで計算します。各ステップで必要な数を保存できます。 2番目の例のような範囲がある場合、1〜300に必要なものから1〜50に必要なものを差し引いたものになります。

これで、自由に呼び出すことができるルックアップテーブルが作成されました。 10,000まで実行すると、数MBしかかかりませんが、計算には数分かかりますか?

3
John

私はこの質問に受け入れられた答えがあることを知っていますが、私は就職の面接のためにこのコードを書くことを任されました。私は高速で、ループを必要とせず、必要に応じて先行ゼロを使用または破棄できる別のソリューションを考え出したと思います。

それは実際には非常に単純ですが説明するのは簡単ではありません。

最初のn個の数値をリストする場合

     1
     2
     3

     .
     .
     .


     9
    10
    11

通常、左から右の方法で開始部屋番号から終了部屋番号までに必要な数字のカウントを開始するため、上記の場合、1 1、1 2、1 3 ... 1 9、2 1 1ゼロ、4つの1など。私が見たほとんどのソリューションでは、このアプローチをいくつかの最適化とともに使用してスピードアップしました。

私がやったことは、数百、数十、および単位のように、縦に列を数えることでした。最高の部屋番号がわかっているので、1つの除算によって100の列にある各桁の数を計算し、次に10の列の数を再帰して計算します。その後、必要に応じて先行ゼロを減算できます。

Excelを使用して数値を書き出すが、数値の各桁に個別の列を使用すると、視覚化が容易になります。

     A    B    C
     -    -    -
     0    0    1  (assuming room numbers do not start at zero)
     0    0    2
     0    0    3
     .
     .
     .
     3    6    4
     3    6    5
     .
     .
     .

     6    6    9
     6    7    0
     6    7    1

     ^
     sum in columns not rows

したがって、最大の部屋番号が671である場合、100の列には100個のゼロが垂直に続き、次に100個のゼロが続き、最大で71個の6桁になります。これらがすべて先行していることがわかっているため、必要に応じて100個のゼロを無視してください。

次に、10まで再帰して同じ操作を実行します。10個のゼロに続いて10個の1などが6回繰り返され、最後の時間が2個の7になります。ここでも、最初の10個のゼロは無視されます。最後にもちろん、必要に応じて最初のゼロを無視して、単位を実行します。

したがって、ループはなく、すべて除算で計算されます。再帰を使用して、列を「上」に移動して、最大値(この場合は数百)に到達し、それから合計を戻します。

私はこれをC#で記述し、興味のある人がベンチマークのタイミングを実行していない場合はコードを投稿できますが、最大で10 ^ 18部屋までの値に対して本質的に瞬時です。

ここまたは他で言及されているこのアプローチを見つけることができなかったので、誰かに役立つかもしれないと思いました。

3
Simon Hawes

これは正確な質問には答えませんが、 ベンフォードの法則 に従って最初の桁の分布に注目するのは興味深いことです。たとえば、一連の数値をランダムに選択した場合、それらの30%は「1」から始まりますが、これは直観に反しています。

後続の数字を表す分布は知りませんが、これを経験的に決定して、近似を計算するための簡単な式を考え出すことができるかもしれません数値の範囲に必要な桁数。

1
Alex Reisner

多くの反復で生の速度が必要な場合は、ルックアップテーブルを試してください。

  1. 2次元の配列を作成:10 x max-house-number

    int nDigits[10000][10] ;   // Don't try this on the stack, kids!
  1. 各行に、ゼロからその数に到達するために必要な桁数を入力します。
    ヒント:前の行を最初に使用します:

    n=0..9999:
       if (n>0) nDigits[n] = nDigits[n-1]
       d=0..9:
           nDigits[n][d] += countOccurrencesOf(n,d)   // 
  1. Number of digits "between" two numbers becomes simple subtraction.

       For range=51 to 300, take the counts for 300 and subtract the counts for 50.
       0's = nDigits[300][0] - nDigits[50][0]
       1's = nDigits[300][1] - nDigits[50][1]
       2's = nDigits[300][2] - nDigits[50][2]
       3's = nDigits[300][3] - nDigits[50][3]
       etc.
1
phord

「より良い」が「より明確」を意味する場合、私はそれを疑います。それが「より高速」を意味するのであれば、そうですが、私は説得力のある必要なしに、より明確なアルゴリズムの代わりに、より高速なアルゴリズムを使用しません。

#!/usr/bin/Ruby1.8

def digits_for_range(min, max, leading_zeros)
  bins = [0] * 10
  format = [
    '%',
    ('0' if leading_zeros),
    max.to_s.size,
    'd',
  ].compact.join
  (min..max).each do |i|
    s = format % i
    for digit in s.scan(/./)
      bins[digit.to_i] +=1  unless digit == ' '
    end
  end
  bins
end

p digits_for_range(1, 49, false) 
# => [4, 15, 15, 15, 15, 5, 5, 5, 5, 5]

p digits_for_range(1, 49, true)
# => [13, 15, 15, 15, 15, 5, 5, 5, 5, 5]

p digits_for_range(1, 10000, false)
# => [2893, 4001, 4000, 4000, 4000, 4000, 4000, 4000, 4000, 4000]

「犬が遅い」として知られている言語であるRuby 1.8は、上記のコードを0.135秒で実行します。これには、インタープリターのロードが含まれます。もっとスピードが必要でない限り、明白なアルゴリズムをあきらめないでください。

1
Wayne Conrad

各桁を分離し( 例はこちらを参照 )、0から9までのエントリを含むヒストグラムを作成し(数値に出現する桁数をカウントします)、 'numbers'の数を掛けます'尋ねた。

しかし、あなたが探しているものではない場合、より良い例を挙げられますか?

編集:

今、私は問題を抱えていると思います。あなたはこれを疑うことができると思います(疑似C):

int histogram[10];
memset(histogram, 0, sizeof(histogram));

for(i = startNumber; i <= endNumber; ++i)
{
    array = separateDigits(i);
    for(j = 0; k < array.length; ++j)
    {
        histogram[k]++;
    }
}

別個の数字は、リンクの機能を実装します。

ヒストグラムの各位置には、各桁の量があります。例えば

histogram[0] == total of zeros
histogram[1] == total of ones

...

よろしく

0
Andres