我在上次面试中遇到的一个问题是:

设计函数f,使得:f(f(n))==-n其中n是32位有符号整数;你不能用复数算术。如果你不能为整个数字范围设计这样的函数,那么就尽可能为最大范围设计它。

有什么想法吗?


当前回答

很简单,只需让f返回看起来等于任何整数的值,并且可以从整数转换。

public class Agreeable
{
    public static bool operator==(Agreeable c, int n)
        { return true; }

    public static bool operator!=(Agreeable c, int n)
        { return false; }

    public static implicit operator Agreeable(int n)
        { return new Agreeable(); }
}

class Program
{
    public static Agreeable f(Agreeable c)
        { return c; }

    static void Main(string[] args)
    {
        Debug.Assert(f(f(0)) == 0);
        Debug.Assert(f(f(5)) == -5);
        Debug.Assert(f(f(-5)) == 5);
        Debug.Assert(f(f(int.MaxValue)) == -int.MaxValue);
    }
}

其他回答

下面是一个简短的Python答案:

def f(n):
  m = -n if n % 2 == 0 else n
  return m + sign(n)

一般情况

稍微调整一下上面的内容就可以处理我们希望k个自调用否定输入的情况——例如,如果k=3,这意味着g(g(g)n))=-n:

def g(n):
  if n % k: return n + sign(n)
  return -n + (k - 1) * sign(n)

这是通过将0保留在适当位置并创建长度为2*k的循环来实现的,因此,在任何循环中,n和-n之间的距离为k。具体来说,每个周期如下:

N * k + 1, N * k + 2, ... , N * k + (k - 1), - N * k - 1, ... , - N * k - (k - 1)

或者,为了更容易理解,这里是k=3的示例循环:

1, 2, 3, -1, -2, -3
4, 5, 6, -4, -5, -6

这组循环最大化了在任何以零为中心的机器类型(如有符号int32或有符号int64类型)内工作的输入范围。

兼容范围分析

映射x->f(x)实际上必须形成长度为2*k的循环,其中x=0是特殊情况下的1-长度循环,因为-0=0。因此,一般k的问题是可解的,当且仅当输入-1(补偿0)的范围是2*k的倍数,并且正负范围是相反的。

对于有符号整数表示,我们总是有一个最小的负数,在该范围内没有正的对应项,因此该问题在整个范围内变得不可解决。例如,有符号字符的范围为[-128127],因此在给定范围内f(f(-128))=128是不可能的。

这是rossfabricant答案的C实现。注意,由于我始终使用32位整数,f(f(2147483647))==2147483648,而不是-2147483647。

int32_t f( int32_t n )
{
    if( n == 0 ) return 0;
    switch( n & 0x80000001 ) {
        case 0x00000000:
            return -1 * ( n - 1 );
        case 0x00000001:
            return n + 1;
        case 0x80000000:
            return -1 * ( n + 1 );
        default:
            return n - 1;
    }
}

如果您将问题定义为允许f()接受并返回int64_t,则会涉及2147483647。当然,switch语句中使用的文字必须更改。

这对所有负数都是正确的。

    f(n) = abs(n)

因为两个互补整数的负数比正数多一个,所以f(n)=abs(n)比f(n(n)=n>0-n:n溶液,与f(n)=-abs(n)相同。一个接一个…:D

更新

不,这对一个以上的案例无效,因为我刚从李布的评论中认识到。。。abs(Int.Min)将溢出。。。

我也想过使用mod 2信息,但得出的结论是,它不起作用。。。到早期。如果操作正确,它将适用于除Int.Min之外的所有数字,因为这将溢出。

更新

我玩了一段时间,寻找一个很好的位操作技巧,但我找不到一个很不错的单行线,而mod 2解决方案适合一个。

    f(n) = 2n(abs(n) % 2) - n + sgn(n)

在C#中,这变成了以下内容:

public static Int32 f(Int32 n)
{
    return 2 * n * (Math.Abs(n) % 2) - n + Math.Sign(n);
}

要使其适用于所有值,必须将Math.Abs()替换为(n>0)+n:-n,并将计算包含在未选中的块中。然后,您甚至可以像未检查的否定一样将Int.Min映射到自身。

更新

受另一个答案的启发,我将解释函数是如何工作的,以及如何构造这样的函数。

让我们从头开始。函数f被重复应用于给定值n,产生一系列值。

    n => f(n) => f(f(n)) => f(f(f(n))) => f(f(f(f(n)))) => ...

这个问题要求f(f(n))=-n,即f的两个连续应用否定这个论点。另外两次应用f-总共四次-再次否定论点,再次产生n。

    n => f(n) => -n => f(f(f(n))) => n => f(n) => ...

现在有一个明显的长度为4的循环。代入x=f(n),并注意所获得的方程式f(f(f)n))=f(f f(x))=-x成立,得出以下结果。

    n => x => -n => -x => n => ...

所以我们得到一个长度为4的循环,有两个数字,两个数字被取反。如果将循环想象为矩形,则取反的值位于相反的角落。

构建这样一个循环的许多解决方案之一是从n开始的以下方法。

 n                 => negate and subtract one
-n - 1 = -(n + 1)  => add one
-n                 => negate and add one
 n + 1             => subtract one
 n

一个具体的例子是这样一个循环:+1=>-2=>-1=>+2=>+1。我们快完成了。注意到所构造的循环包含一个奇数正数,它的偶数后继数,并且两个数都是负数,我们可以很容易地将整数划分为许多这样的循环(2^32是四的倍数),并找到了满足条件的函数。

但我们有一个零的问题。循环必须包含0=>x=>0,因为零对自身求反。因为循环状态已经是0=>x,所以它遵循0=>x=>0=>x。这只是一个长度为2的循环,x在两次应用后变为自身,而不是变为-x。幸运的是,有一个案例解决了这个问题。如果X等于零,我们得到一个长度为1的循环,它只包含零,我们解决了这个问题,得出结论,零是f的不动点。

完成?几乎我们有2^32个数字,零是留下2^32-1个数字的固定点,我们必须将这个数字分成四个数字的循环。糟糕的是,2^32-1不是四的倍数-在任何长度为四的循环中都会保留三个数字。

我将使用范围从-4到+3的较小的3位带符号iteger集来解释解决方案的其余部分。我们用零结束了。我们有一个完整的循环+1=>-2=>-1=>+2=>+1。现在让我们构建从+3开始的循环。

    +3 => -4 => -3 => +4 => +3

出现的问题是+4不能表示为3位整数。我们可以通过将-3减为+3来获得+4,这仍然是一个有效的3位整数,但然后将1加上+3(二进制011)得到100个二进制。它被解释为无符号整数,它是+4,但我们必须将它解释为有符号整数-4。因此实际上,本例中的-4或一般情况下的Int.MinValue是整数算术否定的第二个不动点-0和Int.MinValue映射到它们自己。所以循环实际上如下。

    +3 =>    -4 => -3 => -4 => -3

这是一个长度为2的循环,另外+3通过-4进入循环。因此,-4在两个函数应用程序之后正确映射到自身,+3在两个功能应用程序之后被正确映射到-3,但-3在两个应用程序之后错误映射到自身。

所以我们构造了一个函数,它适用于除1以外的所有整数。我们能做得更好吗?不,我们不能。为什么?我们必须构造长度为4的循环,并且能够覆盖多达四个值的整个整数范围。剩下的值是必须映射到自身的两个固定点0和Int.MinValue,以及必须由两个函数应用程序相互映射的两个任意整数x和-x。

为了将x映射到-x,反之亦然,它们必须形成一个四循环,并且必须位于该循环的相对角。因此,0和Int.MinValue也必须位于相反的角落。这将正确映射x和-x,但在两个函数应用程序之后交换两个固定点0和Int.MinValue,并留下两个失败的输入。因此,不可能构造一个适用于所有值的函数,但我们有一个适用所有值(除了一个值)的函数,这是我们所能达到的最佳效果。

这将在非常广泛的数字范围内发挥作用:

    static int f(int n)
    {
        int lastBit = int.MaxValue;
        lastBit++;
        int secondLastBit = lastBit >> 1;
        int tuple = lastBit | secondLastBit;
        if ((n & tuple) == tuple)
            return n + lastBit;
        if ((n & tuple) == 0)
            return n + lastBit;
        return -(n + lastBit);
    }

我最初的方法是使用最后一位作为检查位,以了解我们在第一次或第二次调用中的位置。基本上,我会在第一次调用后将此位设置为1,以向第二次调用发出第一次调用已经通过的信号。但是,这种方法被负数所击败,负数的最后一位在第一次调用期间已经到达1。

同样的理论适用于大多数负数的倒数第二位。但是,通常发生的情况是,大多数情况下,最后一位和第二位是相同的。它们要么都是负数的1,要么都是正数的0。

所以我的最后一个方法是检查它们是否都是1或都是0,这意味着在大多数情况下这是第一次调用。如果最后一位与第二个最后一位不同,那么我假设我们在第二次调用,然后简单地重新反转最后一位。显然,对于使用最后两位的非常大的数字来说,这不起作用。但是,它再次适用于非常广泛的数字。

MIN_INT不会失败:

int f(n) { return n < 0 ? -abs(n + 1) : -(abs(n) + 1); }