前言
CCPC 2021 Online E - Monopoly 要回答一个无限循环过程中的最早到达时间。直接模拟无法确定要走多少圈,有限的部分只有一圈中的位置;每走完一圈,分数都会增加同一个周期和。
题目
地图上有首尾相接的 n 个整数 a1,a2,…,an。初始分数为 0,第一步走到 a1,以后按顺序循环移动,每一步把当前位置的数加入分数。
每个询问给出目标分数 x。需要输出第一次恰好得到 x 时的步数;如果无法到达则输出 −1。初始状态已经得到 0 分,所以 x=0 的答案为 0。
单个测试中 n,m≤105,全部测试的 n 与 m 之和分别不超过 5×105。目标分数的绝对值可以达到 1012,不能按步数或周期数枚举。
周期中的位置
定义第一圈的前缀和
P0=0,Pi=j=1∑iaj(1≤i≤n),
一整圈带来的分数变化为
S=Pn.
实现中,pre[i] 保存周期内前缀 Pi(0≤i<n),s 保存一个完整周期的总和 S。
任意时刻都可以唯一写成
t=q⋅n+i,q≥0,0≤i<n.
其中 q 是完整走过的圈数,i 是当前一圈中已经走过的步数。此时分数为
q⋅S+Pi.
只保留 P0 到 Pn−1。Pn=S 与下一圈开头的 P0 表示同一个周期边界,若两者同时保留,同一时刻会有两种表示。
对询问 x,问题变成寻找满足
x=q⋅S+Pi,q≥0
的周期内位置 i,并最小化 q⋅n+i。
周期和为零
若 S=0,完整走一圈不会改变分数:
q⋅S+Pi=Pi.
所有可达分数都已经出现在第一圈。对于每个前缀和,只记录它最早出现的步数;询问时在哈希表中查找即可。初始前缀 P0=0 对应步数 0。
同一个前缀和以后再次出现时步数更大,不会成为答案。
这张“前缀和到最早位置”的映射由 f1st 保存。建表时只在前缀和第一次出现时写入,solve1 查询到的自然就是最早步数:
1 2 3
| if (!f1st.count(s)) { f1st[s] = i; }
|
按同余分组
下面考虑 S=0,令
d=∣S∣.
完整周期带来的变化是 d 的整数倍。一个周期内前缀 Pi 能产生目标 x,必要条件为
Pi≡x(modd).
把前缀按模 d 的非负余数分组。固定一组余数 r,可以写成
0≤r<d,Pi=r+ki⋅d,x=r+h⋅d,ki,h∈Z.
同余条件保证所需圈数是整数,剩下的限制是 q≥0。周期和的正负决定了合法前缀位于排序后数组的哪一侧。
实现时,mod 把负余数统一到 [0,d),same_rem 按余数保存候选前缀。每个候选的第一项是 Pi,第二项 val 在下面两个分支中分别保存对应的步数补偿项。
周期和为正
此时 S=d。代入
x=Pi+q⋅S
可得
q=h−ki.
合法条件为
q≥0⟺ki≤h⟺Pi≤x.
对应步数为
q⋅n+i=(h−ki)⋅n+i=h⋅n+(i−ki⋅n).
对于固定询问,h⋅n 不变。将同余组内的前缀按 Pi 升序排列,并对
i−ki⋅n
维护前缀最小值。查询时用二分找到最后一个不大于 x 的前缀,取该位置以前的最小值即可。
对应到实现,val = i - k * n 保存 i−ki⋅n,best 保存按 Pi 排序后的前缀最小值。upper_bound 定位最后一个满足 Pi≤x 的位置,答案写成 h * n + best[rem][idx]。若组内最小的前缀都大于 x,则不存在合法的非负圈数。
周期和为负
此时 S=−d,等式变为
x=Pi−q⋅d.
因此
q=ki−h,
合法条件为
q≥0⟺ki≥h⟺Pi≥x.
步数可以写成
q⋅n+i=(ki−h)⋅n+i=−h⋅n+(i+ki⋅n).
同余组仍按 Pi 升序排列,但这次合法候选构成一个后缀。对
i+ki⋅n
维护后缀最小值,查询时用 lower_bound 找到第一个不小于 x 的前缀,再读取对应的后缀最小值。
这里的补偿项对应 val = i + k * n,best 保存后缀最小值,最终答案为 -h * n + best[rem][idx]。正负两种情况共用同余分组,只改变合法区间方向和维护的最小值方向。
若组内最大的前缀仍小于 x,目标同样不可达。
实现边界
C++ 的负数取模可能得到负余数。分组时统一使用
((vmodd)+d)modd
把余数放到 [0,d),负前缀和负询问才能进入同一组。
同一个前缀值可以出现多次,不需要去重。前缀或后缀最小值会自动保留步数更早的候选。
查询 x=0 时直接输出 0。后续即使再次回到零分,也不可能早于初始状态。
前缀和、询问、商和步数表达式都可能超过 int 范围,统一使用 long long。
复杂度
当 S=0 时,建立最早前缀位置的哈希表平均需要 O(n),每个询问平均为 O(1)。
当 S=0 时,所有分组的排序合计为 O(nlogn),构造最小值数组还需要 O(n)。一次询问先通过哈希表选出同余组,再在组内二分合法区间,因此总时间复杂度平均为
O(nlogn+mlogn),
空间复杂度为 O(n)。
AC 代码
点击展开/折叠 最终 AC 代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127
| #include <bits/stdc++.h> using namespace std; using ll = long long; constexpr int maxn = 1e5 + 5;
int t;
ll mod(ll a, ll b) { return (a % b + b) % b; }
void solve1(int m, unordered_map<ll, int> &f1st) { f1st[0] = 0; for (int i = 1; i <= m; i++) { ll x; cin >> x; if (f1st.count(x)) { cout << f1st[x] << '\n'; } else { cout << -1 << '\n'; } } }
void solve2(int n, int m, ll s, vector<ll> &pre, unordered_map<ll, vector<pair<ll, ll> > > &same_rem) { for (int i = 0; i < n; i++) { ll rem = mod(pre[i], abs(s)); ll k = (pre[i] - rem) / abs(s); ll val; if (s > 0) { val = i - k * n; } else { val = i + k * n; } same_rem[rem].push_back({pre[i], val}); } unordered_map<ll, vector<ll> > best; for (auto &um: same_rem) { ll rem = um.first; sort(um.second.begin(), um.second.end()); int size = um.second.size(); best[rem] = vector<ll>(size); if (s > 0) { best[rem][0] = um.second[0].second; for (int i = 1; i < size; i++) { best[rem][i] = min(best[rem][i - 1], um.second[i].second); } } else { best[rem][size - 1] = um.second[size - 1].second; for (int i = size - 2; i >= 0; i--) { best[rem][i] = min(best[rem][i + 1], um.second[i].second); } } } for (int i = 1; i <= m; i++) { ll x; cin >> x; if (!x) { cout << 0 << '\n'; continue; } ll d = abs(s); ll rem = mod(x, d); if (!same_rem.count(rem)) { cout << -1 << '\n'; continue; } auto &v = same_rem[rem]; if (s > 0) { int idx = upper_bound(v.begin(), v.end(), x, [](ll val, const auto &p) { return val < p.first; }) - v.begin() - 1; if (idx < 0) { cout << -1 << '\n'; continue; } ll h = (x - rem) / s; cout << h * n + best[rem][idx] << '\n'; } else { int idx = lower_bound(v.begin(), v.end(), x, [](const auto &p, ll val) { return p.first < val; }) - v.begin(); if (idx == (int) v.size()) { cout << -1 << '\n'; continue; } ll h = (x - rem) / d; cout << -h * n + best[rem][idx] << '\n'; } } }
void op() { int n, m; cin >> n >> m; ll s = 0; vector<int> a(n + 5); vector<ll> pre(n + 5, 0); unordered_map<ll, int> f1st; unordered_map<ll, vector<pair<ll, ll> > > same_rem; for (int i = 1; i <= n; i++) { cin >> a[i]; s += a[i]; if (!f1st.count(s)) { f1st[s] = i; } if (i != n) { pre[i] = s; } } if (!s) { solve1(m, f1st); } else { pre[0] = 0; solve2(n, m, s, pre, same_rem); } }
int main() { ios::sync_with_stdio(false); cin.tie(nullptr); cin >> t; while (t--) { op(); } return 0; }
|