私自身が表題の問題を解いた時のプログラムについて解説します。
問題の詳細は「単調増加連数」(CodeIQ)を参照してください。
【概要】
正の整数を 2進数で表現したときに、1や0の続く長さがどんどん増えていく数を「単調増加連数」と呼びます。
例えば以下のとおりです:
値(10進数表示) 値(2進数表示) 1や0の続く長さ 単調増加連数? 7 111 3 TRUE 10 1010 1, 1, 1, 1 false 77 1001101 1, 2, 2, 1, 1 false 79 1001111 1, 2, 4 TRUE 240 11110000 4, 4 false 399 110001111 2, 3, 4 TRUE 1136 10001110000 1, 3, 3, 4 false 3975 111110000111 5, 4, 3 false 293888 1000111110000000000 1, 3, 5, 10 TRUE
入力された値よりも大きな「単調増加連数」で、最も小さいものを出力してください。
【入出力】
入力は
77
のようになっています。
普通に 10進数です。
出力も普通に10進数です。
77 より大きな「単調増加連数」として最も小さい値を出力すれば良いので、77に対応する出力は
79
です。
【例】
入力 入力の2進数表示 出力 出力の2進数表示 出力の1や0の続く長さ 77 1001101 79 1001111 1, 2, 4 79 1001111 96 1100000 2, 5 57 111001 63 111111 6
【補足】
不正な入力に対処する必要はありません。
入力は 1 以上 百億以下です。
「単調増加連数」は、この問題のために作った造語です。
Rubyで解答しています。
#!/usr/bin/ruby def part(nums) ret = [nums] arr = nums.dup n = arr.pop if n % 2 == 1 then h = n/2 else h = n/2-1 end min = arr.last != nil ? arr.last+1 : 1 if h >= min then for left in min..h right = n - left nxt = arr + [left, right] ret += part(nxt) end end return ret end def toNum(arr) str = "0b" for i in 0...arr.size v = (i+1) % 2 str += v.to_s * arr[i] end return str.to_i(2) end def solve(n) sz = n.to_s(2).size ps = part([sz]) f = ps.shift ps.push(f) ret = (n == 1) ? 3 : toNum([1,sz]) for arr in ps cand = toNum(arr) if cand <= n then next elsif cand < ret then ret = cand end end return ret end # main while line = gets line.strip! next if line.empty? p solve(line.to_i) end
結構難しいです。
入力値の最大値が大きいので単純にやるとできないと思います。
入力値を数値にしてsolve()に渡します。
結果が配列で帰るのでjoin()して印字します。
問題を解きます。
36行目で入力値のビット長を求めます。ビット長は2進数の文字列表現の場合の文字列の長さです。
37行目で全て異なる数で構成される分割数のリストを生成します。このリストはリストの分割数を単純増加連数にした場合、最初の要素を除いて小さい値から大きい値になるリストになっています。最初の要素は単純増加連数にした場合、最大の数なので38〜39行目でリストの最後に移動しています。
41行目のretは返却値で初期値は0b1000...の様な最初だけが1、それ以降が0の数です。これは入力値が0b111...の様な1だけで構成される数の場合の答えで、入力値が0b111...の様な数の場合、psのリスト内に答えがない時の答えになります。
43〜48行目で入力値の次の単調増加連数を探します。
toNum()は[1,2,3]の様な配列を数値([1,2,3]の場合は39(0b100111))にします。
これがnより大きく、retより小さければretを置き換えます。
最終的にループを終えた時点のretが答えになります。
この関数はnumsの値を全て異なる自然数に分割した場合のパターンを列挙して返します。
引数numsは[1,2,3]の様な分割途中の数列で、最初の呼び出し時は[6]の様に分割したい数を1つだけ要素にもつ配列です。
retは返却値で[nums]は分割数の一つなのでretに含めます(4行目)。
arrは分割操作を行う数列で、初期値はnumsのコピー([入力値])です。
arrから最後の値をpop()してnに取ります。
8〜10はこの操作の分割数の最大値hを計算しており、nが奇数ならn/2、偶数ならn/2-1になります。
12行目はこの操作での分割の最小数で、arrに数字があればその最後の数+1、なければ1に設定します。
13行目のif文はh(分割の最大値)がmin(分割の最小値)より大きければmin〜hの間の数で分割できるので、そのチェックをしています。
14〜19行目のループでmin〜hまでの数で1回の分割を行います。
leftはある数を2つに分割した時の小さい方の値で、例えば7を[3,4]に分けた時の3がleftになります。
15行目のrightはn-leftなのである数を2つに分割した時の残り、例えば7を[3,4]に分割した時の4になります。
nxtは1回の分割を行った結果の数列でarrに[left, right]を付け加えたものになります。
ここまでの操作を具体的に示すと8を分割する場合だと次の様になります。
arr = [8]
n = 8, arr = []
h = 3
min = 1
1〜3で分割を実施。
[]+[1,7]
[]+[2,6]
[]+[3,5]
18行目でこの様にしてできた結果に対し、partを再起的に呼び出すことで末尾の数をさらに分割します。そしてそれ以上分割できなくなった結果をretに順次追加することでリストを得ます。
arrの配列の値に従ってビット列を生成して数値にします。
arrは[1,2,3]の様な単調増加連数の1と0の連続数を大きな位の方から列挙したものです。
配列の要素番号が偶数の時は1、奇数の時は0として文字列にします。
文字列を2進数数値に変換して返します。
この問題、私が最初の正解者でした(バッジの獲得者を見たら1人でした)。
ビット長の分割数というアイデアを比較的早く思いついたことと、分割数列挙のプログラムを持っていた(昔に作ってあった)のでそのまま流用できたのが大きいですが。
分割数の問題は時々出題されますが、検索しても分割数列挙のプログラムはあんまり見つからないし、自分で考えると結構難しいです。