題目鏈接
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;
}