## 王国边缘 #模拟 #倍增 #二分 首先如果坐标都是在模 $n$ 意义下的话,这 $n$ 个位置的下一步走法是固定的。这个可以 $\mathcal O(n)$ 或 $\mathcal O(n\log_2 n)$ 处理出来。(注意对 $m\le n$ 和 $m>n$ 的特别处理。我为了方便搞了 $2n$ 长的,但是当 $m>n$ 时循环了需要特殊处理) 接下来我就开始发癫了。我想得是没错的,从一个地方走了若干步之后一定会到环上并且不会出来,所以维护环就行了。但是事情并没有这么简单。 因为我们不止需要知道模 $n$ 意义下的位置,还需要知道实际的位置。这个实际位置有很多可能,我们不知道,所以需要记录走到下一步所进行的位移。于是我们就需要维护环上的位移前缀和、环周围的链的前缀和……非常复杂,而且讨论很多。 最后发现我是大煞笔。像这种走若干步的问题,直接倍增就行了。$\mathcal O(n\log_2 V)$ 预处理出倍增数组,然后每次走 $2^k$ 步,最后拼出询问的 $K$ 即可。时间复杂度 $\mathcal O((n+q)\log_2 V)$。 需要注意取模部分,因为输入的就有 $10^{18}$ 了。 ```cpp const LL kMaxN = 2e5 + 5, kLog = 63, kMod = 1e9 + 7; // 走 2^k 步能到达的 (mod n) 的地方和 Δx LL nxt[kLog][kMaxN], val[kLog][kMaxN], n, m, q; vector<LL> v; string a; int main() { cin.tie(0)->sync_with_stdio(0); cin >> n >> m >> q >> a; a = " " + a + a; for (LL i = 1; i <= 2 * n; i++) { if (a[i] == '1') { v.push_back(i); } } LL zrr = m / n, wyy = m % n; for (LL i = 1; i <= n; i++) { if (v.empty()) { nxt[0][i] = i % n + 1; val[0][i] = 1; continue; } if (m <= n) { auto p = upper_bound(v.begin(), v.end(), i + m); if (p == v.begin()) { nxt[0][i] = i % n + 1; val[0][i] = 1; continue; } LL x = *prev(p); if (x > i) { nxt[0][i] = (x - 1) % n + 1; val[0][i] = x - i; } else { nxt[0][i] = i % n + 1; val[0][i] = 1; } } else { auto p = upper_bound(v.begin(), v.end(), i + wyy); // 要么在 [1, i+wyy] 当中,如果没有必然 i+wyy<n 且在 (i+wyy,n] 中 if (p == v.begin()) { p = upper_bound(v.begin(), v.end(), n); LL x = *prev(p); nxt[0][i] = x; val[0][i] = (zrr - 1) * n + x - i; continue; } LL x = *prev(p); nxt[0][i] = (x - 1) % n + 1; val[0][i] = zrr * n + x - i; } val[0][i] %= kMod; } for (LL i = 1; i < kLog; i++) { for (LL j = 1; j <= n; j++) { nxt[i][j] = nxt[i - 1][nxt[i - 1][j]]; val[i][j] = (val[i - 1][j] + val[i - 1][nxt[i - 1][j]]) % kMod; } } for (LL x, k; q; q--) { cin >> x >> k; LL ans = x % kMod; x = (x - 1) % n + 1; for (LL i = 0; i < kLog; i++) { if (k >> i & 1) { ans = (ans + val[i][x]) % kMod; x = nxt[i][x]; } } cout << ans << '\n'; } return 0; } ``` ## 买东西题 #贪心 #优先队列 我最开始写了一个线段树的做法。 具体来说,我做个贪心,把折扣优惠 $v_i$ 更多的排在前面,然后用线段树维护下标 $a$ 所对应的 $a-b$ 的区间最小值。按照 $v_i$ 从大到小的顺序枚举每张优惠券,在所有 $a$ 大于等于 $w_i$ 的商品中取出 $a-b$ 最小的买掉(这样实际收益更多),然后把它从线段树当中删除。其它的用 $b_i$ 买。 这个算法不出意料地在大样例上面 TLE 了。$v_i$ 越大的放前面不一定好,因为 $w_i$ 的限制,如果前一个限制更小,然后把 $a$ 小的夺走了;后面一个 $v$ 小一些,但是它限制大,被前面的夺走了。这样子本来这两张优惠券全部可以用上的,结果最后只能用上一张,答案就不对。 既然我们没办法一次就找出哪张优惠券对应哪个商品,所以回归老本行:反悔贪心。 首先,对于一个新物品,它有三种选择策略: 1. 不使用优惠券用折扣价买; 2. 把之前的一个物品的优惠券抢过来用,再给被抢的补一张(相当于新用一个然后交换); 3. 把之前的一个物品的优惠券抢过来用,但不补偿。 第二个操作最长,但是我们实际上只需要按照 $a$ 从小到大的顺序排序处理就不会用到第二种情况了。因为在交换前后都是合法的情况下,交换前后的收益是一样大的。 所以我们只需要考虑第三种情况就行了。反悔贪心的本质就是把第三种情况变到第一种情况去。由于我们是按照 $a$ 排序的,所以 $i$ 抢 $j$ $(j<i)$ 是一定符合条件的。 好的。假设 $i$ 抢了 $j$ $(i>j)$,抢的优惠券优惠力度为 $v$(由于 $a$ 递增所以抢过来合法)。那么抢之前的收益: $ a_i-v+b_j $ 抢之后的收益 $ b_i+a_j-v $ 我们要把抢的过程看作用了一个新的优惠券,那么这个优惠券具体是多少的优惠力度呢?设为 $x$,有 $ (a_i-v)+(a_j-x)=b_i+a_j-v $ 所以 $x=a_i-b_i$。 代码就很简单了。由于 $a$ 单调递增,我们给优惠券的限制 $w$ 也排个序,那么可用的优惠券在序列中就是一个单调边长的前缀。优惠券用优先队列(大根堆)维护(因为维护的都是可以作用的,所以只需要优惠力度大的放前面即可)。 时间复杂度 $\mathcal O(n\log_2 n)$ $(\mathcal O(n)=\mathcal O(m))$。 ```cpp const LL kMaxN = 1e6 + 5; LL n, m, ans; pair<LL, LL> a[kMaxN], b[kMaxN]; priority_queue<LL> q; int main() { cin.tie(0)->sync_with_stdio(0); cin >> n >> m; for (LL i = 1; i <= n; i++) { cin >> a[i].first >> a[i].second; } sort(a + 1, a + n + 1); for (LL i = 1; i <= m; i++) { cin >> b[i].first >> b[i].second; } sort(b + 1, b + m + 1); for (LL i = 1, j = 1; i <= n; i++) { for (; j <= m && b[j].first <= a[i].first; j++) { q.push(b[j].second); } if (q.size() && q.top() > a[i].first - a[i].second) { ans += a[i].first - q.top(); q.pop(); q.push(a[i].first - a[i].second); } else { ans += a[i].second; } } cout << ans << '\n'; return 0; } ``` ## IMAWANOKIWA (Construction ver.) 不会写,暴力都没打。$0$ 分。 ## 魔法少女们 不会写,暴力都没打。$0$ 分。