“撤消”整数环绕


20

几年前,我遇到了一个有趣的理论问题。我从未找到解决方案,当我睡觉时,它仍然困扰着我。

假设您有一个(C#)应用程序,该应用程序在一个称为x的int中保存一些数字。(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:您的评论是唯一正确的答案。
凯文·克莱恩

是的,Euler的方法似乎特别有效,因为的因式分解m只有2 ^ 32或2 ^ 64,加上a模的幂m很简单(只是忽略那里的溢出)
MSalters 2014年

1
我认为特定的问题实际上是理性重构
MSalters 2014年

1
@MSalters:不,那是您的位置r*s^-1 mod m,您需要同时找到rs。在这里,我们拥有r*s mod m并且除了之外什么都不知道r
user2357112支持Monica 2014年

Answers:


50

乘以1041204193。

当乘法的结果不适合int时,您将不会获得确切的结果,但是您将得到一个与精确结果取模2 ** 32相等的数字。这意味着,如果您乘以的是2 ** 32的互质数这只意味着它必须是奇数),则可以乘以其乘积逆数来取回数字。Wolfram Alpha扩展的Euclidean算法可以告诉我们33的乘法逆模2 ** 32为1041204193。因此,乘以1041204193,就可以得到原始的x返回。

如果我们拥有60个而不是33个,则将无法恢复原始数字,但可以将其缩小到几种可能性。通过将60乘以4 * 15,计算15 mod 2 ** 32的逆,然后乘以,我们可以恢复原始数的4倍,而仅剩下数字的2个高阶位供蛮力使用。Wolfram Alpha为我们提供了4008636143的逆函数,它不适合int,但这没关系。我们只是找到一个等于4008636143 mod 2 ** 32的数字,或者无论如何都将其强制为int以使编译器为我们完成此操作,结果也将是15 mod 2 ** 32的倒数。(我们得到-286331153。


5
好家伙。因此,我的计算机在构建地图上所做的所有工作已经由Euclid完成。
v010dya 2014年

21
我喜欢你第一句话的事实。“哦,当然是1041204193。你没有记住吗?” :-P
门把手2014年

2
展示一个适用于几个数字的示例将很有帮助,例如一个x * 33没有溢出而另一个在x * 33发生溢出。
罗伯·瓦茨

2
精神震撼。哇。
Michael Gazonda 2014年

4
您不需要Euclid或WolframAlpha(当然!)来查找33模$ 2 ^ {32} $的逆数。由于$ x = 32 = 2 ^ 5 $是幂等的(顺序为$ 7 $)模为$ 2 ^ 32 $,因此您只需应用几何级数$(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} $。
Marc van Leeuwen 2014年

6

这可能更适合作为Math(sic)SE的问题。基本上,您正在处理模块化算术,因为删除最左边的位是同一回事。

我在数学方面的能力不如在数学(sic)SE上工作的人,但是我会尽力回答。

我们这里得到的是该数字乘以33(3 * 11),并且它与mod的唯一共同点是1。这是因为根据定义,计算机中的位是2的幂,因此您的mod是有两个的力量。

您将能够构造表,其中对于每个先前的值,您都将计算以下的值。问题是,以下数字是否仅对应于前一个数字。

如果不是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。始终只有一个值。


为什么使用非负数据类型会更容易?在计算机中签名和未签名的处理方式不是相同的,只是它们的人工输出格式不同?
Xcelled 2014年

@ Xcelled194好吧,对我来说这些数字更容易些。
v010dya

足够xD人为因素
〜– Xcelled

我删除了关于非负数的陈述,以使其更加明显。
v010dya 2014年

1
@ Xcelled194:无符号数据类型遵循模块化算术的常规规则;签名类型没有。特别是,maxval+1仅对于无符号类型为0。
MSalters 2014年

2

获得它的一种方法是使用蛮力。抱歉,我不了解C#,但以下是类似于C的伪代码来说明解决方案:

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

从技术上讲,您需要的是x*33%(INT_MAX+1) == test_value但整数溢出将自动%为您执行该操作,除非您的语言使用任意精度的整数(bigint)。

这给了您一系列可能是原始数字的数字。打印的第一个数字将是将产生一轮溢出的数字。第二个数字是将产生两次溢出的数字。等等..

因此,如果您知道自己的数据更好,则可以做出更好的猜测。例如,由于大多数人都对今天发生的事情感兴趣,因此常见的时钟数学(每12点溢出)往往使第一个数字更可能出现。


C#的行为类似于具有基本类型的C-即int包装的4字节带符号整数,因此您的答案还是不错的,尽管如果输入很多,强行强制并不是最好的选择!:)
Xcelled 2014年

是的,我尝试通过纸上的模代数规则在纸上进行操作:math.stackexchange.com/questions/346271/…。但是我陷入困境,试图解决这个问题,并最终提出了蛮力解决方案:)
slebetman 2014年

有趣的是,我认为,尽管我必须对其进行深入研究才能使其点击。
Xcelled 2014年

@slebetman看看我的代码。似乎只有一个答案,当它涉及到由33乘以
v010dya

2
更正:int不保证C 可以环绕(请参见编译器的文档)。但是对于无符号类型,这是正确的。
Thomas Eding

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是带符号的,因此我们必须正确地执行此操作。
    因此,我们使用按位与与Int位进行限制。
  • val overflowBit = 0x100000000L
    此位或它的乘法可能会因初始乘法而丢失。
    这是Int范围之外的第一位。
  • for(test <- 0 until multiplier)
    根据第三个想法,最大溢出受到乘数的限制,所以不要尝试超出我们真正需要的范围。
  • if((problemAsLong + overflowBit * test) % multiplier == 0)
    检查是否通过添加可能丢失的溢出来解决问题
  • val original = originalLong.toInt
    最初的问题在Int范围内,所以让我们回到它。否则,我们可能会错误地恢复负数。
  • println(s"$original (test = $test)")
    在第一个解决方案之后不要中断,因为可能还有其他可能的解决方案。

附言:第三个主意并非严格正确,但可以理解。
Int.MaxValue0x7FFFFFFF,但最大溢出为0xFFFFFFFF * multiplier
因此正确的文本应为“溢出不超过-1 * multiplier”。
这是正确的,但不是每个人都会理解。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.