使用恰好k个翻转操作生成的长度为n的不同二进制序列的数量

时间:2016-11-05 16:41:07

标签: algorithm

考虑长度 N 的二进制序列 b 。最初,所有位都设置为0.我们使用2个参数定义一个翻转操作,翻转(L,R),这样:

  • 索引在L和R之间的所有位都是"翻转",意味着值为1的位变为值为0的位,反之亦然。更确切地说,对于范围[L,R]中的所有i:b [i] =!b [i]。
  • 指定范围之外的位没有任何反应。

要求您确定可以使用完全 K 以任意给定数字为模的翻转操作获得的可能不同序列的数量,让我们称之为的 MOD 即可。
更具体地说,每个测试在第一行包含一个数字 T ,即要给出的查询数。然后有T个查询,每个查询的形式为 N K MOD ,具有上述含义。

  • 1≤N,K≤300000
  • T≤250
  • 2≤MOD≤1000 000 007
  • 测试中所有N-s的总和≤600000
  • 时间限制:2秒
  • 内存限制:65536千字节

示例:
输入:
1
2 1 1000
输出:
3
说明:
只有一个查询。初始序列为00.我们可以执行以下操作:
翻转(1,1)⇒10
翻转(2,2)⇒01
翻转(1,2)⇒11
因此,有3个可能的序列可以使用恰好1个翻转生成。

我做了一些快速观察,虽然我不确定它们是完全正确的:
如果K足够大,那就是如果我们有足够多的翻转,我们应该能够获得2个 n 序列。
如果K = 1,那么我们要寻找的结果是N(N + 1)/ 2。它也是C(n,1)+ C(n,2),其中C是二项式系数。
目前正在尝试蛮力方法,看看我是否能够发现某种规则。我认为这是一些二项式系数的总和,但我不确定 我也遇到过这个问题的一个稍微简单的变体,其中翻转操作只翻转一个指定的位。在那种情况下,结果是 C(n,k)+ C(n,k-2)+ C(n,k-4)+ ... + C(n,(1或0))。当然,还有特殊情况,其中k> 0。 ñ,但这并不是一个巨大的差异。无论如何,它很容易理解为什么会发生这种情况。我想这值得注意。

3 个答案:

答案 0 :(得分:3)

以下是一些想法:

  1. 我们可以假设没有两次翻转操作(否则,我们可以假设它没有发生)。它确实影响了操作次数,但我稍后会谈到它。

  2. 我们可以假设没有两个段相交。实际上,如果L1 < L2 < R1 < R2,我们可以执行(L1, L2 - 1)(R1 + 1, R2)翻转。一个段在另一个段内的情况类似地处理。

  3. 我们也可以假设没有两个片段相互接触。否则,我们可以将它们粘合在一起并减少操作次数。

  4. 这些观察结果给出了以下公式:通过精确翻转k段,可以获得不同序列的数量,而没有&#34;冗余&#34;翻转:C(n + 1,2 * k)(我们选择2 * k个段的末尾。它们总是不同的。左端是独占的)。

  5. 如果我们执行的动作不超过K次,则答案为sum for k = 0...K of C(n + 1, 2 * k)

  6. 直观地说,似乎可以将不超过K翻转的序列转换为精确K次翻转的序列(例如,我们可以翻转同一段的两个以上)时间并添加2个操作。我们还可以将两个以上元素的一个段拆分为两个段并添加一个操作)。

  7. 通过执行强力搜索(我知道它不是真正的证据,但看起来与上面提到的观察相结合),如果n或k等于1,确切地说就是总和。

  8. 也就是说,结果为C(n + 1, 0) + C(n + 1, 2) + ... + C(n + 1, 2 * K) - dd = 1如果n = 1 or k = 1则为0

    以下是我用来查找运行强力搜索的模式以及验证小nk的公式是否正确的代码:

    reachable = set()
    was = set()
    
    
    def other(c):
        """
        returns '1' if c == '0' and '0' otherwise
        """
        return '0' if c == '1' else '1'
    
    
    def flipped(s, l, r):
        """
        Flips the [l, r] segment of the string s and returns the result
        """
        res = s[:l]
        for i in range(l, r + 1):
            res += other(s[i]) 
        res += s[r + 1:]
        return res
    
    
    def go(xs, k):
        """
        Exhaustive search. was is used to speed up the search to avoid checking the
        same string with the same number of remaining operations twice.
        """ 
        p = (xs, k)
        if p in was:
            return
        was.add(p)
        if k == 0:
            reachable.add(xs)
            return
        for l in range(len(xs)):
            for r in range(l, len(xs)):
                go(flipped(xs, l, r), k - 1)
    
    
    def calc_naive(n, k):
        """
        Counts the number of reachable sequences by running an exhaustive search
        """
        xs = '0' * n
        global reachable
        global was
        was = set()
        reachable = set()
        go(xs, k)
        return len(reachable)
    
    
    def fact(n):
        return 1 if n == 0 else n * fact(n - 1)
    
    
    def cnk(n, k):
        if k > n:
            return 0
        return fact(n) // fact(k) // fact(n - k)
    
    
    def solve(n, k):
        """
        Uses the formula shown above to compute the answer
        """
        res = 0        
        for i in range(k + 1):
            res += cnk(n + 1, 2 * i)
        if k == 1 or n == 1:
            res -= 1
        return res
    
    
    if __name__ == '__main__':
        # Checks that the formula gives the right answer for small values of n and k
        for n in range(1, 11):
            for k in range(1, 11):   
                assert calc_naive(n, k) == solve(n, k)
    

    此解决方案比穷举搜索要好得多。例如,如果我们使用Pascal三角形计算系数,它可以在每个测试用例的O(N * K)时间内运行。不幸的是,它还不够快。我知道如何更有效地解决它MOD(使用Lucas&#39;定理),但O在一般情况下没有解决方案。

    乘法模块化反转无法立即解决此问题,因为k!(n - k)!可能没有反模MOD

    注意:我假设所有非负C(n, m)n都定义了m,如果0则等于n < m

    我想我现在知道如何为任意MOD解决它。

    1. 让我们将MOD分解为素因子p1^a1 * p2^a2 * ... * pn^an。现在可以独立地解决每个素数因子的这个问题,并使用中国剩余定理来组合结果。

    2. 让我们来修一个素数。我们假设p^a|MOD(也就是说,我们需要以模p^a的形式得到结果)。我们可以使用以下内容预先计算所有p - 免费的阶乘部分和p的最大幂,它将线性时间内的所有0 <= n <= N的阶乘除以:

      powers = [0] * (N + 1)
      p_free = [i for i in range(N + 1)]
      p_free[0] = 1
      for cur_p in powers of p <= N:
          i = cur_p
          while i < N:
              powers[i] += 1
              p_free[i] /= p
              i += cur_p
      

      现在,p-free部分的因子是所有p_free[i]的{​​{1}}的乘积,而除i <= n的p的幂是n!的前缀和1}}。

    3. 现在我们可以划分两个阶乘:powers - 免费部分与p是互质的,所以它总是有一个倒数。 p^a的权力只是被减去。

    4. 我们几乎就在那里。还有一点观察:我们可以在线性时间内预先计算p - 自由部分的逆。让我们使用欧几里德算法计算p的无p部分的逆。现在,我们可以将所有N!i迭代到0. N的倒数 - p的自由部分是i!次{{}的倒数1}}(如果我们使用i + 1元素与p_free[i]相互作用的元素在乘法中形成一个阿贝尔群的事实,我们将p无效部分的逆转换为产品很容易证明。

    5. 此算法在每个测试用例的p^a时间内运行。现在它看起来不错了。

答案 1 :(得分:1)

你已经拥有了二项式系数的好路径。有几个因素需要考虑:

将您的号码视为长度为n的二进制字符串。现在我们可以创建另一个数组,计算一个位被翻转的次数:

[0, 1, 0, 0, 1]   number
[a, b, c, d, e]   number of flips.

但偶数次翻转都会导致相同的结果,所有奇数翻转都是如此。所以基本上分布的相关部分可以表示为%2

逻辑上的下一个问题:有多少种偶数和奇数值的组合可用。我们稍后会处理这个顺序,现在只是假设为了简单起见,将翻转数组按顺序递减。我们从k开始,作为数组中唯一的翻转数字。现在我们要添加一个翻盖。由于整个翻转数组使用%2,我们需要从k的值中删除两个来实现这一点,并将它们分别插入到数组中。 E.g:

[5, 0, 0, 0]    mod 2 [1, 0, 0, 0]
[3, 1, 1, 0]          [1, 1, 1, 0]
[4, 1, 0, 0]          [0, 1, 0, 0]

正如最后一个例子所示(记住我们在最终结果中操作模2),移动单个1并不会改变最终结果中的翻转次数。因此,我们总是必须翻转翻转数组中的偶数位。如果k是偶数,则翻转位的数量也是相同的,反之亦然,无论n的值是多少。

所以现在的问题当然是有多少种不同的阵列填充方式?为简单起见,我们马上从mod 2开始 显然,如果k是奇数,我们从1个翻转位开始,否则用1开始。我们总是添加2个翻转位。我们可以继续这样做,直到我们要么翻转所有n位(或者至少翻转我们可以翻转的位数)

v = (k % 2 == n % 2) ? n : n - 1

或者我们无法在数组上进一步传播k

v = k

把它们放在一起:

noOfAvailableFlips:
    if k < n:
        return k
    else:
        return (k % 2 == n % 2) ? n : n - 1

到目前为止,总有v / 2翻转数组(mod 2)与翻转位数有所不同。现在我们来到下一部分来置换这些数组。这只是一个简单的排列函数(排列,重复精确):

flipArrayNo(flippedbits):
    return factorial(n) / (factorial(flippedbits) * factorial(n - flippedbits)

全部放在一起:

solutionsByFlipping(n, k):
    res = 0
    for i in [k % 2, noOfAvailableFlips(), step=2]:
        res += flipArrayNo(i)

    return res

这也表明,对于足够大的数字,我们无法获得2 ^ n个序列,原因很简单,我们无法按照自己的意愿安排操作。实际影响结果的翻转次数将始终为偶数或奇数,具体取决于k。没有办法解决这个问题。可以得到的最好结果是2^(n-1)序列。

答案 2 :(得分:0)

为了完整起见,这是一个动态程序。它可以很容易地处理任意模数,因为它是基于总和的,但不幸的是我还没有找到一种方法来加速O(n * k)

a[n][k]为长度n的二进制字符串数,其k个非邻近的11块以b[n][k]结尾。设nk长度1的二进制字符串数,0# we can append 1 to any arrangement of k non-adjacent blocks of contiguous 1's # that ends in 1, or to any arrangement of (k-1) non-adjacent blocks of contiguous # 1's that ends in 0: a[n][k] = a[n - 1][k] + b[n - 1][k - 1] # we can append 0 to any arrangement of k non-adjacent blocks of contiguous 1's # that ends in either 0 or 1: b[n][k] = b[n - 1][k] + a[n - 1][k] # complete answer would be sum (a[n][i] + b[n][i]) for i = 0 to k 结尾的a[n][k]个非相邻块。

然后:

b[n][k]

我想知道以下观察是否有用:(1)n < 2*k - 1k(n + 1) / 2时为零,而(2)在另一方面为{{{{1} 1}}大于⌊a = [[0] * 11 for i in range(0,11)] b = [([1] + [0] * 10) for i in range(0,11)] def f(n,k): return fa(n,k) + fb(n,k) def fa(n,k): global a if a[n][k] or n == 0 or k == 0: return a[n][k] elif n == 2*k - 1: a[n][k] = 1 return 1 else: a[n][k] = fb(n-1,k-1) + fa(n-1,k) return a[n][k] def fb(n,k): global b if b[n][k] or n == 0 or n == 2*k - 1: return b[n][k] else: b[n][k] = fb(n-1,k) + fa(n-1,k) return b[n][k] def g(n,k): return sum([f(n,i) for i in range(0,k+1)]) # example print(g(10,10)) for i in range(0,11): print(a[i]) print() for i in range(0,11): print(b[i]) ⌋整体答案似乎相同。

Python代码(完整矩阵是为了简单起见而定义的,但我认为实际上只需要一行,在空间方面,对于自下而上的方法):

    #define VERTEX_BUFFER_BIND_ID 0
    ....
    vertices.inputAttributes[0].binding = VERTEX_BUFFER_BIND_ID;
    vertices.inputAttributes[0].location = 0;
    vertices.inputAttributes[0].format = VK_FORMAT_R32G32B32_SFLOAT;
    vertices.inputAttributes[0].offset = offsetof(Vertex, position);
    // Attribute location 1: Color
    vertices.inputAttributes[1].binding = VERTEX_BUFFER_BIND_ID;
    vertices.inputAttributes[1].location = 1;
    vertices.inputAttributes[1].format = VK_FORMAT_R32G32B32_SFLOAT;
    vertices.inputAttributes[1].offset = offsetof(Vertex, color);