📌  相关文章
📜  数组范围查询以计数斐波纳契数与更新的数目(1)

📅  最后修改于: 2023-12-03 15:26:12.447000             🧑  作者: Mango

数组范围查询以计数斐波纳契数与更新的数目

本文将介绍如何在一个大小为n的数组中,进行范围查询求斐波纳契数的个数,并统计数组的更新次数。本文采用C++编写示例程序。

问题背景

给定一个大小为n的数组a,以及m个询问,每个询问包含两个整数 l 和 r,要求计算在a[l:r]这段区间中,有多少个斐波纳契数(斐波那契数列:1,1,2,3,5,8,13,21...,即前两项为1,从第三项开始,每一项都是前面两项的和)。

同时,需要记录在处理这些询问的过程中,一共进行了多少次的数组更新操作。

解决方案
1. 暴力求解

最简单的方法就是对于每个询问,都遍历区间中的数,判断它是否为斐波纳契数。时间复杂度为 O(m * (r - l) ^ 2),其中m为询问数量,(r - l) ^ 2为区间长度的平方。

此方法的时间复杂度过高,无法通过测试数据。因此,我们需要寻找更高效的算法。

2. 斐波拉契数列的特性

斐波那契数列具有以下性质:

  1. 对于斐波那契数列中的任意三个连续项f(i), f(i+1), f(i+2),都有 f(i) + f(i+1) = f(i+2)。
  2. 在斐波那契数列中,前面的数字对后面的数字有影响。假设我们已知 f(i) 和 f(i+1) 的值,则可以推出 f(i+2) = f(i) + f(i+1)。也就是说,当前项的值只与前两项有关。

由此可以得到一种思路:对于每个询问,从l开始,逐一计算区间中的数和斐波那契数列比较,直到找到第一个大于区间右端点r的斐波那契数。那么该区间中恰好有k个斐波那契数。

但是,我们注意到:当一个数比斐波那契数大时,它会很快超过斐波那契数,而当一个数比斐波那契数小时,需要很长时间才能达到斐波那契数。

因此,我们可以先预处理出一些小于数组最大值的斐波那契数,如此一来,可以在一个区间中,直接通过二分查找得到第一个大于r的斐波那契数,使得时间复杂度为 O(m * logn)。额外的空间复杂度为 O(n)。

3. 简化版

注意到这道题中,所求得的斐波那契数只涉及到区间端点和区间长度,因此可以直接使用斐波那契数列来替换斐波那契数的计算,在维护区间最小值时,同时计算区间斐波那契数的个数。

而计数区间斐波那契数的个数,可以借鉴区间最小值线段树求和的方式进行。时间复杂度为O(m * logn),额外的空间复杂度为 O(n)。

C++示例代码
#include <bits/stdc++.h>
using namespace std;

typedef long long LL;

const int MAXN = 1e5 + 10;

struct Segment_Tree {
    LL val[MAXN << 2], add[MAXN << 2], fib[MAXN];

    void push_up(LL o) {
        val[o] = val[o << 1] + val[o << 1 | 1];
    }

    void push_down(LL o, LL l, LL r) {
        if (add[o]) {
            LL mid = (l + r) >> 1;
            val[o << 1] += fib[mid - l + 1] * add[o];
            add[o << 1] += add[o];
            val[o << 1 | 1] += fib[r - mid] * add[o];
            add[o << 1 | 1] += add[o];
            add[o] = 0;
        }
    }

    void build(LL o, LL l, LL r) {
        if (l == r) {
            val[o] = 0;
            return;
        }
        LL mid = (l + r) >> 1;
        build(o << 1, l, mid);
        build(o << 1 | 1, mid + 1, r);
        push_up(o);
    }

    void update(LL o, LL l, LL r, LL ql, LL qr, LL v) {
        if (ql <= l && r <= qr) {
            add[o] += v;
            val[o] += v * fib[r - l + 1];
            return;
        }
        push_down(o, l, r);
        LL mid = (l + r) >> 1;
        if (ql <= mid) update(o << 1, l, mid, ql, qr, v);
        if (qr > mid) update(o << 1 | 1, mid + 1, r, ql, qr, v);
        push_up(o);
    }

    LL query(LL o, LL l, LL r, LL ql, LL qr) {
        if (ql <= l && r <= qr) {
            return val[o];
        }
        push_down(o, l, r);
        LL mid = (l + r) >> 1;
        LL ret = 0;
        if (ql <= mid) ret += query(o << 1, l, mid, ql, qr);
        if (qr > mid) ret += query(o << 1 | 1, mid + 1, r, ql, qr);
        push_up(o);
        return ret;
    }
}tree;

int n, m, a[MAXN];

void init() {
    tree.fib[0] = tree.fib[1] = 1;
    for (int i = 2; i <= n; i++) {
        tree.fib[i] = tree.fib[i - 1] + tree.fib[i - 2];
        if (tree.fib[i] >= ((LL)1 << 31)) break; 
    }
}

int main() {
    // input n, m, a[n]
    init();
    tree.build(1, 1, n);
    for (int i = 1; i <= m; i++) {
        // input l,r
        tree.update(1, 1, n, l, r, 1);
        printf("%lld\n", tree.query(1, 1, n, l, r));
    }
    return 0;
}
代码解释

变量:

  • val: 用于存储区间和结果的数组;
  • add: 用于存储当前节点的延迟标记;
  • fib: 用于存储斐波那契数列的数组。

函数:

  • push_up: 根据左右子树的值,更新当前节点的区间和;
  • push_down: 将当前节点的延迟标记下传至子节点,并更新子节点的区间和;
  • build: 建立线段树,区间的和初始化为$0$;
  • update: 将区间$[ql,qr]$的值全部增加$v$,并更新节点区间和的值;
  • query: 查询区间$[ql,qr]$的和的值。

时间复杂度为$O(m*logn)$。