题解:洛谷 P14977 Lineup Queries

【题目来源】

洛谷:P14977 [USACO26JAN1] Lineup Queries S - 洛谷

【题目描述】

有一队奶牛,初始时(即时刻 \(t = 0\))只有奶牛 \(0\) 在位置 \(0\)(这里,一头奶牛在位置 \(k\) 表示它前面有 \(k\) 头奶牛)。在时刻 \(t\)\(t=1,2,3,\dots\)),位置 \(0\) 的奶牛移动到位置 \(\lfloor t/2\rfloor\),位置 \(1\dots \lfloor t/2\rfloor\) 上的每头奶牛向前移动一个位置,并且奶牛 \(t\) 加入到队列的末尾(位置 \(t\))。

回答 \(Q\)\(1\le Q\le 10^5\))个独立的查询,每个查询属于以下两种类型之一:

  1. 在时刻 \(t\) 刚结束时,奶牛 \(c\) 在什么位置(\(0\le c\le t\le 10^{18}\))?
  2. 在时刻 \(t\) 刚结束时,位置 \(x\) 上是哪头奶牛(\(0\le x\le t\le 10^{18}\))?

【输入】

第一行包含 \(Q\),表示查询的数量。

接下来的 \(Q\) 行,每行包含三个整数,指定一个查询,格式为 "\(1\) \(c\) \(t\)" 或 "\(2\) \(x\) \(t\)"。

【输出】

将每个查询的答案输出在单独一行。

【输入样例】

2
1 4 9
2 2 9

【输出样例】

2
4

【核心思想】

  1. 问题分析:有一个动态变化的队列,初始时刻 \(t=0\) 时,奶牛 \(0\) 在位置 \(0\)。在每一时刻 \(t \ge 1\),发生以下操作:

    • 位置 \(0\) 的奶牛移动到位置 \(\lfloor t/2 \rfloor\)
    • 位置 \(1\)\(\lfloor t/2 \rfloor\) 的每头奶牛向前(向位置更小的方向)移动一个位置;
    • 奶牛 \(t\) 加入到队列末尾(位置 \(t\))。
      需要回答 \(Q\) 个独立查询(\(Q \le 10^5\)),类型 1:给定奶牛编号 \(c\) 和时间 \(t\),求该奶牛在时刻 \(t\) 刚结束时的位置;类型 2:给定位置 \(x\) 和时间 \(t\),求该位置上奶牛的编号。\(t\) 可达 \(10^{18}\),无法直接模拟,需利用规则发现周期性或分段线性性质来快速跳转。
  2. 算法选择

    • 模拟 + 批量跳跃:代码采用了直接模拟,但在每次迭代中利用数学关系进行“跳跃”,跳过中间大量不会改变状态的时间步。对于类型 1(正向),根据当前奶牛的位置 cur 和当前时间 now,计算下一次状态改变的时间并直接跳转;对于类型 2(逆向),从目标时刻向前反推,同样使用跳跃加速。
    • 状态转移规律:仔细观察移动规则,可以发现奶牛的位置变化具有分段线性特征。例如,位置 0 的奶牛会跳到 \(\lfloor t/2 \rfloor\),而其他位置的奶牛会向前移动一格。这导致每头奶牛的位置在大部分时间内是固定的,只在特定时刻发生跳跃。利用这些“跳跃点”,可以批量跳过。
  3. 关键步骤(基于代码逻辑):

    • 类型 1(正向查询):给定起始奶牛编号 c,当前时间 now = c(奶牛 \(c\) 在时刻 \(c\) 加入队伍),当前位置 cur = c。循环直到 now == t
      • cur == 0:奶牛在位置 0,下一时刻它会被移动到 \(\lfloor (now+1)/2 \rfloor\),因此时间加 1,cur = (now+1)/2
      • cur > (now+1)/2:奶牛位置大于当前时间的一半,它会持续向前移动(每次减 1),直到位置变为 0 或达到某个边界。通过计算目标时间 target = 2*cur - 1,一次性跳过 jump = target - now 个时间步。
      • 否则 cur <= (now+1)/2:奶牛位置较小,它会保持位置不变一段时间,直到新加入的牛从右边靠近它。跳跃量为 cur
      • 循环结束后输出 cur
    • 类型 2(逆向查询):给定位置 x 和时间 t,尝试反推。从 cur_t = tcur_x = x 开始,逆向执行规则直到 cur_t == 0
      • cur_x == cur_t:该位置上是刚加入的牛,返回 cur_t
      • cur_t < 2*cur_x:无法继续反推,停止。
      • cur_x == cur_t / 2:这头牛是从位置 0 跳过来的,反推为 cur_x = 0cur_t--
      • 否则,利用公式 k = (cur_t - 2*cur_x) / 3 计算一次可回退的步数,cur_x += kcur_t -= k
      • 循环结束输出 cur_x
  4. 时间/空间复杂度

    • 时间复杂度:每次查询的循环次数近似 \(O(\log t)\),因为跳跃步数通常较大。
    • 空间复杂度:\(O(1)\)
  5. 模拟 + 批量跳跃的核心思想

    • 发现不变期:在大多数时间内,某头奶牛的位置并不发生变化,只有特定时刻会跳跃。通过分析规则,找到这些跳跃时刻的公式,从而一次性跳过。
    • 正向与逆向对称:类型 2 查询通过逆向模拟,利用相同的跳跃技术从目标时刻反推回初始时刻。
    • 适用场景:适用于动态系统,状态更新规则具有分段线性或周期性的特点,且查询时间极大,需要利用数学性质进行快速跳转的模拟问题。

【解题思路】

【算法标签】

普及+ #模拟

【代码详解】

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

#define int long long  // 将int定义为long long类型,避免大数溢出

int q, type, a, t;  // q: 查询次数,type: 操作类型,a: 初始值,t: 时间/目标值

signed main()  // 使用signed main()替代int main(),因为int被重定义为long long
{
    cin >> q;  // 读入查询次数

    while (q--)  // 处理每个查询
    {
        cin >> type >> a >> t;  // 读入操作类型、初始值和时间参数

        if (type == 1)  // 类型1:正向模拟过程
        {
            int c = a;        // 当前值
            int cur = c;      // 当前状态变量
            int now = c;      // 当前时间

            // 模拟从时间now到时间t的过程
            while (now < t)
            {
                if (cur == 0)  // 如果当前状态为0
                {
                    now++;     // 时间推进1单位
                    cur = now / 2;  // 状态更新为当前时间的一半
                }
                else  // 当前状态不为0
                {
                    int limit = (now + 1) / 2;  // 计算当前时间的上限

                    if (cur > limit)  // 如果当前状态超过上限
                    {
                        // 计算目标状态和时间
                        int target = 2 * cur - 1;
                        int jump = target - now;  // 需要跳跃的时间

                        // 如果跳跃会超过目标时间t,则调整跳跃量
                        if (now + jump > t)
                        {
                            jump = t - now;
                        }

                        now += jump;  // 时间跳跃
                    }
                    else  // 当前状态未超过上限
                    {
                        int jump = cur;  // 跳跃量为当前状态值

                        // 如果跳跃会超过目标时间t,则调整跳跃量
                        if (now + jump > t)
                        {
                            jump = t - now;
                        }

                        cur -= jump;  // 状态减少
                        now += jump;  // 时间推进
                    }
                }
            }

            cout << cur << endl;  // 输出最终状态
        }
        else  // 类型2:逆向推导过程
        {
            int x = a;        // 目标状态
            int cur_t = t;    // 当前时间
            int cur_x = x;    // 当前状态

            // 逆向推导,从时间t向回推导
            while (cur_t > 0)
            {
                if (cur_x == cur_t)  // 特殊情况:状态等于时间
                {
                    cur_x = cur_t;
                    break;
                }

                // 如果当前时间不足以支持当前状态
                if (cur_t < 2 * cur_x)
                {
                    break;  // 停止推导
                }

                if (cur_x == cur_t / 2)  // 特殊规则:状态等于时间的一半
                {
                    cur_x = 0;   // 状态归0
                    cur_t--;     // 时间回退1单位
                }
                else  // 一般情况
                {
                    // 计算回退步数k
                    int k = (cur_t - 2 * cur_x) / 3;
                    if (k == 0)  // 如果计算为0,则至少回退1步
                    {
                        k = 1;
                    }

                    cur_x += k;  // 状态增加
                    cur_t -= k;  // 时间回退
                }
            }

            cout << cur_x << endl;  // 输出推导得到的初始状态
        }
    }

    return 0;
}

【运行结果】

2
1 4 9
2
2 2 9
4
posted @ 2026-08-29 09:37  团爸讲算法  阅读(10)  评论(0)    收藏  举报