T1. 排水系統
將每個 " 排水節點 " 看成是一個 " 點 "。
將每個 " 單向排水管道 " 看成是一條 " 單向邊 "。
不難發現,得到的圖是一張 DAG。
直接模擬題意即可。
依次松弛每個節點的蓄水量,直至到達最終排水口。
需要注意的是,在松弛任意一個節點 \(v\) 的蓄水量時,需要保證:
- 對於圖中的每一條有向邊 \((u, v)\),\(u\) 的蓄水量都被松弛過了。
發現可以通過拓撲序來轉移。
要打高精。
我比較懶,用的 __int128
,大家還是好好打高精吧(:
#include <cstdio>
#include <cstring>
#include <algorithm>
#include <queue>
#include <vector>
#define u128 __int128
using namespace std;
inline void print(u128 x) {
if (x > 9) print(x / 10);
putchar('0' + x % 10);
}
u128 gcd(u128 a, u128 b) {
if (!b) return a;
return gcd(b, a % b);
}
const int N = 400100;
struct Node {
u128 x, y;
} a[N];
Node operator + (Node a, Node b) {
Node c;
c.y = a.y * b.y;
c.x = a.x * b.y + b.x * a.y;
u128 S = gcd(c.x, c.y);
if (!S) {
c.x = 0;
c.y = 1;
} else {
c.x /= S;
c.y /= S;
}
return c;
}
Node operator / (Node a, int num) {
Node b;
b.x = a.x;
b.y = 1ll * a.y * num;
u128 S = gcd(b.x, b.y);
if (!S) {
b.x = 0;
b.y = 1;
} else {
b.x /= S;
b.y /= S;
}
return b;
}
int n, m;
vector<int> to[N];
int deg[N];
void topsort() {
queue<int> q;
for (int i = 1; i <= n; i ++)
if (deg[i] == 0) q.push(i);
while (q.size()) {
int u = q.front(); q.pop();
if (to[u].size() == 0) continue;
Node give = a[u] / to[u].size();
for (int i = 0; i < (int)to[u].size(); i ++) {
int v = to[u][i];
a[v] = a[v] + give;
if (-- deg[v] == 0) q.push(v);
}
}
}
int main() {
scanf("%d%d", &n, &m);
for (int i = 1, S; i <= n; i ++) {
scanf("%d", &S);
while (S --) {
int x;
scanf("%d", &x);
to[i].push_back(x);
}
}
for (int i = 1; i <= m; i ++)
a[i].x = 1, a[i].y = 1;
for (int i = m + 1; i <= n; i ++)
a[i].x = 0, a[i].y = 1;
for (int i = 1; i <= n; i ++)
for (int j = 0; j < (int)to[i].size(); j ++) {
int v = to[i][j];
deg[v] ++;
}
topsort();
for (int i = 1; i <= n; i ++)
if (to[i].size() == 0) {
print(a[i].x);
printf(" ");
print(a[i].y);
puts("");
}
return 0;
}
// I hope changle_cyx can pray for me.
T2. 字符串匹配
算法一
特殊性質:\(n \leq 2^{17}\)。
一個較為簡單的做法,基本不用怎么思考。
注意到答案是要求將字符串划分成 \(S = (AB)^iC\) 的形式。
記 \(F(S)\) 的表示字符串 \(S\) 中出現奇數次的字符的數量。
需要先預處理出:
- 每一個前綴 \(S_{1 .. i}\) 出現奇數次的字符的數量,即 \(F(S_{1..i})\)。
- 每一個后綴 \(S_{i .. n}\) 出現奇數次的字符的數量,即 \(F(S_{i .. n})\)。
考慮枚舉 \(T = (AB)\),那相當於是枚舉一個前綴。
在此基礎上,再從小到大枚舉一個 \(i\),使用 hash 判斷子串是否完全相等。
此時整個字符串的划分結構就已經是確定的了。
\(F(C)\) 已經預處理好了,那這種情況對答案的貢獻,相當於要在 \(T\) 里數出有多少個 \(A\) 滿足 \(F(A) \leq F(C)\)。
注意到每一個 \(A\) 在 \(T\) 中是真前綴,那直接用樹狀數組動態維護一下即可。
時間復雜度 \(\mathcal{O}(n \ln n + n \log |\sum|)\)。
其中 \(\sum\) 表示字符集。
期望得分 \(84 \sim 100\)。
#include <iostream>
#include <cstdio>
#include <cstring>
#include <algorithm>
using namespace std;
const int N = 2000000;
const unsigned long long P = 13331;
int n;
char S[N];
// suf & pre
bool exist[30];
int num;
int suf[N];
int pre[N];
int c[N][30];
// hash
unsigned long long power[N];
unsigned long long hash[N];
unsigned long long H(int l, int r) {
return hash[r] - hash[l - 1] * power[r - l + 1];
}
void work() {
scanf("%s", S + 1);
n = strlen(S + 1);
// suf
for (int i = 0; i < 26; i ++)
exist[i] = 0;
num = 0;
for (int i = n; i >= 1; i --) {
int ch = S[i] - 'a';
exist[ch] ^= 1;
if (exist[ch]) num ++;
else num --;
suf[i] = num;
}
// pre
for (int i = 0; i < 26; i ++)
exist[i] = 0;
num = 0;
for (int i = 1; i <= n; i ++) {
int ch = S[i] - 'a';
exist[ch] ^= 1;
if (exist[ch]) num ++;
else num --;
pre[i] = num;
}
for (int i = 1; i <= n; i ++) {
for (int j = 0; j <= 26; j ++)
c[i][j] = c[i - 1][j];
for (int j = pre[i]; j <= 26; j ++)
c[i][j] ++;
}
// hash
for (int i = 1; i <= n; i ++)
hash[i] = hash[i - 1] * P + (S[i] - 'a');
// work
long long ans = 0;
for (int i = 1; i <= n; i ++) {
for (int j = 1; j <= n / i; j ++) {
int l = (j - 1) * i + 1, r = j * i;
if (H(1, i) != H(l, r)) break;
if (r + 1 > n) break;
ans += c[i - 1][suf[r + 1]];
}
}
printf("%lld\n", ans);
}
int main() {
power[0] = 1;
for (int i = 1; i <= 1500000; i ++) power[i] = power[i - 1] * P;
int T; scanf("%d", &T);
while (T --) work();
return 0;
}
// I hope changle_cyx can pray for me.
算法二
特殊性質:\(n \leq 2^{20}\)。
「算法一」沒有怎么用到題目中的一些性質。
還是考慮枚舉 \(T = (AB) = S_{1 .. x}\)。
對於當前枚舉到的一個 \(x\)。
考慮計算出一個最大的 \(i\),使得 \(S\) 可以被划分成 \((AB)^iC\) 的形式,記這個量為 \(k\)。
首先有一個結論是:若一個長度為 \(n\) 的字符串 \(S\) 的前 \(n - m\) 位和后 \(n - m\) 是相等的且 \(m \mid n\),則 \(S\) 有一個長度為 \(m\) 的整除循環節。
根據該結論,考慮求出字符串 \(S\) 的 \(Z\) 函數,其中 \(Z_i\) 表示:后綴 \(S_{i .. n}\) 與 \(S\) 的最長公共前綴(LCP)的長度。
至於 Z 算法大家懂得都懂。
注意到若 \(S\) 可以被划分成 \((AB)^iC\) 的形式,則必滿足 \(x(i - 1) \leq Z_{x + 1}\) 且 \(xi < n\)。
解得 \(i \leq \frac{Z_{x + 1}}{x}+ 1\) 且 \(i < \frac{n}{x}\),則 \(k = \min \left\{ \left\lfloor \frac{Z_{x + 1}}{x} \right\rfloor + 1, \left\lceil \frac{n}{x} \right\rceil - 1 \right\}\)。
接下來考慮 \(F\left((AB)^i\right)\) 的一些性質(重復性),注意到:
-
當 \(i\) 為奇數時,有 \(F\left( (AB)^i \right)\) 均相等,即 \(F\left(AB\right) = F\left( (AB)^3 \right) = F\left( (AB)^5 \right) = \cdots\)。
那么可以推出,當 \(i\) 為奇數時,對於每個 \((AB)^i\) 划分出來的 \(C\),都有 \(F(C) = F(S_{x + 1 .. n})\)。 -
當 \(i\) 為偶數時,有 \(F\left( (AB)^i \right) = 0\)。
那么可以推出,當 \(i\) 為偶數時,對於每個 \((AB)^i\) 划分出來的 \(C\),都有 \(F(C) = F(S)\)。
那么我們可以知道,划分出來的 \(F(C)\) 的也就只有兩種情況,要么是 \(F(S_{x + 1 .. n})\) 要么是 \(F(S)\)。
並且,有 \(\left\lceil \frac{k}{2} \right\rceil\) 個 \(i\) 為奇數,有 \(\left\lfloor \frac{k}{2} \right\rfloor\) 個 \(i\) 為偶數。
\(F(S_{x + 1 .. n})\) 在枚舉 \(x\) 的時候順便處理一下即可,\(F(S)\) 直接處理即可。
那么現在就是要分別數出 \(T\) 中有多少個真前綴 \(A\) 滿足 \(F(A) \leq F(S_{x + 1 .. n})\),\(F(A) \leq F(S)\)。
可以用樹狀數組做到 \(\mathcal{O}(n \log |\sum|)\)。
注意到 \(F(S)\) 是不變的,那么在枚舉的時候直接判斷一下即可。
至於 \(F(S_{x + 1 .. n})\),當 \(x\) 加 \(1\) 的時候也只會導致 \(F(S_{x + 1 .. n})\) 變化 \(1\),稍微判斷一下補補貢獻即可。
時間復雜度 \(\mathcal{O}(n)\)。
#include <cstdio>
#include <cstring>
#include <algorithm>
using namespace std;
const int N = (1 << 20) + 1000;
int T;
int n;
char s[N];
int Z[N];
void Z_algorithm() {
for (int i = 1; i <= n; i ++) Z[i] = 0;
for (int i = 2, l = 0, r = 0; i <= n; i ++) {
if (i <= r) Z[i] = min(Z[i - l + 1], r - i + 1);
while (i + Z[i] <= n && s[1 + Z[i]] == s[i + Z[i]])
Z[i] ++;
if (i + Z[i] - 1 > r)
l = i, r = i + Z[i] - 1;
}
}
int pre, suf, Fs;
int cnt[26];
bool flag1[26], flag2[26];
int cur1, cur2;
long long ans;
void work() {
scanf("%s", s + 1);
n = strlen(s + 1);
Z_algorithm();
memset(flag1, 0, sizeof(flag1));
memset(flag2, 0, sizeof(flag2));
for (int i = 1; i <= n; i ++) {
int ch = s[i] - 'a';
flag2[ch] ^= 1;
}
Fs = 0;
for (int i = 0; i < 26; i ++)
if (flag2[i]) Fs ++;
pre = 0, suf = Fs;
ans = 0, cur1 = cur2 = 0;
memset(cnt, 0, sizeof(cnt));
for (int i = 1; i < n; i ++) {
int ch = s[i] - 'a';
flag1[ch] ^= 1, flag2[ch] ^= 1;
if (flag1[ch]) pre ++;
else pre --;
if (flag2[ch]) suf ++, cur1 += cnt[suf];
else cur1 -= cnt[suf], suf --;
int k = min((Z[i + 1] / i) + 1, (n - 1) / i);
int odd = (k + 1) / 2, even = k / 2;
ans += 1ll * odd * cur1;
ans += 1ll * even * cur2;
cnt[pre] ++;
if (pre <= suf) cur1 ++;
if (pre <= Fs) cur2 ++;
}
printf("%lld\n", ans);
}
int main() {
scanf("%d", &T);
while (T --) work();
return 0;
}
// I hope changle_cyx can pray for me.
T3. 移球游戲
算法一
特殊性質:\(n = 2\)。
現在有三個柱子 \(x, y, z\)。
其中 \(x\) 號柱與 \(y\) 號柱是滿的,\(z\) 號柱是空的。
這 \(2m\) 個球中有 \(m\) 個關鍵球,現在要將所有關鍵球移動到同一根柱子上。
設 \(x\) 柱上有 \(c\) 個關鍵球,操作如下:
-
(1):將 \(y\) 號柱上的 \(c\) 個球移動到 \(z\) 號柱上。
-
(2):依次考慮 \(x\) 號柱里的每一個球。
若該球為關鍵球,則將其移動到 \(y\) 號柱。
若該球不為關鍵球,則將其移動到 \(z\) 號柱。 -
(3):將 \(z\) 號柱上方的 \(m - c\) 個球移回 \(x\) 號柱。
-
(4):將 \(y\) 號柱上方的 \(c\) 個球移動到 \(x\) 號柱。
-
(5):將 \(z\) 號柱里的 \(c\) 個球移動到 \(y\) 號柱。
-
(6):將 \(x\) 號柱上方的 \(c\) 個球移動到 \(z\) 號柱。
-
(7):依次考慮 \(y\) 號柱里的每一個球。
若該球為關鍵球,則將其移動到 \(z\) 號柱。
若該球不為關鍵球,則將其移動到 \(x\) 號柱。
此時 \(n = 2\) 就做完了,復雜度是 \(\mathcal{O(m)}\) 的。
「算法一」是本題中最基本的操作。
算法二
特殊性質:\(n \leq 50\),\(m \leq 300\)。
可以一個顏色一個顏色來考慮。
假設考慮到第 \(n\) 個顏色,現在要將所有顏色為 \(n\) 的球移動到同一根柱子上:
-
枚舉 \(i = 1 \to (n - 1)\)。
該步驟的意義為:將 \(i\) 號柱里所有顏色為 \(n\) 的球都移動到 \(i\) 號柱子的最頂端。
記 \(i\) 號柱共有 \(c_i\) 個顏色為 \(n\) 的球,操作如下:-
(1):將 \(n\) 號柱移出 \(c_i\) 個空位。
-
(2):依次考慮 \(i\) 號柱里的每一個球。
若該球的顏色為 \(n\),則將其移動到 \(n\) 號柱。
若該球的顏色不為 \(n\),則將其移動到 \(n + 1\) 號柱。 -
(3):將 \(n + 1\) 號柱上方的 \(m - c_i\) 個球移回 \(i\) 號柱。
-
(4):將 \(n\) 號柱上方的 \(c_i\) 個球移回 \(i\) 號柱。
-
(5):將 \(n + 1\) 號柱上方的 \(c_i\) 個球移回 \(n\) 號柱。
-
-
枚舉 \(i = 1 \to (n - 1)\)。
將 \(i\) 號柱子最頂端所有顏色為 \(n\) 的球都移動到 \(n + 1\) 號柱上。 -
依次考慮 \(n\) 號柱子里的每一個球。
若該球的顏色為 \(n\),則將其移動到 \(n + 1\) 號柱。
若該球的顏色不為 \(n\),則將其補到 \(1\) 至 \(n - 1\) 號柱里的一個空位上。
這樣的話就得到了一個規模為 \(n - 1\) 的子問題,直接遞歸調用到 \(1\) 即可。
復雜度是 \(\mathcal{O}(n^2m)\) 的。
來計算一下該算法的嚴格操作數。
記 \(g(n)\) 表示解決一個規模為 \(n\) 的問題,且不向下遞歸調用時需要的操作數,則:
在最壞情況下,\(\sum\limits_{i = 1}^{n - 1} c_i = m\),則:
此時:
發現剛好可以過掉 \(70\) 分。
#include <cstdio>
#include <cstring>
#include <algorithm>
using namespace std;
inline int read() {
int x = 0, f = 1; char s = getchar();
while (s < '0' || s > '9') { if (s == '-') f = -f; s = getchar(); }
while (s >= '0' && s <= '9') { x = x * 10 + s - '0'; s = getchar(); }
return x * f;
}
const int N = 60, M = 450;
int n, m;
int top[N], a[N][M];
int cnt[N][M];
int t;
pair<int, int> ans[820001];
void move(int x, int y) {
ans[++ t] = make_pair(x, y);
int u = a[x][top[x]];
cnt[x][u] --;
cnt[y][u] ++;
a[y][++ top[y]] = a[x][top[x] --];
}
void solve(int u) {
if (u == 1)
return;
for (int i = 1; i < u; i ++) {
int c = cnt[i][u];
for (int j = c; j; j --)
move(u, u + 1);
for (int j = m; j; j --)
if (a[i][j] == u) move(i, u);
else move(i, u + 1);
for (int j = m - c; j; j --)
move(u + 1, i);
for (int j = c; j; j --)
move(u, i);
for (int j = c; j; j --)
move(u + 1, u);
}
for (int i = 1; i < u; i ++)
while (a[i][top[i]] == u)
move(i, u + 1);
int p = 1;
for (int j = top[u]; j; j --) {
if (a[u][j] == u) move(u, u + 1);
else {
while (top[p] >= m) p ++;
move(u, p);
}
}
solve(u - 1);
}
int main() {
n = read(), m = read();
for (int i = 1; i <= n; i ++) {
top[i] = m;
for (int j = 1; j <= m; j ++)
a[i][j] = read(), cnt[i][a[i][j]] ++;
}
solve(n);
printf("%d\n", t);
for (int i = 1; i <= t; i ++)
printf("%d %d\n", ans[i].first, ans[i].second);
return 0;
}
// I hope changle_cyx can pray for me.
算法三
特殊性質:\(n \leq 50\),\(m \leq 400\)。
注意到「算法二」中," 一個顏色一個顏色來考慮 " 有點浪費。
可不可以多個顏色一起考慮呢。
這啟發我們分治。
定義分治函數 solve(l, r)
,取中點 \(\text{mid} = \left\lfloor \frac{l + r}{2} \right\rfloor\)。
在每一輪中我們的目的是:將所有 " 顏色 \(\leq \text{mid}\) 的球 " 與 " 顏色 \(> \text{mid}\) 的球 " 區分開來。
即:經過一系列操作過后,不存在一根柱子上同時有 " 顏色 \(\leq \text{mid}\) 的球 " 和 " 顏色 \(> \text{mid}\) 的球 "。
隨后調用 solve(l, mid)
與 solve(mid + 1, r)
。
問題的關鍵在於如何區分。
在每一輪中,我們將這 \(r - l + 1\) 根柱子取出來。
每次我們可以挑出兩根柱子:
- (1):如果 " 顏色 \(\leq \text{mid}\) 的球 " 超過了 \(m\) 個。
則選取任意 \(m\) 個 " 顏色 \(\leq \text{mid}\) 的球 " 作為關鍵球,進行「算法一」中的基本操作。 - (2):如果 " 顏色 \(> \text{mid}\) 的球 " 超過了 \(m\) 個。
則選取任意 \(m\) 個 " 顏色 \(> \text{mid}\) 的球 " 作為關鍵球,進行「算法一」中的基本操作。
這樣進行 \(r - l\) 次,這 \(r - l + 1\) 根柱子也就達到了每一輪的目的,遞歸下去解決即可。
考慮分治樹的結構,共有 \(\mathcal{O(\log n)}\) 層。
對於每層的所有節點,進行上述的基本操作的復雜度是 \(\mathcal{O(nm)}\) 的。
故復雜度為 \(\mathcal{O(nm \log n)}\),肯定是可以過的。
#include <cstdio>
#include <cstring>
#include <algorithm>
#include <vector>
using namespace std;
inline int read() {
int x = 0, f = 1; char s = getchar();
while (s < '0' || s > '9') { if (s == '-') f = -f; s = getchar(); }
while (s >= '0' && s <= '9') { x = x * 10 + s - '0'; s = getchar(); }
return x * f;
}
const int N = 60, M = 410;
int n, m;
int top[N], a[N][M];
int em;
int t;
pair<int, int> ans[820001];
void move(int x, int y) {
ans[++ t] = make_pair(x, y);
a[y][++ top[y]] = a[x][top[x] --];
}
bool impx[M], impy[M];
int merge(int x, int y, int mid) {
int cx = 0, cy = 0;
for (int i = 1; i <= m; i ++)
impx[i] = a[x][i] <= mid,
impy[i] = a[y][i] <= mid;
for (int i = 1; i <= m; i ++)
cx += impx[i], cy += impy[i];
if (cx + cy > m) {
cx = m - cx, cy = m - cy;
for (int i = 1; i <= m; i ++)
impx[i] ^= 1, impy[i] ^= 1;
}
for (int i = 1; i <= m; i ++)
if (!impx[i] && cx + cy < m)
impx[i] = 1, cx ++;
for (int i = cx; i; i --)
move(y, em);
for (int i = m; i; i --)
if (impx[i]) move(x, y);
else move(x, em);
for (int i = m - cx; i; i --)
move(em, x);
for (int i = cx; i; i --)
move(y, x);
for (int i = cx; i; i --)
move(em, y);
for (int i = cx; i; i --)
move(x, em);
for (int i = m; i; i --)
if (impy[i]) move(y, em);
else move(y, x);
int p = em; em = y;
return p;
}
void solve(int l, int r) {
if (l == r) return;
int mid = (l + r) >> 1;
vector<int> now;
for (int i = 1; i <= n + 1; i ++) {
if (i == em) continue;
if (l <= a[i][1] && a[i][1] <= r) now.push_back(i);
}
for (int i = 0; i + 1 < (int)now.size(); i ++)
now[i + 1] = merge(now[i], now[i + 1], mid);
solve(l, mid), solve(mid + 1, r);
}
int main() {
n = read(), m = read();
em = n + 1;
for (int i = 1; i <= n; i ++) {
top[i] = m;
for (int j = 1; j <= m; j ++)
a[i][j] = read();
}
solve(1, n);
printf("%d\n", t);
for (int i = 1; i <= t; i ++)
printf("%d %d\n", ans[i].first, ans[i].second);
return 0;
}
// I hope changle_cyx can pray for me.
T4. 微信步數
算法一
特殊性質:\(w_i \leq 10^6\)。
不難想到,可以考慮計算 " 走完了第 \(i\) 步后恰好走出場地 " 的所有點對答案的貢獻。
為了方便敘述,約定:
- 一個周期:前 \(n\) 步組成的路線。
- \(L_i\):在第 \(i\) 個維度上,當前仍然在場地內的點的坐標的最小值。
- \(R_i\):在第 \(i\) 個維度上,當前仍然在場地內的點的坐標的最大值。
- \(\overrightarrow{v}\):經過了一個周期后的位移向量。
\(v_i\):位移向量 \(\overrightarrow{v}\) 在第 \(i\) 個維度上的位移。
引理 1
若會出現某一個點死循環,則滿足以下兩個條件。
(\(1\)):經過了一個周期后,存在一個點沒有走出場地。
(\(2\)):\(\overrightarrow{v} = \overrightarrow{0}\)。證明
必要性:顯然。
充分性:考慮滿足(\(1\))的點,因為 \(\overrightarrow{v} = \overrightarrow{0}\),所以無論走了多少個周期,該點始終呆在原地,也不會在走周期的過程中走出場地。故出現死循環。
故命題得證,QED。
判掉了無解的情況。
來考慮一下什么情況下會對答案造成貢獻。
引理 2
若第 \(i\) 步對答案有貢獻,則當前在第 \(c_i\) 維上的位移,一定是向 " 正方向 " 走或向 "反方向 " 走的歷史位移最大值(即可以更新 \(L_{c_i}\) 或 \(R_{c_i}\))。
證明
考慮 \(L_i\) 和 \(R_i\) 的定義,不難發現:
在第 \(c_i\) 維中,所有離開場地的點的坐標一定在 \([1, L_{c_i}) \ \bigcup \ (R_{c_i}, w_{c_i}]\) 內。那么,若第 \(i\) 步后 \(L_{c_i}\) 或 \(R_{c_i}\) 沒有得到更新,那么 \([1, L_{c_i}) \ \bigcup \ (R_{c_i}, w_{c_i}]\) 是不會變的,故離開場地的點的集合也沒變。故在該種情況下,第 \(i\) 步不對答案造成貢獻。
進一步分析可知:
若第 \(i\) 步后 \(L_{c_i}\) 或 \(R_{c_i}\) 得到了更新,那么這一批離開場地的點的坐標均為更新后的 \(L_{c_i} - 1\) 或 \(R_{c_i} + 1\)。故命題得證,QED。
知道了什么情況下造成貢獻。
來考慮一下該種情況下會造成多少貢獻。
引理 3
若第 \(i\) 步對答案有貢獻,則該貢獻值為 \(i \ast \prod\limits_{j \neq c_i} (R_j - L_j + 1)\)。
證明
對於第 \(c_i\) 維,這一批離開場地的點的坐標均為更新后的 \(L_{c_i} - 1\) 或 \(R_{c_i} + 1\)。
對於除了第 \(c_i\) 維的任意一個維度,現在考慮第 \(p\) 維。
顯然,這一批離開場地的點的坐標可以在 \([L_p, R_p]\) 中任意做選擇。根據乘法原理,這一批離開場地的點共有:
\[(R_1 - L_1 + 1) \ast \cdots \ast (R_{c_i - 1} - L_{c_i - 1} + 1) \ast 1 \ast (R_{c_i + 1} - L_{c_i + 1} + 1) \ast \cdots \ast (R_k - L_k + 1) \]即為:
\[\prod\limits_{j \neq c_i} (R_j - L_j + 1) \]將上式乘上 \(i\),即為第 \(i\) 步對答案的貢獻值。
故命題得證,QED。
至此,我們已經有方法可以計算出正確的答案。
只不過,要是直接枚舉 \(i\) 的話,時間復雜度是不能接受的。
我們還沒有利用到移動路線的 " 周期性 ",接下來就來討論一下 " 周期性 " 在本題中的特殊效果。
為了方便敘述,我們稱可以對答案造成貢獻的一步為 " 特殊步 "。
引理 4
(\(1\)):當第一周期中的第 \(i\) 步為 " 特殊步 " 時:
- 則第二周期中的第 \(i + n\) 步,第三周期中的第 \(i + 2n\) 步,...,均不一定為 " 特殊步 "。
(\(2\)):當第一周期中的第 \(i\) 步為 " 特殊步 ",且第二周期中的第 \(i + n\) 步也為 " 特殊步 " 時:
- 第三周期中的第 \(i + 2n\) 步,第四周期中的第 \(i + 3n\) 步,...,一定均為 " 特殊步 "。
引理 4 的推論
第一周期中 " 特殊步 " 的分布是特殊的; 第二、三、四、... 周期中 " 特殊步 " 的分布是相同的。
不要問我上面的東西為什么沒有證明,問就是感性理解。
根據引理 \(4\) 的推論。
我們可以先計算出第一周期中 " 特殊步 " 對答案的貢獻。
對於第二、三、四、... 周期,考慮步數模 \(n\) 的結果,對於模 \(n\) 的所有同余類 \(\overline{1}, \overline{2}, \cdots, \overline{n - 1}, \overline{0}\),我們可以分別計算每個同余類對答案的貢獻。
引理 5
設當前進行到了第 \(i\) 步,若進行到了第 \(i + n\) 步時,該點還在場地內,對於第 \(j\) 維的變化是:
(\(1\)):若 \(v_j > 0\),則 \(L_j \gets L_j + |v_j|\)。
(\(2\)):若 \(v_j < 0\),則 \(R_j \gets R_j - |v_j|\)。
(\(3\)):對於 \(R_j - L_j + 1\),其值將會變化為:
\[(R_j - L_j + 1) - |v_j| \]證明
不難發現,若 " 進行到第 \(i + n\) 步時,該點還在場地內 "。
則 " 前 \(i\) 步組成的路徑 " 與 " 前 \(i + n\) 步組成的路徑 " 之間只差了一個周期位移向量 \(\overrightarrow{v}\)。
(在該情況下,位移的先后順序不影響最終結果)那么,在只考慮第 \(j\) 維的情況下。
相當於進行到了第 \(i\) 步,然后又經過了一個周期后,坐標值被加上了 \(v_j\)。故命題得證,QED。
引理 5 的推論
對於第二周期中的第 \(i\) 步(其中 \(n < i \leq 2n\)),若第 \(i\) 步是一個 " 特殊步 "。
考慮第 \(n \ast x + (i - n)\) 步,若該步也是一個 " 特殊步 ",則該步對答案的貢獻為:\[[n \ast (x - 1) + i] \prod \limits_{j \neq c_i} [-|v_j| \ast (x - 1) + (R_j - L_j + 1)] \]即:
\[[n \ast x + (i - n)] \prod\limits_{j \neq c_i}[-|v_j| \ast x + (R_j - L_j + 1 + |v_j|)] \]
至此,已經有了一個初步的做法。
枚舉 \(i = 1 \to n\),先計算出第一周期中 " 特殊步 " 對答案的貢獻。
再枚舉 \(i = (n + 1) \to 2n\),計算每個模 \(n\) 的同余類 \(\overline{i - n}\) 對答案的貢獻。具體地,根據引理 \(5\) 的推論,在循環內部再枚舉一個 \(x\) 即可。
時間復雜度玄學,上界是 \(\mathcal{O}(nk \ast \max\limits_{1\leq i\leq k} \left\{w_i\right\})\)。
但實際上遠不能達到上界,所以可以拿到可觀的 \(80\) 分。
#include <cstdio>
#include <cstring>
#include <algorithm>
using namespace std;
inline int read() {
int x = 0, f = 1; char s = getchar();
while (s < '0' || s > '9') { if (s == '-') f = -f; s = getchar(); }
while (s >= '0' && s <= '9') { x = x * 10 + s - '0'; s = getchar(); }
return x * f;
}
const int N = 500100, SIZE = 11;
const int mod = 1e9 + 7;
int n, k;
int w[SIZE];
int c[N], d[N];
int L[SIZE], R[SIZE];
int v[SIZE];
int u[SIZE];
int cur[SIZE];
int ans;
int main() {
n = read(), k = read();
for (int i = 1; i <= k; i ++)
w[i] = read();
for (int i = 1; i <= n; i ++)
c[i] = read(), d[i] = read();
// judgment has no answer.
for (int i = 1; i <= k; i ++)
L[i] = 1, R[i] = w[i];
for (int i = 1; i <= n; i ++) {
v[c[i]] += d[i];
if (v[c[i]]) {
if (v[c[i]] > 0) {
int val = v[c[i]];
if (w[c[i]] - val < R[c[i]]) R[c[i]] = w[c[i]] - val;
} else {
int val = -v[c[i]];
if (val + 1 > L[c[i]]) L[c[i]] = val + 1;
}
}
}
bool flag1 = 1;
for (int i = 1; i <= k; i ++)
if (L[i] > R[i]) flag1 = 0;
bool flag2 = 1;
for (int i = 1; i <= k; i ++)
if (v[i]) flag2 = 0;
if (flag1 && flag2) {
puts("-1");
return 0;
}
// work
for (int i = 1; i <= k; i ++)
L[i] = 1, R[i] = w[i];
for (int i = 1; i <= n; i ++) {
int C = c[i], D = d[i];
u[C] += D;
bool great = 0;
if (u[C]) {
if (u[C] > 0) {
int val = u[C];
if (w[C] - val < R[C])
R[C] = w[C] - val, great = 1;
} else {
int val = -u[C];
if (val + 1 > L[C])
L[C] = val + 1, great = 1;
}
}
if (great) {
int val = 1;
for (int j = 1; j <= k; j ++) {
if (j == C) continue;
val = 1ll * val * (R[j] - L[j] + 1) % mod;
}
ans = (ans + 1ll * val * i) % mod;
if (L[C] > R[C]) {
printf("%d\n", ans);
return 0;
}
}
}
for (int i = n + 1; i <= 2 * n; i ++) {
int C = c[i - n], D = d[i - n];
u[C] += D;
bool great = 0;
if (u[C]) {
if (u[C] > 0) {
int val = u[C];
if (w[C] - val < R[C])
great = 1;
} else {
int val = -u[C];
if (val + 1 > L[C])
great = 1;
}
}
if (great) {
int mul = i;
for (int j = 1; j <= k; j ++) cur[j] = R[j] - L[j] + 1;
bool keep = 1;
while (keep) {
int val = 1;
for (int j = 1; j <= k; j ++) {
if (j == C) continue;
val = 1ll * val * cur[j] % mod;
}
ans = (ans + 1ll * val * mul) % mod;
mul = (mul + n) % mod;
for (int j = 1; j <= k; j ++) {
cur[j] -= abs(v[j]);
if (cur[j] <= 0) {
keep = 0;
break;
}
}
}
}
if (u[C]) {
if (u[C] > 0) {
int val = u[C];
if (w[C] - val < R[C])
R[C] = w[C] - val;
} else {
int val = -u[C];
if (val + 1 > L[C])
L[C] = val + 1;
}
}
if (L[C] > R[C]) {
printf("%d\n", ans);
return 0;
}
}
printf("%d\n", ans);
return 0;
}
// I hope changle_cyx can pray for me.
算法二
特殊性質:\(w_i \leq 10^9\)。
看起來「算法一」非常有前途,考慮優化。
優化的重點其實在於計算模 \(n\) 的所有同余類 \(\overline{1}, \overline{2}, \cdots, \overline{n - 1}, \overline{0}\) 對答案的貢獻。
對於第二周期中的第 \(i\) 步(其中 \(n < i \leq 2n\)),若第 \(i\) 步是一個 " 特殊步 "。
考慮第 \(n \ast x + (i - n)\) 步,若該步也是一個 " 特殊步 ",則不難發現,該步對答案的貢獻是一個與 \(x\) 有關的多項式,可以考慮用 \(f(x)\) 表示:
注意到 \(f(x)\) 的每個乘積項,都是關於 \(x\) 的一次二項式。
可以考慮將這 \(k\) 個乘積項卷在一起,即可得到一個 \(k\) 次多項式 \(f(x)\)。
至於計算出 \(f(x)\) 的系數,直接暴力卷即可,因為 \(k\) 很小,所以復雜度肯定是可以接受的。
多項式乘法(卷積):
- 給定兩個多項式 \(f(x)\) 與 \(g(x)\):
\[f(x) = \sum\limits_{i = 0}^n a_i \ast x^i \]\[g(x) = \sum\limits_{j = 0}^m b_j \ast x^j \]
- 要計算多項式 \(Q(x) = f(x) \ast g(x)\):
\[Q(x) = \sum\limits_{i = 0}^n \sum\limits_{j = 0}^m a_i \ast b_j \ast x^{i + j} \]
考慮計算貢獻。設模 \(m\) 的同余類 \(\overline{i}\) 從第二周期開始,最多要計算到第 \(E\) 輪,不難得到:
此時貢獻為:
將 \(f(x)\) 用 \(\sum\limits_{i = 0}^k a_i \ast x^i\) 替換,得:
交換枚舉順序,得:
至於要快速計算函數 \(S_k(n) = \sum\limits_{i = 1}^n i^k\),是一個非常經典的問題。
詳見 CF622F The Sum of the k-th Powers。
本篇題解中,代碼給出的是 " 拉格朗日插值 "。
代碼中並沒有直接拉格朗日插值,而是使用拉格朗日插值先預處理出 \(S_k(n)\) 所對應的關於 \(n\) 的 \(k + 1\) 次多項式,然后再代入 \(x\) 進行計算。
時間復雜度 \(\mathcal{O}(nk^2)\)。
#include <cstdio>
#include <cstring>
#include <algorithm>
using namespace std;
inline int read() {
int x = 0, f = 1; char s = getchar();
while (s < '0' || s > '9') { if (s == '-') f = -f; s = getchar(); }
while (s >= '0' && s <= '9') { x = x * 10 + s - '0'; s = getchar(); }
return x * f;
}
int power(int a, int b, int p) {
int ans = 1;
for (; b; b >>= 1) {
if (b & 1) ans = 1ll * ans * a % p;
a = 1ll * a * a % p;
}
return ans;
}
const int N = 500100, SIZE = 20;
const int mod = 1e9 + 7;
int n, k;
int w[SIZE];
int c[N], d[N];
int L[SIZE], R[SIZE];
int v[SIZE];
int u[SIZE];
struct polynomial {
int m;
int num[SIZE];
polynomial() {
m = 0;
memset(num, 0, sizeof(num));
}
polynomial(int a, int b, int c) { m = a, num[0] = b, num[1] = c; }
};
polynomial operator + (polynomial a, polynomial b) {
polynomial c;
c.m = a.m;
for (int i = 0; i <= a.m; i ++)
c.num[i] = (a.num[i] + b.num[i]) % mod;
return c;
}
polynomial operator * (polynomial a, polynomial b) {
polynomial c;
c.m = a.m + b.m;
for (int i = 0; i <= a.m; i ++)
for (int j = 0; j <= b.m; j ++)
c.num[i + j] = ((c.num[i + j] + 1ll * a.num[i] * b.num[j]) % mod + mod) % mod;
return c;
}
int calc(polynomial a, int x) {
int ans = 0;
for (int i = 0, val = 1; i <= a.m; i ++, val = 1ll * val * x % mod)
ans = (ans + 1ll * a.num[i] * val) % mod;
return ans;
}
polynomial g[SIZE];
int val[SIZE];
void Lagrange(int K) {
val[0] = 0;
for (int i = 1; i <= K + 2; i ++) val[i] = (val[i - 1] + power(i, K, mod)) % mod;
g[K].m = K + 1;
for (int i = 1; i <= K + 2; i ++) {
polynomial p = polynomial(0, 1, 0);
int q = 1;
for (int j = 1; j <= K + 2; j ++) {
if (i == j) continue;
p = p * polynomial(1, mod - j, 1);
q = 1ll * q * (((i - j) + mod) % mod) % mod;
}
p = p * polynomial(0, power(q, mod - 2, mod), 0);
p = p * polynomial(0, val[i], 0);
g[K] = g[K] + p;
}
}
int ans;
int main() {
n = read(), k = read();
for (int i = 1; i <= k; i ++)
w[i] = read();
for (int i = 1; i <= n; i ++)
c[i] = read(), d[i] = read();
// judgment has no answer.
for (int i = 1; i <= k; i ++)
L[i] = 1, R[i] = w[i];
for (int i = 1; i <= n; i ++) {
v[c[i]] += d[i];
if (v[c[i]]) {
if (v[c[i]] > 0) {
int val = v[c[i]];
if (w[c[i]] - val < R[c[i]]) R[c[i]] = w[c[i]] - val;
} else {
int val = -v[c[i]];
if (val + 1 > L[c[i]]) L[c[i]] = val + 1;
}
}
}
bool flag1 = 1;
for (int i = 1; i <= k; i ++)
if (L[i] > R[i]) flag1 = 0;
bool flag2 = 1;
for (int i = 1; i <= k; i ++)
if (v[i]) flag2 = 0;
if (flag1 && flag2) {
puts("-1");
return 0;
}
// work
for (int i = 1; i <= k; i ++)
L[i] = 1, R[i] = w[i];
for (int i = 1; i <= n; i ++) {
int C = c[i], D = d[i];
u[C] += D;
bool great = 0;
if (u[C]) {
if (u[C] > 0) {
int val = u[C];
if (w[C] - val < R[C])
R[C] = w[C] - val, great = 1;
} else {
int val = -u[C];
if (val + 1 > L[C])
L[C] = val + 1, great = 1;
}
}
if (great) {
int val = 1;
for (int j = 1; j <= k; j ++) {
if (j == C) continue;
val = 1ll * val * (R[j] - L[j] + 1) % mod;
}
ans = (ans + 1ll * val * i) % mod;
if (L[C] > R[C]) {
printf("%d\n", ans);
return 0;
}
}
}
for (int i = 0; i <= k; i ++)
Lagrange(i);
for (int i = n + 1; i <= 2 * n; i ++) {
int C = c[i - n], D = d[i - n];
u[C] += D;
bool great = 0;
if (u[C]) {
if (u[C] > 0) {
int val = u[C];
if (w[C] - val < R[C])
great = 1;
} else {
int val = -u[C];
if (val + 1 > L[C])
great = 1;
}
}
if (great) {
int Round = 0x3f3f3f3f;
for (int j = 1; j <= k; j ++) {
if (!v[j]) continue;
Round = min(Round, ((R[j] - L[j]) / abs(v[j])) + 1);
}
polynomial f = polynomial(0, 1, 0);
f = f * polynomial(1, i - n, n);
for (int j = 1; j <= k; j ++) {
if (j == C) continue;
f = f * polynomial(1, R[j] - L[j] + 1 + abs(v[j]), mod - abs(v[j]));
}
for (int j = 0; j <= k; j ++)
ans = (ans + 1ll * f.num[j] * calc(g[j], Round)) % mod;
}
if (u[C]) {
if (u[C] > 0) {
int val = u[C];
if (w[C] - val < R[C])
R[C] = w[C] - val;
} else {
int val = -u[C];
if (val + 1 > L[C])
L[C] = val + 1;
}
}
if (L[C] > R[C]) {
printf("%d\n", ans);
return 0;
}
}
printf("%d\n", ans);
return 0;
}
// I hope changle_cyx can pray for me.