web-dev-qa-db-ja.com

特定のプロパティを持つ大きなAとBの間の整数をカウントする方法は?

プログラミングコンテストでは、多くのタスクで次のパターンが発生します。

巨大な(おそらく10進数で20桁以上の)数値AとBが与えられた場合、特定のプロパティPを持つA≤X≤Bの整数Xの数を決定します。

SPOJにはこれらのようなタスクがたくさんあります 練習用です。

興味深いプロパティの例は次のとおりです。

  • 「Xの桁合計は60」
  • 「Xは数字4と7のみで構成されます」
  • 「Xは回文構造」、つまり、Xの10進表現がその逆に等しいことを意味します(たとえば、X = 1234321)

f(Y)をそのような整数X≤Yの数と定義すると、質問に対する答えはf(B)-f(A-1)軽減された問題は、関数fを効率的に計算する方法です。場合によっては、特定の数学的プロパティを使用して数式を作成できますが、多くの場合、プロパティはより複雑であり、コンテストではそのための十分な時間がありません。

多くの場合に機能するより一般的なアプローチはありますか?また、特定のプロパティで数値を列挙したり、それらの集計を計算したりするために使用できますか?

これのバリエーションは、特定のプロパティを持つk番目の数を見つけることです。これは、もちろん、バイナリ検索とカウント関数を使用して解決できます。

50
Niklas B.

実際、非常に頻繁に機能することが判明したこのパターンへのアプローチがあります。また、指定されたプロパティを持つすべてのXを列挙するために使用することもできます。ただし、その数が適度に小さい場合に限ります。これを使用して、たとえば合計を見つけるために、指定されたプロパティを持つすべてのXの連想演算子を集約することもできます。

一般的な考え方を理解するために、XとYの 10進表現 に関してX≤Yの条件を定式化してみましょう。

X = xがあるとします1 バツ2 ... バツn-1 バツnおよびY = y1 y2 ... yn-1 yn、ここでxおよびyはXとYの10進数です。数字の長さが異なる場合は、短い数字の前に常にゼロを追加できます。

_leftmost_lo_を最小のixで定義します <y。 _leftmost_lo_をn + 1として定義します(そのようなiがない場合)。同様に、_leftmost_hi_を最小のixと定義します > y、またはn + 1.

現在、X≤Yは、_leftmost_lo <= leftmost_hi_である場合に正確です。この観察により、問題に 動的プログラミング アプローチを適用することが可能になり、Xの数字を次々に「設定」します。問題の例を挙げてこれを説明します。

プロパティX≤Yで整数Xの数f(Y)を計算し、Xの桁数は60

上記の定義に従って、nをYの桁数、_y[i]_をi-Yの10進数字とします。次の再帰アルゴリズムは問題を解決します。

_count(i, sum_so_far, leftmost_lo, leftmost_hi):
    if i == n + 1:
        # base case of the recursion, we have recursed beyond the last digit
        # now we check whether the number X we built is a valid solution
        if sum_so_far == 60 and leftmost_lo <= leftmost_hi:
            return 1
        else: 
            return 0
    result = 0
    # we need to decide which digit to use for x[i]
    for d := 0 to 9
        leftmost_lo' = leftmost_lo
        leftmost_hi' = leftmost_hi
        if d < y[i] and i < leftmost_lo': leftmost_lo' = i
        if d > y[i] and i < leftmost_hi': leftmost_hi' = i
        result += count(i + 1, sum_so_far + d, leftmost_lo', leftmost_hi')
    return result
_

これでf(Y) = count(1, 0, n + 1, n + 1)が得られ、問題は解決しました。関数に memoization を追加して、高速化することができます。ランタイムはO(n4この特定の実装用。実際、アイデアを巧みに最適化してO(n)にすることができます。これは読者への課題として残されています(ヒント:_leftmost_lo_および_leftmost_hi_に格納されている情報を1ビットに圧縮でき、_sum_so_far > 60_の場合はプルーニングできます)。解決策はこの投稿の最後にあります。

よく見ると、_sum_so_far_は、Xの数字列から値を計算する任意の関数の例に過ぎません。数字ごとに計算して出力できるany関数十分に小さい結果。それは、数字の積、特定の特性を満たす数字のセットのビットマスク、または他の多くのものである可能性があります。

また、数字が4と7だけで構成されているかどうかに応じて、1または0を返す関数にすることもできます。これにより、2番目の例を簡単に解決できます。 areの先頭にゼロを付けることが許可されているため、ここで少し注意する必要があります。したがって、ゼロを使用できるかどうかを示す再帰関数呼び出しで追加のビットを運ぶ必要があります。数字として。

プロパティX≤Yで整数Xの数f(Y)を計算し、Xは回文型である

これは少し厳しいです。先行ゼロに注意する必要があります。回文数のミラーポイントは、持っている先行ゼロの数に依存するため、先行ゼロの数を追跡する必要があります。

ただし、少し単純化するコツがあります。すべての数値XがYと同じ桁数でなければならないという追加の制限でf(Y)をカウントできる場合、元の同様に、可能なすべての桁数を反復処理し、結果を合計することにより、問題が発生します。

したがって、先行ゼロはまったくないと仮定できます。

_count(i, leftmost_lo, leftmost_hi):
    if i == ceil(n/2) + 1: # we stop after we have placed one half of the number
        if leftmost_lo <= leftmost_hi:
            return 1
        else: 
            return 0
    result = 0
    start = (i == 1) ? 1 : 0    # no leading zero, remember?
    for d := start to 9
        leftmost_lo' = leftmost_lo
        leftmost_hi' = leftmost_hi
        # digit n - i + 1 is the mirrored place of index i, so we place both at 
        # the same time here
        if d < y[i]     and i     < leftmost_lo': leftmost_lo' = i
        if d < y[n-i+1] and n-i+1 < leftmost_lo': leftmost_lo' = n-i+1
        if d > y[i]     and i     < leftmost_hi': leftmost_hi' = i
        if d > y[n-i+1] and n-i+1 < leftmost_hi': leftmost_hi' = n-i+1
        result += count(i + 1, leftmost_lo', leftmost_hi')
    return result
_

結果は再びf(Y) = count(1, n + 1, n + 1)になります。

UPDATE:数値をカウントするだけでなく、それらを列挙したり、グループ構造を公開しない集約関数を計算したりする場合は、再帰中にもXの下限を強制する必要があります。これにより、さらにいくつかのパラメーターが追加されます。

UPDATE 2:O(n)「digit sum 60」の例のソリューション:

このアプリケーションでは、数字を左から右に配置します。 _leftmost_lo < leftmost_hi_が真であるかどうかだけに関心があるので、新しいパラメーターloを追加しましょう。 loは、_leftmost_lo < i_の場合はtrue、そうでない場合はfalseです。 loがtrueの場合、iの位置に任意の数字を使用できます。 falseの場合、0〜Y [i]の数字のみを使用できます。これより大きな数字は_leftmost_hi = i < leftmost_lo_を引き起こし、解決につながらないためです。コード:

_def f(i, sum_so_far, lo):
    if i == n + 1: return sum_so_far == 60
    if sum_so_far > 60: return 0
    res = 0
    for d := 0 to (lo ? 9 : y[i]):
         res += f(i + 1, sum + d, lo || d < y[i])
    return res
_

おそらく、この見方は多少簡単ですが、_leftmost_lo_/_leftmost_hi_アプローチよりも少し明確ではありません。また、パリンドロームの問題のようなやや複雑なシナリオではすぐに動作しません(ただし、パリンドロームの問題も使用できます)。

69
Niklas B.