整数のラップアラウンドを「元に戻す」


20

数年前に興味深い理論上の問題に遭遇しました。私は解決策を見つけたことがなく、寝るときも悩み続けます。

xと呼ばれるintに数値を保持する(C#)アプリケーションがあるとします。(xの値は固定されていません)。プログラムが実行されると、xに33が掛けられ、ファイルに書き込まれます。

基本的なソースコードは次のようになります。

int x = getSomeInt();
x = x * 33;
file.WriteLine(x); // Writes x to the file in decimal format

数年後、Xの元の値が必要であることを発見します。一部の計算は簡単です。ファイル内の数値を33で除算するだけです。ただし、他の場合では、Xが十分に大きいため、乗算によって整数オーバーフローが発生します。ドキュメントによると、C#は数値が未満になるまで高位ビットを切り捨てint.MaxValueます。この場合、次のいずれかが可能ですか?

  1. X自体を回復するか
  2. Xの可能な値のリストを回復しますか?

単純な加算のケースが機能するため(基本的にXに10を加算してラップすると、10を減算してXで終了することができるので、一方または両方が可能になるはずです(私のロジックは確かに欠陥がある可能性があります) )そして、乗算は単純に繰り返される加算です。また、Xがすべての場合に同じ値-定数33で乗算されるという事実も役立ちます(私は信じています)。

これは何年も奇妙な瞬間に私の頭蓋骨の周りで踊っています。それは私に起こるでしょう、私はそれを熟考しようといくつかの時間を費やすでしょう、そして私はそれを数ヶ月間忘れます。この問題を追いかけるのはうんざりです!誰でも洞察を提供できますか?

(サイドノート:私は本当にこれにタグを付ける方法を知りません。提案を歓迎します。)

編集: Xの可能な値のリストを取得できる場合、元の値に絞り込むのに役立つ他のテストがあることを明確にしましょう。



1
@rwong:あなたのコメントが唯一の正しい答えです。
ケビンクライン14

うん、オイラーの方法は特に効果的と思われます。これは、の因数分解がm2 ^ 32または2 ^ 64であり、aモジュロのべき乗mが単純である(オーバーフローを無視するだけ)
MSalters 14

1
特定の問題は、実際にはRational Reconstruction
MSalters 14

1
@MSalters:あなたが持っているところだいいえ、r*s^-1 mod mあなたは両方を見つける必要があるrs。ここで、私たちはr*s mod mすべてを知っていrます。
user2357112はMonicaをサポートしています14

回答:


50

1041204193を掛けます。

乗算の結果がintに収まらない場合、正確な結果は得られませんが、2 ** 32を法とする正確な結果に相当する数値が得られます。つまり、乗算した数値が2 ** 32(それは奇数でなければならないことを意味する)に素である場合、その逆数乗算して数値を戻すことができます。Wolfram Alphaまたは拡張ユークリッドアルゴリズムは、33の2を法とする乗法逆モジュロ2 ** 32が1041204193であることを示します。したがって、1041204193を掛けると、元のxが戻ります。

たとえば33ではなく60だった場合、元の数を復元することはできませんが、いくつかの可能性に絞り込むことができます。60を4 * 15に因数分解し、15 mod 2 ** 32の逆数を計算し、それを乗算すると、元の数の4倍を回復でき、数の上位2ビットのみがブルートフォースになります。Wolfram Alphaは、逆関数に対して4008636143を提供します。これはintに収まりませんが、それでかまいません。4008636143 mod 2 ** 32に相当する数値を見つけるか、それともintに強制してコンパイラーに実行させると、結果も15 mod 2 ** 32の逆数になります。(-286331153を取得します。


5
ああ少年。そのため、私のコンピューターがマップの作成で行ったすべての作業は、すでにユークリッドによって行われました。
v010dya 14

21
私はあなたの最初の文章の事実の問題が好きです。「ああ、それはもちろん1041204193です。それを覚えていないのですか?」:-P-
ドアノブ

2
x * 33がオーバーフローしなかった場合とオーバーフローした場合のように、いくつかの数値に対してこの動作の例を示すと便利です。
ロブ・ワット

2
心が吹き飛ばされた。ワオ。
マイケルガゾンダ14

4
33モジュロ$ 2 ^ {32} $の逆行列を見つけるのに、ユークリッドもWolframAlphaも(確かに!)必要ありません。$ x = 32 = 2 ^ 5 $は$ 2 ^ 32 $を法とする無次元数(オーダー$ 7 $)であるため、幾何級数の恒等式$(1 + x)^ {-1} = 1-x + x ^を適用できます。 2-x ^ 3 + \ cdots + x ^ 6 $(その後、シリーズが途切れる)で$ 33 ^ {-1} = 1-2 ^ 5 + 2 ^ {10} -2 ^ {15} +の数を見つける\ cdots + 2 ^ {30} $は$ 111110000011111000001111100001_2 = 1041204193_ {10} $です。
マークヴァンレーウェン14

6

これはおそらく、Math(sic)SEへの質問として適しています。左端のビットをドロップすることも同じことなので、基本的にモジュラー演算を扱っています。

私は数学(sic)SEを使っている人ほど数学が得意ではありませんが、答えようとします。

ここにあるのは、数に33(3 * 11)を掛けて、MODとの唯一の共通分母が1であるということです。これは、コンピューターのビットが2のべき乗であるため、MODが2のべき乗。

以前の値ごとに次の値を計算するテーブルを作成できます。そして、質問は、次の番号が前の1つにのみ対応するようになります。

33ではなく、素数または素数の力であれば、答えはイエスだと思いますが、この場合は... Math.SEで質問してください。

プログラムテスト

私はC#を知らないので、これはC ++ですが、概念はまだ保持されます。これはあなたができることを示しているようです:

#include <iostream>
#include <map>

int main(void)
{
    unsigned short count = 0;
    unsigned short x = 0;
    std::map<unsigned short, unsigned short> nextprev;

    nextprev[0] = 0;
    while(++x) nextprev[x] = 0;

    unsigned short nextX;
    while(++x)
    {
            nextX = x*33;
            if(nextprev[nextX])
            {
                    std::cout << nextprev[nextX] << "*33==" << nextX << " && " << x << "*33==" << nextX << std::endl;
                    ++count;
            }
            else
            {
                    nextprev[nextX] = x;
                    //std::cout << x << "*33==" << nextX << std::endl;
            }
    }

    std::cout << count << " collisions found" << std::endl;

    return 0;
}

そのようなマップを作成した後、次のXを知っていれば、前のXをいつでも取得できます。常に単一の値のみがあります。


負ではないデータ型を使用する方が簡単なのはなぜですか?署名されたものと署名されていないものはコンピューターで同じように扱われたのではなく、人間の出力形式のみが異なりますか?
Xcelled 14

@ Xcelled194さて、これらの数値について考えるのは簡単です。
v010dya 14

十分に十分なxDヒューマンファクター
Xcelled 14

私はそれをより明確にするために、否定的でないことに関するその声明を削除しました。
v010dya 14

1
@ Xcelled194:符号なしデータ型は、モジュラー演算の通常の規則に従います。署名された型はそうではありません。特に、maxval+1符号なしの型の場合のみ0です。
MSalters

2

それを取得する1つの方法は、ブルートフォースを使用することです。申し訳ありませんが、C#はわかりませんが、以下はソリューションを説明するためのCのような擬似コードです。

for (x=0; x<=INT_MAX; x++) {
    if (x*33 == test_value) {
        printf("%d\n", x);
    }
}

技術的には、必要なのは、言語が任意の精度の整数(bigint)を使用しない限りx*33%(INT_MAX+1) == test_value、整数オーバーフローが自動的に%操作を行うことです。

これがあなたに与えるものは、元の数字であったかもしれない一連の数字です。印刷される最初の番号は、1ラウンドのオーバーフローを生成する番号です。2番目の数値は、2ラウンドのオーバーフローを生成する数値です。等々..

したがって、データがより適切であることがわかっている場合は、より適切な推測を行うことができます。たとえば、一般的な時計の計算(12時ごとのオーバーフロー)は、ほとんどの人が今日起こったことに関心があるため、最初の数字をより高くする傾向があります。


C#は基本型ではCと同じように動作します。つまりint、ラップする4バイトの符号付き整数です。したがって、入力が多い場合、ブルートフォーシングは最善の方法ではありませんが、答えは依然として良好です。:)
Xcelled 14

ええ、私はここからモジュロ代数ルールを使って紙の上でそれをやってみました:math.stackexchange.com/questions/346271/…。しかし、私はそれを理解しようとして立ち往生し、ブルートフォースの解決策になりました:)
slebetman 14

興味深い記事ですが、クリックするためにはもう少し詳しく調べる必要があると思います。
Xcelled

@slebetman私のコードを見てください。33を乗算することになると、1つの答えしかないようです。
v010dya14年

2
修正:C intはラップアラウンドを保証されていません(コンパイラのドキュメントを参照してください)。ただし、符号なしの型には当てはまります。
トーマスエディング14

1

SMTソルバーZ3は、式に満足のいく割り当てを与えるように依頼できますx * 33 = valueFromFile。それはあなたのためにその方程式を逆にし、あなたにすべての可能な値を与えますx。Z3は、乗算を含む正確なビットベクトル演算をサポートしています。

    public static void InvertMultiplication()
    {
        int multiplicationResult = new Random().Next();
        int knownFactor = 33;

        using (var context = new Context(new Dictionary<string, string>() { { "MODEL", "true" } }))
        {
            uint bitvectorSize = 32;
            var xExpr = context.MkBVConst("x", bitvectorSize);
            var yExpr = context.MkBVConst("y", bitvectorSize);
            var mulExpr = context.MkBVMul(xExpr, yExpr);
            var eqResultExpr = context.MkEq(mulExpr, context.MkBV(multiplicationResult, bitvectorSize));
            var eqXExpr = context.MkEq(xExpr, context.MkBV(knownFactor, bitvectorSize));

            var solver = context.MkSimpleSolver();
            solver.Assert(eqResultExpr);
            solver.Assert(eqXExpr);

            var status = solver.Check();
            Console.WriteLine(status);
            if (status == Status.SATISFIABLE)
            {
                Console.WriteLine(solver.Model);
                Console.WriteLine("{0} * {1} = {2}", solver.Model.Eval(xExpr), solver.Model.Eval(yExpr), solver.Model.Eval(mulExpr));
            }
        }
    }

出力は次のようになります。

SATISFIABLE
(define-fun y () (_ BitVec 32)
  #xa33fec22)
(define-fun x () (_ BitVec 32)
  #x00000021)
33 * 2738875426 = 188575842

0

その結果を取り消すと、ゼロ以外の有限量の数値(通常は無限ですがint、ℤの有限サブセット)が得られます。これが許容範囲内であれば、数値を生成するだけです(他の回答を参照)。

それ以外の場合は、変数の履歴の履歴リスト(有限または無限の長さ)を維持する必要があります。


0

いつものように、科学者による解決策とエンジニアによる解決策があります。

上に、科学者からの非常に良い解決策があります。これは常に機能しますが、「乗法逆数」を計算する必要があります。

エンジニアからの簡単なソリューションを次に示します。これにより、考えられるすべての整数を試す必要がなくなります。

val multiplier = 33 //used with 0x23456789
val problemAsLong = (-1947051863).toLong & 0xFFFFFFFFL

val overflowBit = 0x100000000L
for(test <- 0 until multiplier) {
  if((problemAsLong + overflowBit * test) % multiplier == 0) {
    val originalLong = (problemAsLong + overflowBit * test) / multiplier
    val original = originalLong.toInt
    println(s"$original (test = $test)")
  }
}

アイデアは何ですか?

  1. オーバーフローしたので、大きなタイプを使用して回復します(Int -> Long
  2. おそらくオーバーフローのためにいくつかのビットを失ったので、それらを回復しましょう
  3. オーバーフローは Int.MaxValue * multiplier

完全な実行可能コードはhttp://ideone.com/zVMbGVにあります

詳細:

  • val problemAsLong = (-1947051863).toLong & 0xFFFFFFFFL
    ここでは、保存された数値をLongに変換しますが、IntとLongは署名されているため、正しく処理する必要があります。
    したがって、ビット単位のANDとビット単位のIntを使用して数を制限します。
  • val overflowBit = 0x100000000L
    このビットまたはその乗算は、最初の乗算によって失われる可能性があります。
    Int範囲外の最初のビットです。
  • for(test <- 0 until multiplier)
    3番目のアイデアによると、最大オーバーフローは乗数によって制限されるため、実際に必要な以上のことはしないでください。
  • if((problemAsLong + overflowBit * test) % multiplier == 0)
    失われた可能性のあるオーバーフローを追加することで解決策が得られるかどうかを確認します
  • val original = originalLong.toInt
    元の問題はInt範囲にあったので、それに戻りましょう。そうしないと、負の数を誤って回復する可能性があります。
  • println(s"$original (test = $test)")
    他の可能な解決策があるかもしれないので、最初の解決策の後に壊れないでください。

PS: 3番目のアイデアは厳密には正しくありませんが、理解しやすいように残しておきます。
Int.MaxValueはですが0x7FFFFFFF、最大オーバーフローは0xFFFFFFFF * multiplierです。
したがって、正しいテキストは「オーバーフローは-1 * multiplier」を超えていません。
これは正しいですが、誰もがそれを理解するわけではありません。

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.