仅使用Int64中间体

时间:2016-10-03 07:31:41

标签: c# precision mod

计算square pyramidal number n (n + 1) (2 n + 1) / 6 mod M的n值高达10 ^ 9(和素数M)会带来一些挑战,因为模数减少前的中间结果可能超过10 ^ 27,因此可以对于64位整数而言太大了。

在乘法之前减少以M为模的因子会导致除以6的问题,因为在减少模M之后执行该除法显然会给出无意义的结果。

我正在使用解决方法的时刻基于n (n + 1)必须对任何n都是偶数且n (n + 1)(2 n + 1)必须可以被3整除的事实:

const int M = 1000000007;

static int modular_square_pyramidal_number (int n)
{
    var a = (Int64)n * (n + 1) / 2;
    var b = 2 * n + 1;
    var q = a / 3;
    var p = q * 3 == a ? (q % M) * b : (a % M) * (b / 3);

    return (int)(p % M);
}

正如你所看到的,这真的很尴尬。是否有一种更优雅/更有效的方式来执行此计算而无需使用BigInteger或Decimal,可能以某种方式使用中间减少模3 M?

背景:在解决HackerEarth的Tic Tac Toe练习问题时遇到了问题。基于我的尴尬黑客的提交被接受但我对这个半生不熟的解决方案不满意。这就是这些练习问题的重点,不是吗:如果我接受任何基于先前存在的知识的半生不熟的解决方案,我就不会学习任何东西。因此,我一直致力于改进解决方案,直到它们达到简洁和优雅的状态......

1 个答案:

答案 0 :(得分:1)

我对减少模3 M的直觉已被淘汰 - 在测试显示它有效之后,它只需要花费一些时间将数据固定下来。

关键是Chinese Remainder Theorem,它有效地保证了互质p和q

(x / q) mod p = ((x mod pq) / q) mod p

让我们按照我的问题计算公式的相同分割:

n (n + 1) (2 n + 1) / 6 mod M = a b / 3 mod M

a = n (n + 1) / 2
b = 2 n + 1

a或b必须可以被3整除,但不知道是哪一个,并且a * b可能太大而不适合64位整数(大约90位,假设n≤0的原始约束) 1E9)。

然而,对于M = 1000000007(即通常的1e9 + 7),术语3 * M仅需要32位,同样适用于a减少的模3 M.自{{1已经适合31位,这意味着产品可以使用64位算术计算:

b

更改了代码:

((a mod 3 M) * b) / 3 mod M

这使用了无符号算术,这在这里是合适的并且也更有效,因为有符号算术通常需要编译器额外的努力(读取:发出附加指令)以便实现带符号的算术语义。

基准测试显示这比我的问题中的原始代码快两倍 - 但仅限于旧框架版本(最多3.5)。从版本4.0开始,JIT编译器不再将无符号除以常数转换为乘法+移位。除法指令往往至少比multiplicati要慢一个数量级,因此代码变得比使用新编译器的系统上的原始代码慢很多。

在这样的系统上,最好使用流程并使用低效但政治上正确的签名整数:

static int v1 (int i)
{
    var n = (uint)i;
    var a = ((UInt64)n * (n + 1) >> 1) % (M * 3U);
    var b = 2 * n + 1;

    return (int)((a * b / 3) % M);
}

以下是针对框架版本2.0的老化Haswell笔记本电脑上1000000次呼叫的基准测试:

static int v2 (int n)
{
    var a = ((Int64)n * (n + 1) >> 1) % (M * 3L);
    var b = 2 * n + 1;

    return (int)((a * b / 3) % M);
}

时间以毫秒为单位,v0代表我的问题中的原始代码。很容易看出签名语义的开销如何使v2比在内部使用无符号算术的v1慢得多。

Environment.Version和时序对于高达3.5的框架版本完全相同,所以我猜他们都使用相同的环境/编译器。

现在,微软新推出的和改进的编译器的时间安排在框架4.0和更新版本中:

IntPtr.Size = 8, Environment.Version = 2.0.50727.8009
bench 1000000:    8,407 v0    3,413 v1    4,653 v2
bench 1000000:    8,017 v0    3,179 v1    5,038 v2
bench 1000000:    8,641 v0    3,114 v1    4,801 v2

Environment.Version和时序与框架版本4.0到4.6.1完全相同。

POST SCRIPTUM - 使用模乘法逆

另一种解决方案是使用除数的modular multiplicative inverse。在本案中,这是有效的,因为已知最终产品可被除数整除(即3);如果不是那么结果将是非常不准确的。示例(333333336是3模1000000007的乘法逆):

IntPtr.Size = 8, Environment.Version = 4.0.30319.42000
bench 1000000:    9,518 v0   20,479 v1    5,687 v2
bench 1000000:    9,225 v0   20,251 v1    5,540 v2
bench 1000000:    9,133 v0   20,333 v1    5,389 v2

这个主题的存在理由是整数除法可能是有损的,因为它会丢弃余数,如果有的话,所以如果错误的因子除以3,则金字塔形平方计算的结果将是错误的。 p>

模块化除法 - 即与乘法逆的乘法 - 不是有损的,因此哪个因子与逆相乘并不重要。这可以很容易地在刚刚显示的例子中看到,其中7和8的古怪残差有效地编码小数余数,并且加上它们 - 对应于计算7/3 + 8/3 - 给出1000000012等于5 mod 1000000007就像预期

因此,问题的关键在于 final 产品可以被除数整除,但无论何时何地发生'除法'(与逆相乘)都无关紧要。结果代码的效率略低于v1,与v2大致相同,因为在与反函数相乘后需要额外的减少模M。但是,无论如何我都在展示它,因为这种方法有时会派上用场:

7 * 333333336 % 1000000007 = 333333338  // 7 mod 3 != 0
8 * 333333336 % 1000000007 = 666666674  // 8 mod 3 != 0
9 * 333333336 % 1000000007 =         1  // 9 mod 3 == 0     

注意:我放弃了右移并将除数2合并到逆中,因为单独除2除此处不再有任何用途。时间与v2相同。