【比賽題解】NOIP2020 題解


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. 移球游戲

感謝 xyz32768 的指導,以及他的題解

算法一

特殊性質:\(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\) 的球移動到同一根柱子上:

  1. 枚舉 \(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\) 號柱。

  2. 枚舉 \(i = 1 \to (n - 1)\)
    \(i\) 號柱子最頂端所有顏色為 \(n\) 的球都移動到 \(n + 1\) 號柱上。

  3. 依次考慮 \(n\) 號柱子里的每一個球。

    若該球的顏色為 \(n\),則將其移動到 \(n + 1\) 號柱。
    若該球的顏色不為 \(n\),則將其補到 \(1\)\(n - 1\) 號柱里的一個空位上。

這樣的話就得到了一個規模為 \(n - 1\) 的子問題,直接遞歸調用到 \(1\) 即可。

復雜度是 \(\mathcal{O}(n^2m)\) 的。
來計算一下該算法的嚴格操作數。

\(g(n)\) 表示解決一個規模為 \(n\) 的問題,且不向下遞歸調用時需要的操作數,則:

\[\begin{aligned}g(n)& = \sum\limits_{i = 1}^{n - 1} (2m + 2c_i) + \sum\limits_{i = 1}^{n - 1} c_i + m\\& = 2m(n - 1) + 3\sum\limits_{i = 1}^{n - 1} c_i + m\\& = 2nm - m + 3\sum\limits_{i = 1}^{n - 1} c_i\end{aligned} \]

在最壞情況下,\(\sum\limits_{i = 1}^{n - 1} c_i = m\),則:

\[\begin{aligned} g(n) & = 2nm - m + 3m \\ & = 2nm + 2m \\ & = 2m(n + 1) \end{aligned} \]

此時:

\[\begin{aligned}\text{answer}& = \sum\limits_{i = 2}^n g(i)\\& = \sum\limits_{i = 2}^n 2m(i + 1) \\& = 2m \sum\limits_{i = 2}^n (i + 1)\\& = 2m [\frac{(n + 1)(n + 2)}{2} - 3]\\& = m(n - 1)(n + 4)\end{aligned} \]

發現剛好可以過掉 \(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) = [n \ast x + (i - n)] \prod\limits_{j \neq c_i}[-|v_j| \ast x + (R_j - L_j + 1 + |v_j|)] \]

注意到 \(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\) 輪,不難得到:

\[E = \min\limits_{1 \leq i \leq k, v_i \neq 0} \left\{ \left\lfloor\frac{R_i - L_i}{|v_i|}\right\rfloor + 1 \right\} \]

此時貢獻為:

\[\sum\limits_{x = 1}^E f(x) \]

\(f(x)\)\(\sum\limits_{i = 0}^k a_i \ast x^i\) 替換,得:

\[\sum\limits_{x = 1}^E \sum\limits_{i = 0}^k a_i \ast x^i \]

交換枚舉順序,得:

\[\sum\limits_{i = 0}^k \left( a_i \ast \sum\limits_{x = 1}^E x^i \right) \]

至於要快速計算函數 \(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. 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM