yLOI2020 题解

· · 个人记录

A

本题来源于江苏一道模拟题。原题是给定序列 c_i = \frac 1 i,求有多少对 s,t 满足 c_3, c_s, c_t 构成等差数列。

16 个点可以随便乱搞,考察选手推式子的能力。

值得注意的是,如果 b \gt c 并且 d = 1,那么式子显然无解。因此,作为一道签到题,输出一排 0 可以得到 25 分的好成绩。

本题大概唯一的难点在于看到正整数直接考虑数论做法然后误入歧途。经过提示排除,我们直接推式子。

\frac a x + \frac b c = \frac d y \Rightarrow \frac{ac + bx}{cx} = \frac d y \Rightarrow dcx = acy + bxy \Rightarrow dcx - bxy = acy \Rightarrow x(dc - by) = acy

之所以会有 dcx = acy + bxy \Rightarrow dcx - bxy=acy 这一步是我们考虑题目限制看起来像是个枚举条件,而我们要进行枚举需要构造出一个减法的形式来,才能确定枚举的范围,因此考虑项,加法变减法。

注意到变量都是正整数,因此 acy 是正整数,x 是正整数,因此 (dc - by) 是正整数。因此 by \lt dc,即 y \lt \frac{dc} b。由此确定了 y 的枚举范围,枚举即可。时间复杂度 O(dc)

#include <iostream>

typedef long long int ll;

ll a, b, c, d, T;

int main() {
  for (std::cin >> T; T; --T) {
    std::cin >> a >> b >> c >> d;
    int ans = 0;
    for (ll lim = c * d / b, y = 1; y <= lim; ++y) {
      ll u = a * c * y, v = c * d - b * y;
      if (v <= 0) continue;
      if ((u % v) == 0) ++ans;
    }
    std::cout << ans << std::endl;
  }
  return 0;
}

B

这是校内模拟赛的 Day1A

Algorithm 1

考虑枚举初始属性。显然初始属性不会高于需求的最大值,然后用全排列爆搜每种穿装备顺序。时间复杂度 O(n! \times a \times b )。可以通过前三个子任务,期望得分 30 分。

Algorithm 2

注意到因为要先最小化第一个再最小化第二个,所以两个属性实际上是独立的。也即我们可以不考虑第二个属性,先通过枚举确定第一个属性的最小初始值是多少。然后在此基础上通过枚举确定第二个属性的最小初始值。穿装备的顺序同样可以全排列爆搜。这个算法与第一个算法的区别是,将 ab 分开枚举,而不是乘起来。时间复杂度 O(n! \times \max(a, b))。可以通过前四个子任务,期望得分 40 分。

Algorithm 3

从算法 1 的基础上扩展,考虑去掉全排列枚举的部分。我们可以把所有的装备按照 a 值排序。人物的第一个属性增加时,把所有的 a 值不大于当前属性的装备用一个小根堆维护。这个小根堆以 b 为关键字。人物的第二个属性增加时,把堆中所有 b 不大于当前属性的装备取出并穿戴。也即堆里维护的是当前第一个属性符合要求但是第二个属性不符合要求的装备。

枚举初始属性后,每次需要 O(n \log n) 的时间 check 是否合法。总时间复杂度 O(a \times b \times n\log n)。可以通过子任务 1,2,3,5,期望得分 40 分。

Algorithm 4

把算法 2 同样用算法 3 的方式替换掉全排列的部分,时间复杂度 O(\max(a, b) \times n \log n),可以通过前六个子任务,期望得分 60 分。

Algorithm 5

#### Algorithm 6 把装备按照 $a$ 值排序,check 时扫一遍装备就能确定是否合法了。也就是把算法 3 的前半部分拿过来替换算法五的全排列。时间复杂度 $O(n \log n \log a)$,可以通过子任务 7,8,期望得分 20 分。结合算法 4 可以得到 80 分。 #### Algorithm 7 通过算法 3 的结论,两个属性是独立的。因此可以用算法 5 的方式先二分确定第一个值,然后再二分第二个值,全排列 check 即可。时间复杂度 $O(n! \log\max(a, b))$,可以通过子任务 1,2,3,4,7,9,期望得分 60 分。 #### Algorithm 8 如果选手没有发现两个属性相互独立,但是会使用算法 3 的方式维护装备,写二分套二分内层 check 可以得到一个 $O(n \log n \log a \log b)$ 的算法。可以得到 90 分。如果大力卡常,有可能得到 100 分。 #### Algorithm 9 把算法 7 中的全排列 check 用算法 3 中的堆 check 替换,则得到了一个时间复杂度为 $O(n \log n \log\max(a,b))$ 的算法,可以通过全部的测试点,期望得分 100 分。 #### Algorithm 10 事实上可以发现 $a$ 是不需要二分的。把装备按照 $a$ 排序以后,维护一个数组 $t_i$ 表示穿戴完前 $i$ 件装备以后的 $a$ 值最小达到了多少。再对 $a$ 维护一个前缀和 $s_i$。显然 $t_n - s_n$ 就是初始的第一个属性值。在从 $i$ 转移到 $i + 1$ 的过程中,如果 $t_i \geq a_{i + 1}$,那么 $t_{i + 1} = t_i + c_{i + 1}$,否则 $t_{i + 1} = a_{i + 1} + c_i$。时间复杂度为 $O(n \log n \log b)$。可以通过全部的测试点,期望得分 100 分。std 写的是算法 9。 #### Sunmary - 本题弱智特判分 10 分,暴力分 30 分。 - 选手想到二分答案可以得到 50 分。 - 选手发现两个属性相互独立的性质可以得到 50 分。 - 选手会用堆维护可选装备可以得到 50 分。 - 上述三个主要 idea 相互组合,可以得到 $60 \sim 100$ 不等的分数。 - 本题在代码实现上并不困难,std 在 70 行左右,使用堆维护装备需要一点技巧,可以参考 std。 - 总的来说,本题部分分充足,样例良心,题意清晰,码量友好,可以较好的区分弱智选手,暴力选手,基本功扎实的选手,善于发现性质的选手和能灵活运用数据结构的选手。是一道优秀的签到题。 ```cpp #include <cstdio> #include <queue> #include <algorithm> const int maxn = 100005; int T; int n, ma, mb, fusu, zxy; int a[maxn], b[maxn], c[maxn], d[maxn], tmp[maxn]; bool check1(long long x); bool check2(long long x); inline bool cmp(const int x, const int y) { return a[x] < a[y]; } int main() { // freopen("forever7-1.in", "r", stdin); // freopen("forever.out", "w", stdout); scanf("%d%d", &T, &n); for (int i = 1; i <= n; ++i) { scanf("%d%d%d%d", a + i, b + i, c + i, d + i); ma = std::max(ma, a[i]); mb = std::max(mb, b[i]); tmp[i] = i; } std::sort(tmp + 1, tmp + 1 + n, cmp); for (int l = 0, r = ma, mid = (l + r) >> 1; l <= r; mid = (l + r) >> 1) if (check1(mid)) { fusu = mid; r = mid - 1; } else { l = mid + 1; } for (int l = 0, r = mb, mid = (l + r) >> 1; l <= r; mid = (l + r) >> 1) if (check2(mid)) { zxy = mid; r = mid - 1; } else { l = mid + 1; } printf("%d %d\n", fusu, zxy); } struct Cmp { inline bool operator()(const int x, const int y) { return b[x] > b[y]; } }; bool check2(long long x) { std::priority_queue<int, std::vector<int>, Cmp> Q; int i = 1, j; long long y = fusu; while (true) { while ((i <= n) && (a[tmp[i]] <= y)) Q.push(tmp[i++]); if (Q.empty()) { return i > n; } bool flag = false; while ((!Q.empty()) && (b[j = Q.top()] <= x)) { flag = true; Q.pop(); y += c[j]; x += d[j]; } if (flag == false) return false; } } bool check1(long long x) { int i = 1; while (x >= a[tmp[i]]) { x += c[tmp[i]]; if (++i > n) return true; } return false; } ``` ### C 这是校内模拟赛的 day2B。 (事实上,因为 day1 得分情况不理想,所以 day2 出的比 day1 简单,这道题是原定的 day2A) #### Algorithm 1 依照题意模拟,时间复杂度 $O(qn^3)$,可以通过子任务 $1$,期望得分 $10$ 分。 #### Algorithm 2 优化算法 1 的枚举过程,求区间异或和可以和枚举右端点放在一起完成,时间复杂度 $O(qn^2)$,可以通过前两个子任务,期望得分 $20$ 分。 #### Algorithm 3 考虑这个修改方式明示我们求前缀异或和。因为异或的性质,这样的修改方式相当于对前缀异或和数组 $s_i$ 进行对 $s_p$ 的单点修改。原序列中一段异或和为 $0$ 的区间等价于前缀异或和序列上一对前缀异或和相等的端点。考虑每次查询时扫描整个前缀异或和序列,用桶记录每个数出现的次数,那么答案即为 $\sum\limits_{i} (b_i \times b_{i} - 1)$,其中 $b_i$ 表示 $i$ 出现的次数。可以通过前三个子任务,期望得分 $40$ 分。 #### Algorithm 4 这个部分分是给像我一样一眼看题想到树状数组套权值线段树(也就是传说中的带修主席树)的选手的。同时也是为了避免正解卡常而设置的。树状数组套权值线段树的时间复杂度为 $O(q \log^2 n)$,可以通过前四个子任务,期望得分 $70$ 分。 #### Algorithm 5 考虑优化算法三。注意到每次修改操作对 $b$ 数组都只会修改它的两个位置,而每个位置对答案的贡献是独立的,因此我们维护全局答案,假设我们可以直接维护所有的 $b_i$,那么我们只需要在修改时被修改的两个位置对答案的贡献重新计算即可。因为 $b$ 的下标值域是 $10^9$ 无法直接维护。有两个解决方法,首先可以直接用 `std::unordered_map`/hash 来 $O(1)$ 维护桶,时间复杂度为 $O(q + n)$,也可以离线把所有修改都跑一遍,求出所有可能出现的值,放在一起离散化。时间复杂度为 $O((n + q) \log n)$。都可以通过全部的测试点,期望得分 $100$ 分。 ```cpp #include <cstdio> #include <algorithm> #include <unordered_map> const int maxn = 10000007; template <typename T> inline void qr(T &x) { char ch = getchar(); while (!isdigit(ch)) ch = getchar(); while (isdigit(ch)) x = x * 10 + ch - 48, ch = getchar(); } int n, m, cnt; long long ans, o, u, v, w = 114514191981000ll; int a[maxn], s[maxn]; std::unordered_map<int, int> bk; int gv(const int x); int main() { qr(n); qr(m); for (int i = 1; i <= n; ++i) { qr(a[i]); } ++bk[0]; for (int i = 1; i <= n; ++i) { ans += bk[s[i] = s[i - 1] ^ a[i]]++; } for (int i = 1, p, x; i <= m; ++i) { p = x = 0; qr(p); qr(x); ans -= --bk[s[p]]; ans += bk[s[p] ^= x]++; o ^= ans; if (ans & 1) ++u; v = (v > ans) ? v : ans; w = (w < ans) ? w : ans; } printf("%lld\n%lld\n%lld\n%lld\n", o, u, v, w); return 0; } ``` ```cpp #include <cstdio> #include <algorithm> #include <unordered_map> const int maxn = 10000007; template <typename T> inline void qr(T &x) { char ch = getchar(); while (!isdigit(ch)) ch = getchar(); while (isdigit(ch)) x = x * 10 + ch - 48, ch = getchar(); } int n, m, cnt; long long ans, o, u, v, w = 114514191981000ll; int a[maxn], s[maxn], p[maxn], x[maxn], bk[maxn]; std::unordered_map<int, int> t; int gv(const int x); void add(const int x); int main() { // freopen("dream.in", "r", stdin); // freopen("dream.out", "w", stdout); qr(n); qr(m); add(0); for (int i = 1; i <= n; ++i) { qr(a[i]); add(s[i] = s[i - 1] ^ a[i]); } for (int i = 1; i <= m; ++i) { qr(p[i]); qr(x[i]); add(s[p[i]] ^= x[i]); } ++bk[1]; for (int i = 1; i <= n; ++i) { ans += bk[gv(s[i] = s[i - 1] ^ a[i])]++; } for (int i = 1; i <= m; ++i) { ans -= --bk[gv(s[p[i]])]; ans += bk[gv(s[p[i]] ^= x[i])]++; o ^= ans; if (ans & 1) ++u; v = std::max(v, ans); w = std::min(w, ans); } printf("%lld\n%lld\n%lld\n%lld\n", o, u, v, w); return 0; } inline void add(const int x) { if (t.count(x) == false) t[x] = ++cnt; } inline int gv(const int x) { return t[x]; } ``` 这两份 std,第一份是直接用 `std::unordered_map` 做桶,第二份是先离线跑修改再离散化(当然,这里图省事直接用了 `std::unordered_map` 离散化,只是为了表述离线修改的做法。事实上应该使用 `std::unique` 和 `std::lower_bound` 正式离散化,否则看起来非常的多此一举)。 ### D 这是校内模拟赛的 day1B。为了帮助理解,这里放上了简化版题意。 #### Description 本题的游戏规则与《弹弹堂》基本一致,回合制游戏,回合期间行动方可以释放技能增加伤害,但是会增加 delay 值。每回合的行动方是 delay 值较低的一方。使用道具的限制为,每种道具一回合只能使用一次,使用后双放 delay 值之差的绝对值不能超过 100。在双方均最优决策的情况下,求「己方造成的伤害-对方造成的伤害」的最大值。 游戏共进行 $n$ 回合,有 $m$ 种道具。每回合固定增加 $w$ 点 delay 值。第 $i$ 种道具会增加 $p_i$ 点 delay 值,伤害增益十万分之 $k_i$。 #### Subtasks - Subtask 1(5 points):$n = 0$。 - Subtask 2(10 points):$m = 0$。 - Subtask 3(15 points):$n, m \leq 5$。 - Subtask 4(20 points):$n \leq 3$。 - Subtask 5(20 points):$m \leq 10$。 - Subtask 6(30 points):no extra restrictions。 - forall:$1 \leq n \leq 10^3$,$1 \leq m \leq 10^5$。 #### Algorithm 1 当没有道具时,不需要进行决策,只需要依题意模拟每回合出手即可通过前两个子任务,期望得分 15 分。 #### Algorithm 2 爆搜。对于每回合枚举放什么技能,每回合放技能有 $2^m$ 种可能,共有 $n$ 回合。总共有 $2^{nm}$ 种可能。时间复杂度 $O(2^{nm})$,可以通过前三个子任务,期望得分 30 分。 注意因为是博弈论,搜的时候必须倒着搜,也即从最后一回合搜到第一回合。 #### Algorithm 3 从 delay 值入手考虑。考虑如果确定了回合结束以后想要增加的 delay 值 $x$,对于每一回合,最优的使用道具的方案都是相同的。$x$ 不会超过 $t = 200$,因此可以进行 dp:设 $f_{i,j}$ 是考虑前 $i$ 个道具,增加 $j$ 的 delay 值时,最大的伤害增益是多少。转移显然: $$f_{i, j} = \min(f_{i - 1, j}, f_{i - 1, j - p_i} + k_i)$$ 这是一个标准的 01 背包问题。这部分 dp 的时间复杂度为 $O(mt)$,其中 $t = 200$。 回合结束后,双方的 delay 值之差也只有 $t$ 种情况。可以直接枚举每回合结束后双方的 delay 值之差。这样做的复杂度显然是 $O(t^n)$。回合内使用背包预处理出的给定 delay 增量后的最优方案(最大伤害增益)即可。 时间复杂度 $O(mt)-O(t^n)$,可以通过子任务 1,2,4,期望得分 35 分。结合算法 2 可以得到 50 分。 #### Algorithm 4 可以发现,如果确定了第 $i$ 回合前的 delay 值之差 $j$,那么双方第 $i \sim n$ 回合的最大伤害差 $g_{i, j}$ 也是可以 dp 的: $$g_{i, j} = \max\limits_{k = w}^{t} \{g_{i + 1, j - k} + f_{m, k}\}$$ 这是一个标准的博弈 dp。也即在转移过程中枚举当前回合增加的 delay 值。当然其中有一些细节,比如处理负数下标,或差值不能超过 200 等。 dp 的过程时间复杂度显然是 $O(nt^2)$。 如果爆搜求 $f$,那么预处理 $f$ 的复杂度是 $O(2^m)$,总复杂度为 $O(2^m) - O(nt^2)$。可以通过子任务 1,2,5,期望得分 35 分,结合算法 2 可以得到 50 分。 如果使用算法 3 的方式求 $f$,那么总时间复杂度为 $O(mt) - O(nt^2)$,可以通过全部的测试点,期望得分 100 分。 #### Summary - 本题弱智特判分 5 分,依题意模拟 15 分,暴力 30 分。 - 会用 01 背包预处理道具可以得到 50 分。 - 会用博弈 dp 求解可以得到 50 分。 - 上述两个 dp 结合可以得到 100 分。 本题模型简单,做法基础,码量友好,部分分充足合理。着重考查了选手对于问题的建模能力以及对于基础动态规划问题的掌握程度。可以较好的区分出理解转化能力强并且基本功扎实的选手。是一道优秀的基础题。 ```cpp #include <cstdio> #include <algorithm> typedef long long int ll; const int maxt = 500; const int maxn = 2005; const int maxm = 200005; const int INF = 0x3f3f3f3f; const ll inf = 0x3f3f3f3f3f3f3f3fll; int T, t = 200; int n, m, w, da, db; int k[maxm], p[maxm]; ll xa, xb; ll f[maxt], g[maxn][maxt]; int main() { scanf("%d\n%d %d %d", &T, &n, &m, &w); for (int i = 1; i <= m; ++i) scanf("%d", k + i); for (int i = 1; i <= m; ++i) scanf("%d", p + i); scanf("%lld %lld %d %d", &xa, &xb, &da, &db); std::fill(f, f + maxt, -INF); f[w] = 100000; xa /= 100000; xb /= 100000; for (int i = 1; i <= m; ++i) { for (int j = t, lim = std::max(p[i], w); j >= lim; --j) if (f[j - p[i]] != -INF) { f[j] = std::max(f[j], f[j - p[i]] + k[i]); } } for (int i = 1; i <= n; ++i) { int di = i - 1; for (int j = 0; j <= 100; ++j) { g[i][j] = -inf; for (int h = j + w; h <= t; ++h) if (f[h - j] != -INF) { g[i][j] = std::max(g[i][j], g[di][h] + f[h - j] * xa); } } for (int j = 101; j <= t; ++j) { g[i][j] = inf; for (int h = j - w; ~h; --h) if (f[j - h] != -INF) { g[i][j] = std::min(g[i][j], g[di][h] - f[j - h] * xb); } } } printf("%lld\n", g[n][da - db + 100]); return 0; } ``` ### E 这是校内模拟赛的 day2C。 #### Algorithm 1 考虑枚举所有的情况,即每条线路经过哪一层,每条线路都有 $n$ 种可能,爆搜即可,时间复杂度 $O(n^n)$。这里省略了与 $m$ 有关的项。期望得分 $35$ 分。 #### Algorithm 2 注意到数据范围明示状压 dp。考虑设计状态:$f_{i, S}$ 表示考虑了放在深度为前 $i$ 层的地铁,已经考虑了集合 $S$ 中的所有路线的最小花费,转移显然: $$f_{i, S} = \min_{s \subseteq S} f_{i - 1, s} + val(i, S - s)$$ 其中 $s$ 是 $S$ 的子集,$val(S - s)$ 表示 $s$ 在 $S$ 中的补集中的元素都放到第 $i$ 层时的花费。$val$ 是可以预处理的。具体可以参考 std~~和将来会有的民间题解~~。 暴力枚举 $s$ 并判断 $s$ 是不是 $S$ 的子集来转移,共有 $O(n 2^n)$ 个状态,单次转移复杂度 $O(2^n)$,总复杂度 $O(n 4^n + m)$,期望得分 $70$ 分。 #### Algorithm 3 考虑优化算法二中的子集枚举方式。按照如下方式枚举子集显然可以精确枚举 $S$ 的每个子集而不会无用枚举: ```cpp for (int s = S; s; s = (s - 1) & s) ``` 这样转移的复杂度就变成了 $O(2^k)$,其中 $k$ 表示 $S$ 二进制为 $1$ 的位数。考虑这样的时间复杂度为 $O(m + n \times \sum\limits_{k = 0}^n C_{n}^k \times 2^k)$。其中 $C_{n}^k$ 表示二进制共有 $k$ 位为 $1$ 的数的个数。根据二项式定理,$\sum\limits_{k = 0}^n C_{n}^k \times 2^k = \sum\limits_{k = 0}^n C_{n}^k \times 2^k \times 1^{n - k} = (2 + 1)^n = 3^n$,因此总复杂度为 $O(m + n \times 3^n)$。期望得分 $100$ 分。 值得注意的是,为了对标「NOIp2017 宝藏」的真实情况,本题没有加捆绑测试,也没有对着爆搜加剪枝卡(事实上数据量这么小根本就没法卡)。因此爆搜加剪枝可能可以获得比较可观的分数。 ```cpp #include <cctype> #include <cstdio> #include <algorithm> typedef long long int ll; const int maxn = 15; const int maxm = 100005; const int maxt = 1 << maxn; const ll INF = 1000000000000000000ll; template <typename T> inline void qr(T &x) { char ch = getchar(); while (!isdigit(ch)) ch = getchar(); while (isdigit(ch)) x = x * 10 + ch - 48, ch = getchar(); } int n, m, upc; int a[maxn][maxm], b[maxn][maxm], tmp[maxn]; ll fee[maxn][maxn], g[maxn][maxt], f[maxn][maxt]; bool ac[maxn][maxn]; int main() { qr(n); qr(m); upc = 1 << n; for (int i = 0; i < n; ++i) { for (int j = 1; j <= m; ++j) { qr(a[i][j]); } } for (int i = 0; i < n; ++i) { qr(b[i][0]); for (int j = 1; j <= b[i][0]; ++j) { qr(b[i][j]); } std::sort(b[i] + 1, b[i] + 1 + b[i][0]); for (int j = 0; j < n; ++j) { for (int k = 1; k <= b[i][0]; ++k) { fee[i][j] += a[j][b[i][k]]; // printf("Aa%d %d %d %d %d %d\n", i, j, k, b[i][k], a[j][b[i][k]], fee[i][j]); } // printf("ovO%d %d %d\n", i, j, fee[i][j]); } } for (int i = 0; i < n; ++i) { for (int j = i + 1; j < n; ++j) { ac[i][j] = ac[j][i] = true; int u = 1, v = 1; while ((u <= b[i][0]) && (v <= b[j][0])) { if (b[i][u] == b[j][v]) { ac[j][i] = ac[i][j] = false; break; } if (b[i][u] < b[j][v]) ++u; else ++v; } } } for (int s = 1; s < upc; ++s) { int cnt = 0; for (int i = 0; i < n; ++i) if (s & (1 << i)) { tmp[++cnt] = i; for (int j = 1; j < cnt; ++j) if (ac[tmp[j]][i] == false) { for (int k = 0; k < n; ++k) { g[k][s] = INF; } break; } if (g[0][s] == INF) break; for (int k = 0; k < n; ++k) { g[k][s] += fee[i][k]; } } } for (int s = 1; s < upc; ++s) { f[0][s] = g[0][s]; } for (int i = 1; i < n; ++i) { for (int s = 0; s < upc; ++s) { f[i][s] = f[i - 1][s]; for (int t = s; t; t = (t - 1) & s) { f[i][s] = std::min(f[i][s], f[i - 1][s ^ t] + g[i][t]); } // printf("%d %d %lld\n", i, s, f[i][s]); } } printf("%lld\n", f[n - 1][upc - 1]); } ``` ### F #### Algorithm 1 对于虫洞间距离不超过 $2$ 的情况,显然答案只有 $0$ 和 $1$ 两种可能。期望得分 $20$ 分。 #### Algorithm 2 考虑推一推式子。设 $f_i$ 表示在坐标 $i$ 时的期望步数,那么转移显然: $$f_{i} = \frac 1 2(f_{i - 1} + f_{i + 1}) + 1$$ 这个式子的意思是分别有 $\frac 1 2$ 的概率向左走向右走,然后变为对应坐标的期望步数。同时因为走了一步,所以最后要 $+1$。 边界条件是在虫洞结点上 $f_i = 0$。 对于子任务 $3$,可以通过手算发现这时候不在虫洞上的 $f$ 的值为 $2$。结合算法一,期望得分 $30$ 分。 #### Algorithm 3 用高斯消元求解算法二中的式子,时间复杂度 $O(qn^3)$,期望得分 $50$ 分。 #### Algorithm 4 验题人拉瓦手算带入消元得到了一个递推式,可以矩阵加速,时间复杂度 $O(q \log n)$ 带 $8$ 倍常数。期望得分 $70$ 分。 #### Algorithm 5 考虑把算法二中的式子乘二然后整理,可以得到 $$(f_{i + 1} - f_{i}) - (f_{i} - f_{i - 1}) = -2$$ 这就是说,$f$ 的二阶差分是一个非零常数。因此 $f$ 的通项公式一定是一个二次多项式。 我们考虑正着推式子:对于 $g(x) = ax^2 + bx + c$,有 $g(x - 1) = a(x^2 - 2x + 1) +b(x - 1) + c$,$g(x + 1) = a(x^2 + 2x + 1) +b(x + 1) + c$。 因此 $g(x) - g(x - 1) = 2ax - a + b$;$g(x + 1) - g(x) = 2ax + a + b$。 因此 $[g(x + 1) - g(x)] - [g(x) - g(x - 1)] = 2a$。这就是说,二次函数的二阶差分值是其二次项系数的两倍。因此可以得到 $f$ 的通项公式的二次项系数是 $-1$。同时我们还知道 $f$ 的两个零点,因此可以直接以零点式写出 $f$ 的通项公式,带入 $x$ 即可 $O(1)$ 计算。时间复杂度 $O(n \log n + q)$。这里的 $\log n$ 需要对 $x$ 数组进行排序然后用指针指向第一个大于询问值的元素。因为询问是单调不降的,所以指针移动是均摊 $O(1)$ 的。期望得分 $100$ 分。 ```cpp #include <cstdio> #include <cctype> #include <iostream> #include <algorithm> template <typename T> inline void qr(T &x) { char ch = getchar(); x = 0; while (!isdigit(ch)) ch = getchar(); while (isdigit(ch)) x = x * 10 + ch - 48, ch = getchar(); } const int maxn = 100005; const int mod = 998244353; int n, q; int ans, answ, answe = -1, answer = mod + 1; int a[maxn]; int main() { qr(n); qr(n); qr(q); for (int i = 1; i <= n; ++i) { qr(a[i]); } std::sort(a + 1, a + 1 + n); for (int x, p = 1; q; --q) { qr(x); while ((p < n) && (a[p] <= x)) ++p; int y = 1ll * (x - a[p - 1]) * (a[p] - x) % mod; ans ^= y; if (y & 1) ++answ; answe = std::max(answe, y); answer = std::min(answer, y); } std::cout << ans << std::endl << answ << std::endl << answe << std::endl << answer << std::endl; } ```