NOIP 2020 简要题解
题目链接
https://loj.ac/p?keyword=NOIP2020
题解
A. 排水系统 / water
拓扑排序部分较为简单,主要考虑高精度分数的运算与处理。
我们依然用两个 long long 类型的整数 \(a, b\) 表示大整数 \(a \times 10^{18} + b\)。高精度下分数的通分相加再约分很麻烦,但在本题中,分数的分母一定是 \(60^{11}\) 的约数,因此我们将任意分数都用 \(\frac{x}{60^{11}}\) 来表示,这样便可以只保存分子。该情况下,污水分流对应于将分子除以一个不超过 \(5\) 的整数,污水汇集对应于分子相加。最后输出答案时要对 \(\frac{x}{60^{11}}\) 进行约分,只需不断让分子分母同时除以 \(2, 3, 5\) 直至不能除尽即可。
#include<bits/stdc++.h>
using namespace std;
const int N = 123456;
const long long base = 1e18;
struct bigint_t {
long long foo, bar;
bigint_t() {
foo = bar = 0;
}
bigint_t(long long foo, long long bar): foo(foo), bar(bar) {
}
bigint_t operator + (const bigint_t& x) {
bigint_t result;
result.foo = foo + x.foo;
result.bar = bar + x.bar;
if (result.bar >= base) {
++result.foo;
result.bar -= base;
}
return result;
}
bigint_t operator / (const int& x) {
bigint_t result;
result.foo = foo / x;
result.bar = ((foo % x) * base + bar) / x;
return result;
}
int operator % (const int& x) {
return ((foo % x) * base + bar) % x;
}
void print() {
if (foo) {
cout << foo;
cout << setw(18) << setfill('0') << bar;
} else {
cout << bar;
}
}
};
const bigint_t root = bigint_t(36, 279705600000000000ll); // 60^11
int n, m, degree[N];
vector<int> adj[N];
bigint_t water[N];
int main() {
freopen("water.in", "r", stdin);
freopen("water.out", "w", stdout);
ios::sync_with_stdio(false);
cin.tie(0);
cin >> n >> m;
for (int i = 1; i <= n; ++i) {
int k, x;
cin >> k;
while (k--) {
cin >> x;
adj[i].push_back(x);
++degree[x];
}
}
queue<int> q;
vector<int> nodes;
for (int i = 1; i <= n; ++i) {
if (!degree[i]) {
q.push(i);
nodes.push_back(i);
}
}
while (!q.empty()) {
int x = q.front();
q.pop();
for (auto y : adj[x]) {
if (--degree[y] == 0) {
q.push(y);
nodes.push_back(y);
}
}
}
for (auto x : nodes) {
if (x <= m) {
water[x] = root;
}
if (adj[x].size()) {
water[x] = water[x] / (int)adj[x].size();
for (auto y : adj[x]) {
water[y] = water[y] + water[x];
}
}
}
vector<int> factor(3);
factor[0] = 2;
factor[1] = 3;
factor[2] = 5;
for (int i = 1; i <= n; ++i) {
if (!adj[i].size()) {
bigint_t num = water[i], den = root;
for (auto j : factor) {
while (num % j == 0 && den % j == 0) {
num = num / j;
den = den / j;
}
}
num.print();
cout << ' ';
den.print();
cout << '\n';
}
}
return 0;
}
B. 字符串匹配 / string
首先利用 KMP 的 fail 数组求出 \(S\) 每一个前缀的最短循环节。之后枚举 \(|AB|\) 和 \(i\),利用最短循环节判断 \((AB)^i\) 是否为 \(S\) 的一个前缀。当 \((AB)^i\) 确定时,\(C\) 串也就随之确定,再统计出有多少种 \(A\) 串(\(A\) 也为 \(S\) 的一个前缀)满足出现奇数次的字符数量不超过 \(C\) 串即可。统计部分可以用一个长为 \(\sigma = 26\) 的数组的前缀和完成,如果每次暴力 \(O(\sigma)\) 修改前缀和,那么单次查询前缀和的时间复杂度就能降到 \(O(1)\)。设 \(n = |S|\),则总复杂度为 \(O(Tn(\log n + \sigma))\),常数较小。
#include<bits/stdc++.h>
using namespace std;
const int N = 1234567;
const int sigma = 26;
int n, fail[N], loop[N], pre[N], suf[N];
string s;
int main() {
freopen("string.in", "r", stdin);
freopen("string.out", "w", stdout);
ios::sync_with_stdio(false);
cin.tie(0);
int tt;
cin >> tt;
while (tt--) {
cin >> s;
n = s.length();
for (int i = 1, j; i < n; ++i) {
for (j = fail[i]; j && s[j] != s[i]; j = fail[j]);
fail[i + 1] = s[j] == s[i] ? j + 1 : 0;
}
for (int i = 1; i <= n; ++i) {
if (i % (i - fail[i]) == 0) {
loop[i] = i - fail[i];
} else {
loop[i] = i;
}
}
vector<int> pool(sigma, 0);
for (int i = 1; i <= n; ++i) {
pre[i] = pre[i - 1];
if (++pool[s[i - 1] - 'a'] & 1) {
++pre[i];
} else {
--pre[i];
}
}
suf[n + 1] = 0;
pool = vector<int>(sigma, 0);
for (int i = n; i; --i) {
suf[i] = suf[i + 1];
if (++pool[s[i - 1] - 'a'] & 1) {
++suf[i];
} else {
--suf[i];
}
}
pool = vector<int>(sigma + 1, 0);
long long answer = 0;
for (int i = 2; i < n; ++i) {
for (int j = pre[i - 1]; j <= sigma; ++j) {
++pool[j];
}
for (int j = i; j < n; j += i) {
if (i % loop[j]) {
break;
}
answer += pool[suf[j + 1]];
}
}
cout << answer << '\n';
}
return 0;
}
C. 移球游戏 / ball
先将问题简化:所有球只有两种颜色(用黑/白表示),我们需要将一种颜色的球放在前一半柱子上,将另一种颜色的球放在后一半柱子上。如果我们能找到解决这一问题的方案,那么原题就能使用分治来解决。
针对简化后的问题,我们分两个阶段进行构造:
- 整理。对于单根柱子 \(i\),设它上面本来应该放白球,且现在上面有 \(x\) 个黑球,\(m - x\) 个白球。首先任选另外一根柱子 \(j\),将这根柱子顶部的 \(x\) 个球放到第 \(n + 1\) 根柱子上去。之后,在第 \(i\) 根柱子顶部的球如果为黑色,就移动其到第 \(j\) 根柱子上,否则移动其到第 \(n + 1\) 根柱子上。最后,将第 \(n + 1\) 根柱子上的 \(m - x\) 个白球和第 \(j\) 根柱子上的 \(x\) 个黑球依次移回柱子 \(i\) 上,再将第 \(n + 1\) 根柱子上剩下的 \(x\) 个球移回柱子 \(j\) 上。那么在该过程结束后,柱子 \(i\) 上的所有黑球(即不应被放在这一柱子上的球)均已位于所有白球的上端,且其余任何一根柱子上球的顺序未改变。
- 交换。在整理完所有柱子后,设柱子 \(i\) 上本来应该放白球,它的顶部现有 \(x\) 个黑球;柱子 \(j\) 上本来应该放黑球,它的顶部现有 \(y\) 个白球。不妨令 \(x > y\)。首先将柱子 \(i\) 上的 \(x\) 个黑球放到第 \(n + 1\) 根柱子上,再将柱子 \(j\) 上的 \(y\) 个白球放到柱子 \(i\) 上,最后将第 \(n + 1\) 根柱子上的所有黑球放到柱子 \(i\) 与 \(j\) 上。那么在该过程结束后,柱子 \(j\) 上的球已全为黑色(颜色均已正确),柱子 \(i\) 的顶部也只剩下 \(x - y\) 个黑球需要继续进行交换。不断重复交换过程,直至所有柱子上的球颜色都正确。
整理阶段所需的最大总操作次数为 \(nm \log n\) 级别(具体常数为 \(4\),\(\log n\) 来源于分治);交换阶段所需的最大总操作次数也为 \(nm \log n\) 级别(具体常数为 \(2\))。总操作次数在最坏情况下接近但小于 \(6nm \log n\)。
#include<bits/stdc++.h>
using namespace std;
const int N = 56;
const int M = 456;
int n, m, a[N][M], b[N][M], top[N];
vector<pair<int, int>> answer;
void move(int x, int y) {
answer.emplace_back(x, y);
a[y][top[y] + 1] = a[x][top[x]];
b[y][top[y] + 1] = b[x][top[x]];
++top[y];
--top[x];
}
void order(int x, int y, int z, int type) {
for (int i = 1; i <= z; ++i) {
move(y, n + 1);
}
for (int i = m; i >= 1; --i) {
move(x, b[x][i] == type ? y : n + 1);
}
for (int i = 1; i <= m - z; ++i) {
move(n + 1, x);
}
for (int i = 1; i <= z; ++i) {
move(y, x);
}
for (int i = 1; i <= z; ++i) {
move(n + 1, y);
}
}
void divide(int l, int r) {
if (l == r) {
return;
}
int mid = l + r >> 1;
for (int i = l; i <= r; ++i) {
top[i] = m;
for (int j = 1; j <= m; ++j) {
b[i][j] = a[i][j] > mid;
}
}
bool stop = true;
for (int i = l; i <= mid; ++i) {
int foobar = 0;
for (int j = 1; j <= m; ++j) {
if (b[i][j] == 1) {
stop = false;
++foobar;
}
}
if (foobar) {
order(i, r, foobar, 1);
}
}
for (int i = mid + 1; i <= r; ++i) {
int foobar = 0;
for (int j = 1; j <= m; ++j) {
if (b[i][j] == 0) {
++foobar;
}
}
if (foobar) {
order(i, l, foobar, 0);
}
}
if (!stop) {
int p1 = l, p2 = mid + 1;
while (1) {
while (p1 <= mid && b[p1][m] == 0) {
++p1;
}
while (p2 <= r && b[p2][m] == 1) {
++p2;
}
if (p1 == mid + 1) {
break;
}
pair<int, int> info1(p1, m), info2(p2, m);
for (int i = m; i; --i) {
if (b[p1][i] == 0) {
info1.second = m - i;
break;
}
}
for (int i = m; i; --i) {
if (b[p2][i] == 1) {
info2.second = m - i;
break;
}
}
if (info1.second < info2.second) {
swap(info1, info2);
}
for (int i = 1; i <= info1.second; ++i) {
move(info1.first, n + 1);
}
for (int i = 1; i <= info2.second; ++i) {
move(info2.first, info1.first);
}
for (int i = 1; i <= info1.second - info2.second; ++i) {
move(n + 1, info1.first);
}
for (int i = 1; i <= info2.second; ++i) {
move(n + 1, info2.first);
}
}
}
divide(l, mid);
divide(mid + 1, r);
}
int main() {
freopen("ball.in", "r", stdin);
freopen("ball.out", "w", stdout);
ios::sync_with_stdio(false);
cin.tie(0);
cin >> n >> m;
for (int i = 1; i <= n; ++i) {
for (int j = 1; j <= m; ++j) {
cin >> a[i][j];
}
}
divide(1, n);
cout << answer.size() << '\n';
for (auto p : answer) {
cout << p.first << ' ' << p.second << '\n';
}
return 0;
}
D. 微信步数 / walk
求从每个点出发能走的步数之和等同于求每一步合法位置的数量之和。更进一步地,假设每一维的出发坐标均为 \(0\),在走了 \(i\) 步之后第 \(j\) 维经过坐标的最小值为 \(l_{i, j}\),最大值为 \(r_{i, j}\),那么答案为 \(\sum_\limits{i} \prod_\limits{j = 1}^k [w_j - (r_{i, j} - l_{i, j})]\)。
将每 \(n\) 步视为一个周期,那么从第二个周期开始,每个周期内 \(l_{i, j}, r_{i, j}\) 的变化情况是相同的。第一个周期的答案可以暴力计算,第 \(x(x \geq 2)\) 个周期的答案可以表示为一个关于 \(x\) 的函数 \(f(x) = \sum_\limits{i = 1}^n \prod_\limits{j = 1}^k[w_j - (r_{n, j} - l_{n, j}) - c_{i, j} - (x - 2) c_{n, j}]\),其中 \(c_{i, j}\) 表示从第二个周期开始的每个周期内,走了 \(i\) 步之后第 \(j\) 维坐标的 \(r_{*, j} - l_{*, j}\)(\(*\) 即对应总步数)会增加多少。注意到整个 \(f(x)\) 是一个关于 \(x\) 的 \(k\) 次多项式,因此我们计算答案所需要的 \(f(x)\) 的前缀和则可以被表示为一个 \(k + 1\) 次多项式,当 \(x\) 较大时,使用拉格朗日插值即可求得结果。
这是 NOIP?
#include<bits/stdc++.h>
using namespace std;
const int K = 13;
const int N = 567890;
const int mod = 1e9 + 7;
void add(int& x, int y) {
x += y;
if (x >= mod) {
x -= mod;
}
}
void sub(int& x, int y) {
x -= y;
if (x < 0) {
x += mod;
}
}
int mul(int x, int y) {
return (long long)x * y % mod;
}
int power(int x, int y) {
int result = 1;
while (y) {
if (y & 1) {
result = mul(result, x);
}
x = mul(x, x);
y >>= 1;
}
return result;
}
struct info_t {
int l, r, p;
info_t() {
l = r = p = 0;
}
info_t(int l, int r, int p): l(l), r(r), p(p) {
}
int length() {
return r - l;
}
void modify() {
l = min(l, p);
r = max(r, p);
}
info_t operator + (const info_t& a) {
return info_t(min(l, p + a.l), max(r, p + a.r), p + a.p);
}
} pre[K][N];
int n, k, w[K], coef[K][N], value[K], inv[K], invfac[K];
int calc(int x) {
int result = 0;
for (int i = 1; i <= n; ++i) {
int foo = 1;
for (int j = 1; j <= k; ++j) {
int bar = w[j];
sub(bar, coef[j][i]);
sub(bar, mul(coef[j][n], x));
foo = mul(foo, bar);
}
add(result, foo);
}
return result;
}
int lagrange(int x) {
vector<int> pre(k + 4), suf(k + 4);
pre[0] = suf[k + 3] = 1;
for (int i = 1; i <= k + 2; ++i) {
pre[i] = mul(pre[i - 1], x - i + mod);
}
for (int i = k + 2; i; --i) {
suf[i] = mul(suf[i + 1], x - i + mod);
}
int result = 0;
for (int i = 1; i <= k + 2; ++i) {
int den = mul(invfac[i - 1], invfac[k + 2 - i]);
if (k + 2 - i & 1) {
den = mul(den, mod - 1);
}
add(result, mul(mul(value[i], den), mul(pre[i - 1], suf[i + 1])));
}
return result;
}
int main() {
freopen("walk.in", "r", stdin);
freopen("walk.out", "w", stdout);
ios::sync_with_stdio(false);
cin.tie(0);
cin >> n >> k;
invfac[0] = inv[1] = invfac[1] = 1;
for (int i = 2; i <= k + 1; ++i) {
inv[i] = mul(mod - mod / i, inv[mod % i]);
invfac[i] = mul(invfac[i - 1], inv[i]);
}
int answer = 1, foobar;
for (int i = 1; i <= k; ++i) {
cin >> w[i];
answer = mul(answer, w[i]);
}
foobar = answer;
bool stop = false;
for (int i = 1; i <= n; ++i) {
int x, y;
cin >> x >> y;
for (int j = 1; j <= k; ++j) {
pre[j][i] = pre[j][i - 1];
if (j == x) {
pre[j][i].p += y;
pre[j][i].modify();
if (pre[j][i].length() != pre[j][i - 1].length()) {
foobar = mul(foobar, power(w[j], mod - 2));
foobar = mul(foobar, w[j] - 1);
if (--w[j] == 0) {
stop = true;
}
}
}
}
add(answer, foobar);
}
if (stop) {
cout << answer << '\n';
return 0;
}
int infty = 0;
for (int i = 1; i <= k; ++i) {
if (!pre[i][n].p && pre[i][n].r - pre[i][n].l < w[i]) {
++infty;
}
}
if (infty == k) {
cout << -1 << '\n';
return 0;
}
for (int i = 1; i <= n; ++i) {
for (int j = 1; j <= k; ++j) {
coef[j][i] = coef[j][i - 1];
if ((pre[j][n] + pre[j][i]).length() != (pre[j][n] + pre[j][i - 1]).length()) {
++coef[j][i];
}
}
}
for (int i = 0; i <= k + 2; ++i) {
value[i] = (value[i - 1] + calc(i)) % mod;
}
int need = w[1];
for (int i = 1; i <= k; ++i) {
if (coef[i][n]) {
need = min(need, w[i] / coef[i][n]);
}
}
for (int i = 1; i <= k; ++i) {
w[i] -= need * coef[i][n];
}
if (need) {
add(answer, lagrange(need - 1));
}
foobar = 1;
for (int i = 1; i <= k; ++i) {
foobar = mul(foobar, w[i]);
}
for (int i = 1; i <= n; ++i) {
for (int j = 1; j <= k; ++j) {
if ((pre[j][n] + pre[j][i]).length() != (pre[j][n] + pre[j][i - 1]).length()) {
foobar = mul(foobar, power(w[j], mod - 2));
foobar = mul(foobar, w[j] - 1);
--w[j];
}
}
add(answer, foobar);
}
cout << answer << '\n';
return 0;
}