[CSP-J 2023] 小苹果

GitHub跳转原题关系图返回列表

利用每天剩余苹果数和最后一个苹果当前位置都按 floor(2x/3) 递推,直接在 O(log n) 内求出总天数与删除时刻。

OJ: luogu

题目 ID: P9748

难度:普及-

标签:模拟推导cspj

日期: 2026-06-18 21:39

题意

n 个苹果排成一列。每天会删除当前序列中位置为 1,4,7,... 的苹果,然后把剩余苹果重新按原顺序排成一列。

要求输出两件事:

  • 全部苹果被删完需要多少天。
  • 编号为 n 的苹果在哪一天被删掉。

思路

先看一个可以直接验证过程的朴素模拟:

cpp
#include <bits/stdc++.h>
using namespace std;

int main() {
    ios::sync_with_stdio(false);
    cin.tie(nullptr);

    int n;
    cin >> n;

    vector<int> apples;
    for (int i = 1; i <= n; ++i) {
        apples.push_back(i);
    }

    int day = 0;
    int removed_day = 0;
    while (!apples.empty()) {
        ++day;
        vector<int> next_apples;
        for (int i = 0; i < (int)apples.size(); ++i) {
            int pos = i + 1;
            if (pos % 3 == 1) {
                if (apples[i] == n) {
                    removed_day = day;
                }
            } else {
                next_apples.push_back(apples[i]);
            }
        }
        apples = next_apples;
    }

    cout << day << ' ' << removed_day << '\n';
    return 0;
}

朴素做法会真的维护还活着的苹果编号,但 n 最多到 10910^9,显然不能这么做。

关键观察是:如果当前还剩 m 个苹果,那么当天删掉的位置是 1,4,7,...,删掉的个数是 ceil(m/3)ceil(m/3),所以第二天会剩下 floor(2m/3)floor(2m/3) 个苹果。

同理,若某个苹果当前位于第 p 个位置:

  • pp % 3 == 1,说明它今天就会被删掉;
  • 否则它会保留下来,并在新队列中跑到第 floor(2p/3)floor(2p/3) 个位置。

于是我们分别做两个递推:

  1. rest=floor(2rest/3)rest = floor(2rest/3) 不断压缩,统计删空需要多少轮。
  2. pos 追踪编号为 n 的那个苹果当前排在第几位,直到它第一次满足 pospos % 3 == 1

这样就把大规模删除过程压成了两个 O(logn)O(log n) 的循环。

代码

cpp
#include <bits/stdc++.h>
using namespace std;

int main() {
    ios::sync_with_stdio(false);
    cin.tie(nullptr);

    long long n;
    cin >> n;

    long long rest = n;
    long long total_days = 0;
    while (rest > 0) {
        ++total_days;
        // 每天拿走当前位置为 1,4,7,... 的苹果,剩下 floor(2*rest/3) 个。
        rest = rest * 2 / 3;
    }

    long long pos = n;
    long long removed_day = 0;
    while (pos > 0) {
        ++removed_day;
        if (pos % 3 == 1) {
            break;
        }
        // 原来的第 pos 个苹果没被拿走,删除它左边若干苹果后位置变小。
        pos = pos * 2 / 3;
    }

    cout << total_days << ' ' << removed_day << '\n';
    return 0;
}

复杂度

时间复杂度是 O(logn)O(log n),空间复杂度是 O(1)O(1)

总结

这题表面上是反复删数组,真正有用的是“数量如何变化”和“目标元素的位置如何变化”。只抓住这两个递推,就能把过程直接算出来。