【问题标题】:Need Explanation of this code需要解释此代码
【发布时间】:2014-09-14 04:53:18
【问题描述】:

代码输出满足条件的 (i,j) 对数

(2^j-1) % (2^i-1) == 0

在哪里

1<=i<j<=n

n 是用户输入的数字,在该数字下可以找到 (i,j) 对。 代码完美运行,但这段代码背后的逻辑是什么让人难以理解。

P.S: t 是一个变量,它允许用户一次输入多个数字。

    #include<stdio.h>
    #include<math.h>
    int main()
    {   
        int t;
        long n,sum,ans; 
        scanf("%d",&t);
        while(t--)
        {
            scanf("%ld",&n);
            int nrt=(int)sqrt(n);
            sum=0;
            for(int i=1;i<=nrt;i++)
            {
                 sum+=n/i;
            }
            ans=2*sum-nrt*nrt-n;
            printf("%ld\n",ans);
        }
    return 0;
    }

【问题讨论】:

  • 有什么难的? 2^j 是一个切换 2 位的 xor 操作; -1 从中减去一个。 % 运算符的 RHS 也有类似的操作。当 LHS 除以 RHS 时,% 运算符给出余数。但是,看过代码后,我很困惑——关于(2^j-1) % (2^i-1) == 0 的问题与显示的代码有什么关系?这是计算相同结果的另一种方法吗?
  • @JonathanLeffler OP 几乎可以肯定在这种情况下(不是 C 代码)使用“^”来表示求幂。 “这是另一种计算相同结果的方法吗?” -- OP 说“代码输出满足条件的 (i,j) 对的数量......” -- 这是关系的明确陈述。
  • Prateek:最好问问写代码的人。他们可能会为您提供一篇数学文章,证明给定问题的这种解决方案。做出这样的证明可能并非易事。
  • “t 是一个变量,它允许用户一次输入多个数字”——只要 n 的 scanf 成功,循环会好得多。
  • 使用所有警告和调试信息 (gcc -Wall -g) 编译您的程序。考虑scanf 的返回值(它是成功扫描的项目数)。 使用调试器 (gdb)

标签: c algorithm math expression


【解决方案1】:

让我们对问题采取暴力破解方法并打印结果*:

 #############################   2^^1 -1 == 1
  -#-#-#-#-#-#-#-#-#-#-#-#-#-#   2^^2 -1  == 3
   --#--#--#--#--#--#--#--#--#   2^^3 -1  == 7
    ---#---#---#---#---#---#--   2^^4 -1  == 15
     ----#----#----#----#----#   2^^5 -1  == 31
      -----#-----#-----#-----#   2^^6 -1  == 63
       ------#------#------#--   2^^7 -1  == 127
        -------#-------#------   2^^8 -1  == 255
         --------#--------#---   2^^9 -1  == 511
          ---------#---------#   2^^10 -1 == 1023
           ----------#--------   2^^11 -1 == 2047
            -----------#------   2^^12 -1 == 4095
             ------------#----   2^^13 -1 == 8191
              -------------#--   2^^14 -1 == 16383
               --------------#   2^^15 -1 == 32767
                --------------   2^^16 -1 == 65535
                 -------------   2^^17 -1 == 131071
                           ...   ...

井号表示满足您的条件的情况。出现了一个很好的模式:你的每个数字都可以被 1 整除,每个数字都可以被 3 整除,每个三分之一都可以被 7 整除,以此类推。每个ith 数字都可以被2^^i - 1整除。**

有了这种见解,我们可以将您的函数编码为:

int f(int n)
{
    int sum = 0;
    int i;

    for (i = 1; i <= n; i++) sum += (n - i) / i;

    return sum;
}

我们可以将(n - i) / i替换为n / i - 1,并将公共减数-1移入返回值:

int g(int n)
{
    int sum = 0;
    int i;

    for (i = 1; i <= n; i++) sum += n / i;

    return sum - n;
}

现在让我们看看总和∑(1, n: n / i)。例如:

∑(i = 1, 9: 9 / i) = 9 + 4 + 3 + 2 + 1 + 1 + 1 + 1 + 1

我们可以通过从右到左查看它并计算每个加法出现的频率来得到相同的总和:

∑(i = 1, 9: 9 / i) = 5*1 + 1*2 + 1*3 + 1*4 + 1*9

我们可以很容易地得到这个表示:

∑(i = 1, n: n / i) = ∑(1, n: i * (n / i - n / (i + 1))

这实际上只是另一种写总和的方式;您可以通过对总和进行不同的分组来看到这一点,以便它们共享相同的分母:

∑(i = 1, N: i * (n / i - n / (i + 1))
    = n + ∑(i = 1, n: ((i + 1) - i)  * n / (i + 1))
    = n + ∑(i = 1, n: n / (i + 1)) - (N + 1) * (n / (N + 1))
    = n + ∑(i = 2, n + 1: n / i) - c
    = ∑(i = 1, n: n / i) - c

附加术语c = (N + 1) * (n / (N + 1)) 是一个修正术语,因为i = n + 1 仅使用了一半的术语。在整个范围内求和时,n / (n + 1) 为零并消失。仅对数组的一部分求和时它不会消失,我们稍后会看到。

如果我们在s = sqrt(n) 处将总和分成头部和尾部,我们得到:

∑(i = 1, n: n / i) = ∑(i = 1, s: n / i) + ∑(s + 1, n: n / i)

让我们以原始方式表示头部,以“计算和数”方式表示尾部,例如:

∑(i = 1, 9: 9 / i) = (9 + 4 + 3)   +   (5*1 + 1*2)

对于任何n

∑(i = 1, n: n / i) 
    = ∑(i = 1, s: n / i) + ∑(1, s - 1: i * (n / i - n / (i + 1))
    = ∑(i = 1, s: n / i) + ∑(1, s: n / i) - s * (n / s)

所有除法都是整数除法(这就是为什么有时必须有括号)和n / s == s,所以:

∑(1, n: n / i) = ∑(i = 1, s: n / i) + ∑(i = 1, s: n / i) - s * (n / s)
               = 2 * ∑(i = 1, s: n / i) - s²

这会产生你原来的功能:

int h(int n)
{
    int nrt = sqrt(n);
    int sum = 0;
    int i;

    for(i = 1; i <= nrt; i++) sum += n/i;

    return 2 * sum - nrt * nrt - n;
}

上面g 中的∑(1, n: n / i) 已替换为2 * ∑(i = 1, s: n / i) - s²

*) 我在这里偷了D's power operator^^,以免混淆那些将^ 视为表面价值的老C 爱好者,即xor。

**) 我不知道,为什么模式会显示。可能有一个很好的解释,但就目前而言,我相信我的模式匹配技能。盲目地。 编辑 @nevets 的回答对这种模式进行了解释。

【讨论】:

  • 如果您将输出列表从2^^1 == 1 编辑为2^^1 - 1 == 1 会更正确;)
  • @Jongware:哎哟!这来自于对运营商自鸣得意,而没有关注其他方面。我会编辑。
  • @nevets:感谢您的解释,我已经交叉引用了您的解决方案。
  • 抱歉,前面的答案没有概括为一般情况,即 (2^(nk) - 1)。有关详细信息,请参阅我的修订答案:)
【解决方案2】:

这是一个非常有趣的问题。如果你尝试了一些小的输入,你会对代码有一个大概的了解。

n = 10 时,我使用了一个非常简单的代码来生成所有有效对,这就是我得到的:

1 2
1 3
1 4
1 5
1 6
1 7
1 8
1 9
1 10
2 4
2 6
2 8
2 10
3 6
3 9
4 8
5 10

惊喜?我们可以在这里看到一个非常明显的模式:当i, j 满足j = k * i 时,k 是一个整数,j = k * i &lt; ni, j 是一个有效的对。和原方程完全没有关系,只取决于n

其实这并不奇怪,因为(2^(nk) - 1) = ((2^k)^n - 1) = (a^n - 1),其中a = 2^k因此我们可以申请factoring rule,得到(a^n - 1) = (a - 1)(a^(n - 1) + a^(n - 2) + .. + 1),因此可以被(a - 1)分解,即(2^(nk) - 1) % (2^k - 1) == 0

现在问题变成了如何有效地计算这个数字。根据条件,我们有j &gt; i。以前我们知道j = k * i。因此,k 必须在[2, n / i] 的范围内。对于每个i,我们完全有(n / i) - 2 + 1 = (n / i) - 1k 的有效选择。因此,有效对的总数将为sigma((n / i) - 1, 1 &lt;= i &lt;= n)

至于如何将方程转换成你给出的代码,请参考@MOehm的回答。

【讨论】:

  • 啊,很好!您已经找到了我只看到但无法解释的模式的解释。现在很明显了。
  • @MOehm 我认为我们使用不同的方法达到了相同的解决方案:)
【解决方案3】:

变量 i 从 1 到 nrt,nrt 是 n converted explicitily into an int value 的平方根。每次循环工作时,总和都会与 (n/i) 的结果相加。然后代码打印出ans(long 类型),计算方式为(sum-nrt square-n 的两倍)。

【讨论】:

  • OP 询问为什么该计算会产生所需的结果。
猜你喜欢
  • 1970-01-01
  • 2016-09-06
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2021-07-03
  • 1970-01-01
  • 2010-11-04
相关资源
最近更新 更多