古くはビル・ゲイツの試験、最近だと #tcfm で 話題になった「マイナス2進数」が興味深い。

導入

Answer 33 Count in base negative 2.

...

In general, any base works like a set of building blocks of differnt sizes. In base 10, the blocks come in sizes 1, 10, 100, 1,000, etc. In base 2, the blocks come in sizes 1, 2, 4, 8, 16, etc. Combining units in these standard sizes creates any desired number.

So what would base -2 notation be?

...

"How Would You Move Mount Fuji?: Microsoft's Cult of the Puzzle - How the World's Smartest Companies Select the Most Creative Thinkers"

2進数は、一番右のビット(LSB)を20、その次が21、その次が22… と表現することだが、ここでのマイナス2進数とは「メカニカルに」2→-2に置き換えたものだ。 すなわち、一番右のビット(LSB)を(-2)0、その次が(-2)1、その次が(-2)2… と表現することだ。

マイナス2進数のLSBから0番目として桁を振っていったとしてマイナス2進数の各桁を n k 0 , 1 0 k L - 1 すなわち数値を n L - 1 n L - 2 n L - 3 ... n 1 n 0 と表すとすると、10進数への変換式は

k = 0 L - 1 n k ( - 2 ) k

ちなみに4桁のマイナス2進数と10進数の関係は下記のようになる。

count base negative 2 in 4-bits
decimalbase negative 2
-101010
-91011
-81000
-71001
-61110
-51111
-41100
-31101
-20010
-10011
00000
10001
20110
30111
40100
50101

これの逆変換式を求めたい。

補題:ビット幅で表現できる数値

手計算すると感覚的に下記の通りビット幅拡張によって表現できる範囲が、 正の方向と負の方向の交互に隙間なく広がることがわかる。

bit length vs range of "base negative 2"
bit length: L minimal MAXIMUM
1 0 1
2 -2 1
3 -2 5
4 -10 5

Lビット列のマイナス2進数で表現できる値の最大値と最小値は、 手続き的に書くとそれぞれ下記の通り。

fn mbin_positive_max(l: u32) -> i32 {
    let mut val = 0;
    for i in 0..l {
        if i % 2 == 0 {
            val = val + pow2u32(i) as i32;
        }
    }
    val
}

fn mbin_negative_min(l: u32) -> i32 {
    let mut val = 0;
    for i in 0..l {
        if i % 2 == 1 {
            val = val - pow2u32(i) as i32;
        }
    }
    val
}

#[inline]
fn pow2u32(exp: u32) -> u32 {
    1 << exp
}

なお、等比級数の和であるのだから数式としては

4 L 2 - 1 3 , - 2 4 L 2 - 1 3

注意すべき点としては、 Lが奇数ならば、Lビットのときの最大値はL+1ビットの最大値と同じで Lビットのときの最小値はL-1ビットの最小値と同じである。 また、Lが偶数ならば、Lビットのときの最大値はL-1ビットの最大値と同じで Lビットのときの最小値はL+1ビットの最小値と同じである。

実のところマイナス2進数では、上述の範囲内の数値は隙間なくすべて表現できる。 Lの数値が小さいときは手作業で確認できるとして、このルールが続くことを下記に示す。

Lが奇数のとき、L-1ビットからLビットに拡張したときに広がる数値の範囲の最小値は、 最も左のビット(MSB)が1となる数値の値に、L-1ビットのときの最小値(これはLビットのときの最小値と同じであるが)を 加算した値である。

2 L - 1 - 2 4 L - 1 2 - 1 3

これを解くと

... = 2 L - 1 + 2 3 = 2 L - 1 - 1 3 + 1 = 4 L - 1 2 - 1 3 + 1

であるから、L-1ビットのときの最大値(これはL-2ビットのときの最大値に同じ)に +1加算したものと等しい。

よって、Lが奇数のとき、L-1ビットのとき隙間なくすべて表現できるのならば、 Lビットのときも隙間なくすべて表現できる。

Lが偶数のとき(ただし0は除く)も同様に、L-1ビットからLビットに拡張したときに広がる数値の範囲の最大値は、

- 2 L - 1 + 4 L 2 - 1 3 = ... = - 2 4 L 2 - 1 3 - 1

であり、L-1ビットのときの最小値(これはLビットのときの最小値に同じ)に -1したものと等しい。

よって、Lが偶数のとき、L-1ビットのとき隙間なくすべて表現できるのならば、 Lビットのときも隙間なくすべて表現できる。

したがってLが偶数であっても奇数であっても、帰納的に隙間なく表現できる、 すなわちビット幅拡張によって表現できる範囲が隙間なく広がっていくことを示せた。

10進数→マイナス2進数変換アルゴリズム

既述の補題の考察より、(有界な数値であるならば、) MSB側からビットが立つか判定していけば良いことがわかる。

  1. r=10進数の値
  2. k=L-1,L-2,...,1,0について繰り返し:
    1. kビットからk+1ビットで拡張される値の範囲内に属するならば、kビット目の値mk=1。そうでなければ0
    2. rからmk-2kを引く
  3. おわり。変換された値は n L - 1 n L - 2 n L - 3 ... n 1 n 0

オンライン変換サンプル(powered by WebAssembly)

仕様:オーバーフローを考慮していないので絶対値の大きな値だとエラーが出たり 不正な値がでる。やろうと思えばなんとか対処できそうだが本題ではないので 省いた。


ソースは rust で書いた後、WebAssembly に変換している。 変換前のソースコードは gist:cat-in-136/d0efb4879290b09c8cebb358632d8749 においてある。 test code も書いてあり(WebAssembly からは取り除かれている)、 こちらは official release (WebAssembly 非対応) の rust でも動作する。

なお WebAssembly の仕様上、文字列の引き渡しに問題があるので整数(u32)を 単なる bitstring を表現するものとして扱ってやり取りしている。

10進法→マイナス2進法の変換の考察

  • 確かに数式としては数字の表現方法としては成り立つ
  • でもむちゃくちゃ計算とかがやりづらい

参考文献