题解:洛谷 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\))个独立的查询,每个查询属于以下两种类型之一:
- 在时刻 \(t\) 刚结束时,奶牛 \(c\) 在什么位置(\(0\le c\le t\le 10^{18}\))?
- 在时刻 \(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
【核心思想】
-
问题分析:有一个动态变化的队列,初始时刻 \(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}\),无法直接模拟,需利用规则发现周期性或分段线性性质来快速跳转。
-
算法选择:
- 模拟 + 批量跳跃:代码采用了直接模拟,但在每次迭代中利用数学关系进行“跳跃”,跳过中间大量不会改变状态的时间步。对于类型 1(正向),根据当前奶牛的位置
cur和当前时间now,计算下一次状态改变的时间并直接跳转;对于类型 2(逆向),从目标时刻向前反推,同样使用跳跃加速。 - 状态转移规律:仔细观察移动规则,可以发现奶牛的位置变化具有分段线性特征。例如,位置 0 的奶牛会跳到 \(\lfloor t/2 \rfloor\),而其他位置的奶牛会向前移动一格。这导致每头奶牛的位置在大部分时间内是固定的,只在特定时刻发生跳跃。利用这些“跳跃点”,可以批量跳过。
- 模拟 + 批量跳跃:代码采用了直接模拟,但在每次迭代中利用数学关系进行“跳跃”,跳过中间大量不会改变状态的时间步。对于类型 1(正向),根据当前奶牛的位置
-
关键步骤(基于代码逻辑):
- 类型 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 = t,cur_x = x开始,逆向执行规则直到cur_t == 0:- 若
cur_x == cur_t:该位置上是刚加入的牛,返回cur_t。 - 若
cur_t < 2*cur_x:无法继续反推,停止。 - 若
cur_x == cur_t / 2:这头牛是从位置 0 跳过来的,反推为cur_x = 0,cur_t--。 - 否则,利用公式
k = (cur_t - 2*cur_x) / 3计算一次可回退的步数,cur_x += k,cur_t -= k。 - 循环结束输出
cur_x。
- 若
- 类型 1(正向查询):给定起始奶牛编号
-
时间/空间复杂度:
- 时间复杂度:每次查询的循环次数近似 \(O(\log t)\),因为跳跃步数通常较大。
- 空间复杂度:\(O(1)\)。
-
模拟 + 批量跳跃的核心思想:
- 发现不变期:在大多数时间内,某头奶牛的位置并不发生变化,只有特定时刻会跳跃。通过分析规则,找到这些跳跃时刻的公式,从而一次性跳过。
- 正向与逆向对称:类型 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
浙公网安备 33010602011771号