首次出现在斯特恩的双原子序列中

时间:2013-05-04 11:30:31

标签: algorithm oeis

你得到一个整数n,你需要在斯特恩的双原子序列中找到它的第一次出现的索引。

序列的定义如下:

a[0]     = 0
a[1]     = 1
a[2*i]   = a[i]
a[2*i+1] = a[i] + a[i+1]

请参阅MathWorld

因为n可以达到400000,所以强制它不是一个好主意,特别是因为时间限制是4000毫秒。

序列很奇怪:第一次出现8是21,但第一次出现6是33。

任何想法如何解决这个问题?

也许这可能会有所帮助:OEIS

2 个答案:

答案 0 :(得分:8)

我们可以在4秒内轻松解决第一次出现400000范围内的数字:

Prelude Diatomic> firstDiatomic 400000
363490989
(0.03 secs, 26265328 bytes)
Prelude Diatomic> map firstDiatomic [400000 .. 400100]
[363490989,323659475,580472163,362981813,349334091,355685483,346478235,355707595
,291165867,346344083,347155797,316314293,576398643,315265835,313171245,355183267
,315444051,315970205,575509833,311741035,340569429,313223987,565355925,296441165
,361911645,312104147,557145429,317106853,323637939,324425077,610613547,311579309
,316037811,311744107,342436533,348992869,313382235,325406123,355818699,312128723
,347230875,324752171,313178421,312841811,313215645,321754459,576114987,325793195
,313148763,558545581,355294101,359224397,345462093,307583675,355677549,312120731
,341404245,316298389,581506779,345401947,312109779,316315061,315987123,313447771
,361540179,313878107,304788843,325765547,316036275,313731751,355635795,312035947
,346756533,313873883,349358379,357393763,559244877,313317739,325364139,312128107
,580201947,358182323,314944173,357403987,584291115,312158827,347448723,363246413
,315935571,349386085,315929427,312137323,357247725,313207657,320121429,356954923
,557139285,296392013,576042123,311726765,296408397]
(2.45 secs, 3201358192 bytes)

它的关键是Calkin-Wilf树。

从分数1/1开始,它由规则构建,对于具有分数a/b的节点,其左子项包含分数a/(a+b),其右子分数为(a+b)/b 1/1 / \ / \ / \ 1/2 2/1 / \ / \ 1/3 3/2 2/3 3/1

                         1
                        / \
                       /   \
                      /     \
                     2       3
                    / \     / \
                   4   5   6   7
                  / \ 
                 8   9 ...

等。双原子序列(从索引1开始)是Calkin-Wilf树中分数的分子序列,当它逐级遍历时,每个级别从左到右。

如果我们查看索引树

k

我们可以轻松验证Calkin-Wilf树中索引a[k]/a[k+1]处的节点是否通过归纳传递分数k = 1

a[1] = a[2] = 1k = 2*j)显然属实,从那时起,

  • 对于j,我们有一个索引为a[j]/(a[j]+a[j+1])的节点的左子节点,因此分数为a[k] = a[j]a[k+1] = a[j] + a[j+1]k = 2*j+1为序列的定义方程式。

  • 对于j,我们拥有索引为(a[j]+a[j+1])/a[j+1]的节点的正确子节点,因此分数为a[k]/a[k+1],并且通过定义等式再次为0/1

所有阳性降低的部分在Calkin-Wilf树中恰好出现一次(作为读者的练习),因此所有正整数都出现在双原子序列中。

我们可以通过索引的二进制表示从索引中找到Calkin-Wilf树中的节点,从最高有效位到最小位,对于1位我们转到右边的子节点和0位于左侧。 (为此,使用节点1/1扩充Calkin-Wilf树是很好的,节点p/q的右子节点是p > q节点,因此我们需要为索引的最重要的设置位设置一个步骤。)

现在,这对于解决手头的问题并没有多大帮助。

但是,让我们先解决一个相关问题:对于减少的分数p/q,确定其索引。

假设(p-q)/q。然后我们知道p-q > q是一个正确的孩子,其父母是(p - 2*q)/q。如果还p = a*q + b, 1 <= b < q ,我们又有一个正确的孩子,其父母是p/q。继续,如果

b/q

然后我们通过转到正确的孩子a次来到b/q节点的b/(q-b)节点。

现在我们需要找到一个分子小于其分母的节点。那当然是其父母的左子女。 q = c*b + d, 1 <= d < b 的父级是c。如果

b/d

我们必须从节点b/q转到左子1/1次才能到达p/q

等等。

我们可以找到从根(p/q)到p > q节点的方式,使用p/q = [a_0, a_1, ..., a_r,1] 的连续分数(我只考虑这里的简单连续分数)扩展。设p/q

1

r结尾的a_r的持续分数扩展。

  • 如果a_(r-1)是偶数,则转到右侧的孩子a_1次,然后转到左侧a_0次,然后转到右侧的孩子......然后r到左边的孩子的时间,最后到右边的a_r次。
  • 如果a_(r-1)是奇数,那么首先转到左侧的孩子a_1次,然后转到右侧的a_0次......然后转到左侧的孩子p < q次,最后r次向右。

对于r,我们必须结束向左移动,因此开始向右移动k并开始向右移动奇数[c_1, c_2, ..., c_j] (all c_i > 0)

因此,我们发现了索引的二进制表示与节点通过从根到节点的路径所承载的分数的连续分数扩展之间的紧密联系。

让索引k的行程编码为

c_1

即。 c_2的二进制表示形式以c_3个开头,然后是c_j个零,然后是k个等,最后是j

  • ,如果k是奇数,那么j也是奇数;
  • 零,如果[c_j, c_(j-1), ..., c_2, c_1]是偶数 - 因此a[k]/a[k+1]也是偶数。

然后k0/1的持续分数展开,其长度与1/1具有相同的奇偶性(每个有两个连续的分数扩展,一个具有奇数长度,另一个具有奇数长度,另一个具有奇数长度甚至长度。)

RLE提供从a[k]/a[k+1]以上k节点到n > 0的路径。路径的长度是

  1. 表示a[k] = a[k/2]
  2. 所需的位数
  3. 连续分数扩展中的部分商的总和。
  4. 现在,为了找到双原子序列中第一次出现k的索引,我们首先观察到最小的索引必须是奇数,因为k = 2*j+1对于偶数k。让最小的索引为k。然后

    1. a[2*j+1]/a[2*j+2] = (a[j] + a[j+1])/a[j+1]的RLE长度为奇数,
    2. 索引为k的节点的分数为a[k] = n,因此它是一个正确的孩子。
    3. 因此n的最小索引n/m对应于具有分子0 < m <= n的节点的所有最短路径的最左端。

      最短路径对应于n的连续分数扩展,其中p/q = [a_0, a_1, ..., a_r]a_0 > 0相互作用[必须减少分数],其中部分商的总和最小。< / p>

      我们需要什么样的长度?给定s = a_0 + ... + a_r 的持续分数p和总和

      F(s+1)

      分子qF(s)限定,分母F(j)j限定,其中a_0 = a_1 = ... = a_r = 1F(s+1)/F(s) - 斐波纳契数。边界很明显,F(t) < n <= F(t+1)分数为>= t

      因此,如果m,则连续分数展开(两者中的任何一个)的部分商的总和为n/m。通常会有一个tF(5) = 5 < 6 <= F(6) = 8 的连续分数展开的部分商的总和恰好是6/m,但并非总是如此:

      0 < m <= 6

      6/1 = [6] (alternatively [5,1]) 6/5 = [1,4,1] (alternatively [1,5]) 两个减少的分数t+2的连续分数展开

      n/m

      使用部分商的总和6.然而,部分商的最小可能总和绝不会大得多(我意识到的最大值是n/(n-m))。

      m < n/2n/m = [a_0, a_1, ..., a_r] 的持续分数扩展密切相关。我们假设a_0 >= 2,并让

      (n-m)/m = [a_0 - 1, a_1, ..., a_r]
      

      然后n/(n-m) = 1 + m/(n-m) = 1 + 1/((n-m)/m)

      n/(n-m)

      以及

      n/(n-m) = [1, a_0 - 1, a_1, ..., a_r]
      

      m的持续分数扩展

      n > 2

      特别是,两者的部分商的总和是相同的。

      不幸的是,我不知道如何在没有暴力的情况下找到具有最小部分商数总和的0 < m < n/2,所以算法是(我假设为n

      1. n/m互译> 1,找到[a_0, a_1, ..., a_r]的连续分数扩展,收集具有最小部分商的总和的数据(通常的算法产生扩展,最后一个部分商是[a_0, a_1, ..., a_(r-1), a_r - 1, 1],我们假设)。

      2. 通过以下方式调整找到的连续分数扩展[数量不大]:

        • 如果CF [1, a_0 - 1, a_1, ..., a_(r-1), a_r - 1, 1]的长度均匀,请将其转换为n/m
        • 否则,请使用n/(n-m)

        (选择导致较小索引的module Diatomic (diatomic, firstDiatomic, fuscs) where import Data.List strip :: Int -> Int -> Int strip p = go where go n = case n `quotRem` p of (q,r) | r == 0 -> go q | otherwise -> n primeFactors :: Int -> [Int] primeFactors n | n < 1 = error "primeFactors: non-positive argument" | n == 1 = [] | n `rem` 2 == 0 = 2 : go (strip 2 (n `quot` 2)) 3 | otherwise = go n 3 where go 1 _ = [] go m p | m < p*p = [m] | r == 0 = p : go (strip p q) (p+2) | otherwise = go m (p+2) where (q,r) = m `quotRem` p contFracLim :: Int -> Int -> Int -> Maybe [Int] contFracLim = go [] where go acc lim n d = case n `quotRem` d of (a,b) | lim < a -> Nothing | b == 0 -> Just (a:acc) | otherwise -> go (a:acc) (lim - a) d b fixUpCF :: [Int] -> [Int] fixUpCF [a] | a < 3 = [a] | otherwise = [1,a-2,1] fixUpCF xs | even (length xs) = case xs of (1:_) -> fixEnd xs (a:bs) -> 1 : (a-1) : bs | otherwise = case xs of (1:_) -> xs (a:bs) -> 1 : fixEnd ((a-1):bs) fixEnd :: [Int] -> [Int] fixEnd [a,1] = [a+1] fixEnd [a] = [a-1,1] fixEnd (a:bs) = a : fixEnd bs fixEnd _ = error "Shouldn't have called fixEnd with an empty list" cfCompare :: [Int] -> [Int] -> Ordering cfCompare (a:bs) (c:ds) = case compare a c of EQ -> cfCompare ds bs cp -> cp fibs :: [Integer] fibs = 0 : 1 : zipWith (+) fibs (tail fibs) toNumber :: [Int] -> Integer toNumber = foldl' ((+) . (*2)) 0 . concat . (flip (zipWith replicate) $ cycle [1,0]) fuscs :: Integer -> (Integer, Integer) fuscs 0 = (0,1) fuscs 1 = (1,1) fuscs n = case n `quotRem` 2 of (q,r) -> let (a,b) = fuscs q in if r == 0 then (a,a+b) else (a+b,b) diatomic :: Integer -> Integer diatomic = fst . fuscs firstDiatomic :: Int -> Integer firstDiatomic n | n < 0 = error "Diatomic sequence has no negative terms" | n < 2 = fromIntegral n | n == 2 = 3 | otherwise = toNumber $ bestCF n bestCF :: Int -> [Int] bestCF n = check [] estimate start where pfs = primeFactors n (step,ops) = case pfs of (2:xs) -> (2,xs) _ -> (1,pfs) start0 = (n-1) `quot` 2 start | even n && even start0 = start0 - 1 | otherwise = start0 eligible k = all ((/= 0) . (k `rem`)) ops estimate = length (takeWhile (<= fromIntegral n) fibs) + 2 check candidates lim k | k < 1 || n `quot` k >= lim = if null candidates then check [] (2*lim) start else minimumBy cfCompare candidates | eligible k = case contFracLim lim n k of Nothing -> check candidates lim (k-step) Just cf -> let s = sum cf in if s < lim then check [fixUpCF cf] s (k - step) else check (fixUpCF cf : candidates) lim (k-step) | otherwise = check candidates lim (k-step) 和{{1}}之间的那个)

      3. 反转连续分数以获得相应指数的游程编码

      4. 选择其中最小的一个。

      5. 在步骤1中,使用到目前为止发现的最小总和是有用的。

        代码(Haskell,因为那是最简单的):

        {{1}}

答案 1 :(得分:2)

我建议你阅读这个letter from Dijkstra,它解释了通过以下方式计算此功能的另一种方法:

n, a, b := N, 1, 0;
do n ≠ 0 and even(n) → a, n:= a + b, n/2
             odd(n) → b, n:= b + a, (n-1)/2
od {b = fusc(N)}

这从a开始,b = 1,0并且有效地使用N的连续位(从最小到最重要)来增加a和b,最终结果是b的值。

因此,可以通过查找此迭代将导致b值的最小n来计算b的特定值的第一次出现的索引。

找到这个最小n的一种方法是使用A* search,其中cost是n的值。算法的效率将取决于您选择的启发式算法。

对于启发式,我建议注意:

  1. 最终值将始终是gcd(a,b)的倍数(这可以用来排除一些永远不会产生目标的节点)。
  2. b总是增加
  3. 存在b可以增加的最大(指数)速率(速率取决于a的当前值)
  4. 修改

    下面是一些示例Python代码来说明A *方法。

    from heapq import *
    
    def gcd(a,b):
        while a:
            a,b=b%a,a
        return b
    
    def heuristic(node,goal):
        """Estimate least n required to make b==goal"""
        n,a,b,k = node
        if b==goal: return n
        # Otherwise needs to have at least one more bit set
        # Improve this heuristic to make the algorithm faster
        return n+(1<<k)
    
    def diatomic(goal):
        """Return index of first appearance of n in Stern's Diatomic sequence"""
        start=0,1,0,0
        f_score=[] # This is used as a heap
        heappush(f_score, (0,start) )
        while 1:
            s,node = heappop(f_score)
            n,a,b,k = node
            if b==goal:
                return n
            for node in [ (n,a+b,b,k+1),(n+(1<<k),a,b+a,k+1) ]:
                n2,a2,b2,k2 = node
                if b2<=goal and (goal%gcd(a2,b2))==0:
                    heappush(f_score,(heuristic(node,goal),node))
    
    print [diatomic(n) for n in xrange(1,10)]