Skip to content

FibonacciPartition - 题解

标签与难度

标签: 数据结构, set, 斐波那契, 数论, 贪心, 构造 难度: 2900

题目大意喵~

主人你好呀,这道题是关于斐波那契数列的一个有趣问题!喵~

首先,题目定义了一个斐波那契数列:

Fn={1,n=12,n=2Fn1+Fn2,otherwiseF_n = \begin{cases} 1, & n=1 \\ 2, & n=2 \\ F_{n-1} + F_{n-2}, & \text{otherwise} \end{cases}

这个数列的前几项是 1,2,3,5,8,13,1, 2, 3, 5, 8, 13, \dots

然后,对于一个正整数 nn,我们定义 partition(n) 为:将 nn 表示成不同的斐波那契数之和时,能够使用的最多的项数。 比如说,partition(6)=3,因为 66 可以表示成 1+2+3=F1+F2+F31+2+3 = F_1+F_2+F_3,这是用最多项的表示方法。

题目会给我们一个初始为 00 的整数 xx。接下来会有一系列操作,每次操作会给出两个数 aia_ibib_i,我们要把 xx 的值加上 aiFbia_i \cdot F_{b_i}。在每次操作之后,我们都需要计算并输出当前的 partition(x) 是多少。

简单来说,就是维护一个大数 xx,在每次更新后,找出把它拆成不同斐波那e契数的和时,最多能拆成几项,呐。

解题思路分析

这道题真的很有挑战性呢,喵!直接处理一个可能非常大的数字 xx 并计算它的 partition(x) 是不现实的。我们需要找到一种方法来表示 xx,这种表示方法要能方便地进行加减法,并且能快速地计算出 partition(x)

核心思想:寻找最优表示法

要想让斐波那契数的项数最多,我们的策略应该是“反过来”的 Zeckendorf 定理。Zeckendorf 定理告诉我们,任何正整数都可以唯一地表示为一组不相邻的斐波那契数之和,这通常是项数最少的表示。而我们想要项数最多,就要反其道而行之:尽量把大的斐波那契数换成小的。

我们有恒等式 Fk=Fk1+Fk2F_k = F_{k-1} + F_{k-2}。这意味着,只要我们的和里面有 FkF_k 但没有 Fk1F_{k-1}Fk2F_{k-2},我们就可以把 FkF_k 替换掉,增加一项。这个过程可以一直进行下去,直到无法再分解为止。

经过一番探索(和偷看了一点点答案,喵呜~),可以发现一个神奇的结论:任何正整数 xx 都可以被唯一地表示为一堆“同奇偶性连续斐波那契数之和”的和。

这是什么意思呢?一个“同奇偶性连续斐波那契数”的片段,我们叫它“梳子”(comb),形如 Fl+Fl+2+Fl+4++FrF_l + F_{l+2} + F_{l+4} + \dots + F_r,其中所有下标的奇偶性相同。例如,F2+F4+F6F_2+F_4+F_6 就是一个偶数梳子。

一个非常漂亮的性质是,这样一个梳子的和可以用一个简单的公式计算:

Fl+Fl+2++Fr=Fr+1Fl1F_l + F_{l+2} + \dots + F_r = F_{r+1} - F_{l-1}

(这里我们需要定义 F0=1,F1=0F_0=1, F_{-1}=0 来让公式在边界情况下也成立)。

所以,任何数字 xx 都可以表示成 (Fri+1Fli1)\sum (F_{r_i+1} - F_{l_i-1}) 的形式,其中 [li,ri][l_i, r_i] 是一系列互不相交的梳子。这个表示是唯一的!

计算 partition(x)

当我们得到了这个“梳子表示法”,partition(x) 的值就可以通过一个公式计算出来。它等于所有梳子中斐波那契数的总数,再加上因分解梳子边界而额外产生的项数

具体来说,partition(x) 的值是:

  1. 所有梳子 [l, r] 的大小之和。一个梳子 [l, r] 的大小为 (rl)/2+1(r-l)/2 + 1
  2. 梳子之间的“间隙”产生的额外项。对于一个不是从 F1F_1F2F_2 开始的梳子 [l, r],它可以看作是从一个更大的斐波那契数分解而来的。这个分解过程会填补它与前一个梳子之间的间隙,从而产生更多的项。

这个计算方式有点复杂,但在代码中 MdfAns 函数实现了这个逻辑。它在每次插入或删除一个梳子时,动态地更新总答案。

维护梳子集合

现在,问题转化为了如何维护这个梳子的集合。每次操作 x += a * F_b,我们都需要更新这个集合。

第一步:分解 a * F_b

单个的 FbF_b 好办,但我们面对的是 aFba \cdot F_b。如果 aa 很大,我们不能真的加 aaFbF_b。这里需要一个“黑魔法”,就是把 aa 本身也用一个类似斐波那契的数系来表示。利用 2Fi=Fi+1+Fi22F_i = F_{i+1} + F_{i-2} 这样的恒等式,我们可以把任意整数 aa 表示成 dj(something related to Fj)\sum d_j \cdot (\text{something related to } F_j) 的形式。然后 aFba \cdot F_b 就可以变成 djFb+j\sum d_j \cdot F_{b+j'} 的形式。

参考代码中的 ToModifies 函数做的就是这个工作。它是一个相当复杂的数论过程,把 aFba \cdot F_b 分解成了一系列 ±Fk\pm F_k 的加减操作。我们可以暂时把它当成一个可靠的黑盒,它会告诉我们,一次 x += a * F_b 的操作等价于进行若干次 x += F_kx -= F_k

第二步:处理单个 FkF_k 的加减

现在我们只需要实现 Put(k) (加 FkF_k) 和 Take(k) (减 FkF_k) 这两个核心操作。这两个操作会修改我们维护的梳子集合。

Put(k) 的逻辑分析:

当我们执行 Put(k),即向 xx 中加入 FkF_k 时,有以下几种情况:

  1. 创造新梳子: 如果 Fk2F_{k-2}Fk+2F_{k+2} 都不在当前的和中,那么 FkF_k 就会形成一个自己的新梳子 [k, k]
  2. 合并梳子:
    • 如果 Fk2F_{k-2} 存在(即一个梳子以 k2k-2 结尾),FkF_k 会和它合并,将梳子 [l, k-2] 扩展为 [l, k]
    • 同理,如果 Fk+2F_{k+2} 存在(一个梳子以 k+2k+2 开始),FkF_k 会和它合并。
    • 如果两边都存在,那么 FkF_k 会像一座桥梁,把两个梳子 [l1, k-2][k+2, r2] 连接成一个大梳子 [l1, r2]
  3. 梳子内加法 (最复杂的情况): 如果 FkF_k 已经存在于某个梳子 [l, r] 中了(即 lkrl \le k \le rk,lk, l 奇偶性相同),我们相当于执行了 Fk+Fk=2FkF_k + F_k = 2F_k。根据恒等式 2Fk=Fk+1+Fk22F_k = F_{k+1} + F_{k-2},我们需要:
    • 从原有的和中移除 FkF_k。这会把梳子 [l, r]kk 处断开,分裂成 [l, k-2][k+2, r] 两个小梳子。
    • 然后,我们再加入 Fk+1F_{k+1}Fk2F_{k-2}。这相当于递归调用 Put(k+1)Put(k-2)

Take(k) 的逻辑与 Put(k) 相反,同样复杂,是它的逆操作。

总的来说,整个算法就是用 std::set 维护一堆梳子,然后通过一系列精巧的合并、分裂操作来响应 ±Fk\pm F_k 的更新。这只我的爪子都快算不过来了,喵~ 但只要理清了这些情况,我们就可以写出代码啦!

代码实现

这是我根据上面的思路,重新整理和注释的代码。希望能帮助你理解这个精妙的过程,喵~

cpp
#include <iostream>
#include <vector>
#include <set>
#include <algorithm>
#include <numeric>

// 使用快读模板
namespace io {
    const int BUF_SIZE = 1 << 21 | 1;
    char in_buf[BUF_SIZE], *in_s, *in_t, out_buf[BUF_SIZE], *out_s = out_buf, *out_t = out_s + BUF_SIZE - 1;
    int f, t;
    char c, ch[100];

    #define GET_CHAR() (in_s == in_t ? (in_t = (in_s = in_buf) + fread(in_buf, 1, BUF_SIZE, stdin), in_s == in_t ? EOF : *in_s++) : *in_s++)

    inline void flush() {
        fwrite(out_buf, 1, out_s - out_buf, stdout);
        out_s = out_buf;
    }

    inline void put_char(char x) {
        *out_s++ = x;
        if (out_s == out_t) flush();
    }

    template <class T>
    inline void read(T &x) {
        for (f = 1, c = GET_CHAR(); c < '0' || c > '9'; c = GET_CHAR()) if (c == '-') f = -1;
        for (x = 0; c >= '0' && c <= '9'; x = (x << 3) + (x << 1) + (c & 15), c = GET_CHAR());
        x *= f;
    }

    template <class T>
    inline void print(T x, char k = '\n') {
        if (!x) put_char('0');
        if (x < 0) put_char('-'), x = -x;
        while (x) ch[++t] = x % 10 + '0', x /= 10;
        while (t) put_char(ch[t--]);
        put_char(k);
    }

    struct Flusher { ~Flusher() { flush(); } } flusher;
}
using io::read;
using io::print;

const int INF = 0x3f3f3f3f;

// "梳子"结构体,表示 F_min + F_{min+2} + ... + F_max
struct Comb {
    int min_idx, max_idx;
    Comb(int l = 0, int r = 0) : min_idx(l), max_idx(r) {}
    // set需要一个排序依据,我们按max_idx排序
    bool operator<(const Comb &other) const {
        return max_idx < other.max_idx;
    }
};

std::set<Comb> comb_set;
using Iter = std::set<Comb>::iterator;
long long total_partition_count;

// 更新总答案。在集合中添加或删除梳子p2时,它与前后梳子p1, p3的关系会影响答案
void update_answer(Comb p1, Comb p2, int sign) {
    if (p2.max_idx == INF) return;

    // 情况1: 梳子p2从F_1或F_2开始
    // 贡献就是它自身的项数
    if (p2.min_idx <= 2) {
        total_partition_count += sign * ((p2.max_idx - p2.min_idx) / 2 + 1);
    } 
    // 情况2: 梳子p2从 > F_2 的地方开始
    // 贡献 = 自身项数 + 填补与前一个梳子p1之间空隙产生的项数
    else {
        int prev_bound = (p1.max_idx == -INF) ? 0 : (p1.min_idx <= 2 ? p1.max_idx : p1.max_idx - 1);
        long long comb_size = (p2.max_idx - p2.min_idx) / 2 + 1;
        long long gap_fill_count = (p2.min_idx - prev_bound - 3) / 2;
        total_partition_count += sign * (comb_size + gap_fill_count);
    }
}

void insert_comb(Comb c) {
    Iter it = comb_set.insert(c).first;
    Iter prev = it, next = it;
    --prev; ++next;
    update_answer(*prev, *next, -1); // 减去旧的 p1-p3 间隙贡献
    update_answer(*prev, *it, 1);    // 加上 p1-it 间隙贡献
    update_answer(*it, *next, 1);    // 加上 it-p3 间隙贡献
}

void erase_comb(Iter it) {
    Iter prev = it, next = it;
    --prev; ++next;
    update_answer(*prev, *it, -1);    // 减去 p1-it
    update_answer(*it, *next, -1);    // 减去 it-p3
    update_answer(*prev, *next, 1);    // 加上 p1-p3
    comb_set.erase(it);
}

// 声明函数,因为它们会相互递归调用
void put(int pos);
void take(int pos);

// 处理在梳子内部添加 F_pos 的情况
// 对应 2*F_k = F_{k+1} + F_{k-2}
void handle_put_inside_comb(Iter it, int pos) {
    int cur_min = it->min_idx, cur_max = it->max_idx;
    erase_comb(it);
    // 原梳子分裂
    if (cur_min != pos) insert_comb(Comb(cur_min + 1, pos - 1));
    // 递归处理 F_{k+1} 和 F_{k-2}
    put(cur_max + 1);
    if (cur_min != 1) put(cur_min == 2 ? 1 : cur_min - 2);
}

// 处理在梳子右边添加 F_pos 的情况
// 对应 F_{k} + F_{k+1} = F_{k+2}
void handle_put_right_of_comb(Iter it, int pos) {
    int cur_min = it->min_idx, cur_max = it->max_idx;
    erase_comb(it);
    if (cur_min != cur_max) insert_comb(Comb(cur_min, cur_max - 2));
    put(pos + 1);
}

// 处理在梳子间隙添加 F_pos 的情况
void handle_put_in_gap(Iter it, int pos) {
    int cur_min = it->min_idx, cur_max = it->max_idx;
    erase_comb(it);
    if (cur_min <= pos - 1) insert_comb(Comb(cur_min, pos - 1));
    put(cur_max + 1);
}

// 添加 F_pos
void put(int pos) {
    // 找到第一个 max_idx >= pos-1 的梳子
    Iter it = comb_set.lower_bound(Comb(0, pos - 1));

    // Case 1: pos 与梳子 it "接触"
    if (it->min_idx - 1 <= pos && pos <= it->max_idx + 1) {
        if (pos == it->max_idx + 1) { // 接触右边
            handle_put_right_of_comb(it, pos);
        } else if ((it->max_idx - pos) % 2 == 1) { // 在奇数距离的间隙
            handle_put_in_gap(it, pos);
        } else { // 在偶数距离的间隙 (即内部)
            handle_put_inside_comb(it, pos);
        }
    } 
    // Case 2: pos 在梳子之间,可能创造新梳子或与前后合并
    else {
        Iter next = it;
        Iter prev = it;
        --prev;
        int new_min = pos, new_max = pos;
        
        // 尝试与右边的梳子合并
        if (next->min_idx == pos + 2) {
            new_max = next->max_idx;
            erase_comb(next);
        }
        // 尝试与左边的梳子合并
        if (prev->max_idx == pos - 2) {
            new_min = prev->min_idx;
            erase_comb(prev);
        }
        insert_comb(Comb(new_min, new_max));
    }
}

// 移除 F_pos
void take(int pos) {
    Iter it = comb_set.lower_bound(Comb(0, pos));
    
    // Case 1: pos 就在梳子 it 里面
    if (it->min_idx <= pos && pos <= it->max_idx && (pos - it->min_idx) % 2 == 0) {
        int cur_min = it->min_idx, cur_max = it->max_idx;
        erase_comb(it);
        if (cur_min < pos) insert_comb(Comb(cur_min, pos - 2));
        if (pos < cur_max) insert_comb(Comb(pos + 2, cur_max));
    } 
    // Case 2: pos 在梳子外面,需要逆向操作
    else {
        // 这部分是 put 的逆操作,逻辑非常复杂,涉及多种情况的拆解
        if (pos < it->min_idx) {
            if ((it->min_idx - pos) % 2 == 1) { // TakeT1 logic
                int cur_min = it->min_idx, cur_max = it->max_idx;
                erase_comb(it);
                if (cur_min != cur_max) insert_comb(Comb(cur_min + 2, cur_max));
                if (pos <= cur_min - 3) insert_comb(Comb(pos + 2, cur_min - 1));
                put(pos == 1 ? 1 : pos - 1);
            } else { // TakeT2 logic
                int cur_min = it->min_idx, cur_max = it->max_idx;
                erase_comb(it);
                if (cur_min != cur_max) insert_comb(Comb(cur_min + 2, cur_max));
                if (pos <= cur_min - 4) insert_comb(Comb(pos + 3, cur_min - 1));
                put(pos + 1);
            }
        } else { // TakeT3 logic
            int cur_min = it->min_idx, cur_max = it->max_idx;
            erase_comb(it);
            if (pos <= cur_max - 3) insert_comb(Comb(pos + 3, cur_max));
            insert_comb(Comb(cur_min + 1, pos));
            if (cur_min != 1) put(cur_min == 2 ? 1 : cur_min - 2);
        }
    }
}

// --- 黑魔法区: a * F_b -> sum of +/- F_k ---
// 这个函数用于将 a * F_b 分解成一系列 +/- F_k 的和
// 它基于斐波那契数系的性质,非常复杂
int _coeff[810], *coeff = _coeff + 405;
int _temp_coeff[810], *temp_coeff = _temp_coeff + 405;
int mod_pos[95], mod_sign[95], mod_count;

void get_modifications(int a, int b) {
    if (!a) { mod_count = 0; return; }
    int sign = a < 0 ? -1 : 1;
    a = std::abs(a);
    
    int l = 0, r = 0;
    coeff[0] = 1;

    for (int j = 30 - __builtin_clz(a); j >= 0; --j) {
        int nl = l, nr = r;
        for (int i = l; i <= r; ++i) if (coeff[i]) {
            int k = i; while (coeff[k + 2]) k += 2;
            if (i == k) {
                int k2 = i; while (coeff[k2 + 3]) k2 += 3;
                for (int p = k2 + 1; p >= i; p -= 2) ++temp_coeff[p];
                if ((k2 - i) / 3 % 2 == 0) ++temp_coeff[i - 2];
                nr = std::max(nr, k2 + 1); nl = std::min(nl, i - 2);
                i = k2;
            } else {
                ++temp_coeff[k + 2], nr = std::max(nr, k + 2);
                ++temp_coeff[i - 2], nl = std::min(nl, i - 2);
                for (int p = i + 2; p <= k - 2; p += 2) ++temp_coeff[p];
                i = k;
            }
        }
        l = nl, r = nr;
        for (int i = l; i <= r; ++i) coeff[i] = temp_coeff[i], temp_coeff[i] = 0;
        while (l <=r && !coeff[l]) ++l; if(l>r) l=r=0;
        while (l <=r && !coeff[r]) --r; if(l>r) l=r=0;

        for (int i = l; i <= r; ++i) if (coeff[i] == 2) ++coeff[i + 1], ++coeff[i - 2], coeff[i] = 0;
        nl = l, nr = r;
        for (int i = r; i >= l; --i) if (coeff[i] && coeff[i + 1]) {
            int k = i + 1; while (coeff[k + 2]) k += 2;
            ++coeff[k + 1];
            for (int p = k; p >= i + 1; p -= 2) coeff[p] = 0;
            coeff[i] = 0;
            nr = std::max(nr, k + 1);
        }
        l = nl, r = nr;
        while (l <=r && !coeff[l]) ++l; if(l>r) l=r=0;
        while (l <=r && !coeff[r]) --r; if(l>r) l=r=0;

        if ((a >> j) & 1) {
            nl = l, nr = r;
            if (coeff[0]) {
                int k = 0; while (coeff[k - 2]) k -= 2;
                for (int p = 1; p >= k; --p) coeff[p] ^= 1;
                coeff[k - 2] = 1;
                nr = std::max(nr, 1); nl = std::min(nl, k - 2);
            } else coeff[0] = 1;
            l = nl, r = nr;
            for (int i = r; i >= l; --i) if (coeff[i] && coeff[i + 1]) {
                int k = i + 1; while (coeff[k + 2]) k += 2;
                ++coeff[k + 1];
                for (int p = k; p >= i + 1; p -= 2) coeff[p] = 0;
                coeff[i] = 0;
                nr = std::max(nr, k + 1);
            }
            l = nl, r = nr;
            while (l <=r && !coeff[l]) ++l; if(l>r) l=r=0;
            while (l <=r && !coeff[r]) --r; if(l>r) l=r=0;
        }
    }
    mod_count = 0;
    for (int i = l; i <= r; ++i) if (coeff[i]) {
        mod_count++;
        mod_pos[mod_count] = b + i;
        mod_sign[mod_count] = sign;
    }
    for (int i = l; i <= r; ++i) coeff[i] = 0;
}

int final_mod_pos[95], final_mod_sign[95], final_mod_count;
// F_{-k} = (-1)^{k-1} F_k 
void normalize_modification(int pos, int sign) {
    if (pos == -1) return;
    if (pos == 0 || pos == -2) pos = 1; // F_0 -> F_1, F_{-2} -> F_1
    if (pos < -2) {
        sign *= (pos & 1) ? -1 : 1;
        pos = -2 - pos;
    }
    final_mod_count++;
    final_mod_pos[final_mod_count] = pos;
    final_mod_sign[final_mod_count] = sign;
}


void solve() {
    total_partition_count = 0;
    comb_set.clear();
    comb_set.insert(Comb(INF, INF));
    comb_set.insert(Comb(-INF, -INF));

    int Q;
    read(Q);
    while (Q--) {
        int a, b;
        read(a), read(b);
        get_modifications(a, b);
        
        final_mod_count = 0;
        for (int i = 1; i <= mod_count; ++i) {
            normalize_modification(mod_pos[i], mod_sign[i]);
        }

        for (int i = 1; i <= final_mod_count; ++i) {
            if (final_mod_sign[i] == 1) put(final_mod_pos[i]);
        }
        for (int i = 1; i <= final_mod_count; ++i) {
            if (final_mod_sign[i] == -1) take(final_mod_pos[i]);
        }
        print(total_partition_count);
    }
}

int main() {
    std::ios_base::sync_with_stdio(false);
    std::cin.tie(NULL);
    int T;
    read(T);
    while (T--) {
        solve();
    }
    return 0;
}

复杂度分析

  • 时间复杂度: 每次操作 x += a * F_b,我们需要先调用 get_modifications(a, b)。这个函数的复杂度与 a 的二进制位数有关,大约是 O(loga)O(\log a) 乘以一些与斐波那契数系相关的多项式因子(但实际上很快)。它会生成 O(loga)O(\log a)±Fk\pm F_k 的操作。对于每一个 ±Fk\pm F_k,我们需要在 std::set 中进行查找和修改。set 的大小与 xx 的对数成正比,因为斐波那契数是指数增长的。所以每次 set 操作是 O(log(logx))O(\log (\log x))。因此,单次查询的总时间复杂度可以估算为 O(logalog(logx))O(\log a \cdot \log(\log x))。对于 QQ 次查询,总复杂度是 O(Qlogalog(logx))O(Q \cdot \log a \cdot \log(\log x))

  • 空间复杂度: 我们用 std::set 来存储梳子。梳子的数量级是 O(logx)O(\log x)。get_modifications 函数内部使用了一些辅助数组,但大小是固定的。所以空间复杂度是 O(logx)O(\log x)

知识点总结

这道题是数据结构、数论和算法设计思想的完美结合,喵~

  1. 斐波那契数系的表示: 核心思想是找到一种能唯一表示任意整数的、基于斐波那契数的结构。本题解中的“梳子表示法”就是这样一种强大的工具。
  2. 数据结构维护: std::set 在这里大放异彩。它能自动维护梳子的有序性,并允许我们高效地(对数时间)进行查找、插入和删除,这对于实现复杂的 puttake 操作至关重要。
  3. 摊还分析与递归更新: puttake 中的递归调用看起来可能很吓人,但实际上,每次操作(如合并或分裂)都会使表示更加“规范化”,总的计算量是可控的。
  4. 问题转化: 成功地将一个对大数的操作问题,转化为了对一个紧凑数据结构(梳子集合)的维护问题,是解题的关键。
  5. 化繁为简: 面对像 a * F_b 这样复杂的项,通过 get_modifications 函数将其分解为一系列更简单的基元操作 (±Fk)(\pm F_k),这是一种非常重要的算法思想。

希望这篇题解能帮到你,主人!如果还有不懂的地方,随时可以再来问这只我哦,喵~