Loading

南开 2025 新生赛个人记录

题目质量相当好,除了小插曲有点多.顺序大致按照过题数降序排列.

N 爆炸了,然后个人不会 C,E,S,所以是 16 个题的个人题解.E 只会 \(m\) 是奇数不会 \(m\) 是偶数.不过赛后问了一下 S 是原题 ABC261G,我说这个题号咋这么熟悉,事后发现做过这套题的 H.H 是一道很经典的有向图博弈,可以去看看.

I

\(\sum_{i = 1}^n n^2 + n \bmod 998244353\)

  • \(n \le 10^{18}\)

使用自然数和与自然数平方和公式即可.一开始想了一下 \(n \le 10^{18}\) 乘法溢出了怎么办,后来发现先取模即可…

#include <bits/stdc++.h>
#define int long long 
int read() {
	int x = 0; bool f = true; char ch = getchar();
	for (; !isdigit(ch); ch = getchar()) if (ch == '-') f = false;
	for (; isdigit(ch); ch = getchar()) x = (x << 1) + (x << 3) + ch - '0';
	return f ? x : (~(x - 1));
}
const int mod = 998244353;
int pw(int a, int p = mod - 2) {
	int ans = 1;
	for (; p; p >>= 1, (a *= a) %= mod)
		if (p & 1)
			(ans *= a) %= mod;
	return ans;
}

signed main() {
	int n = read() % mod;
	int ans = n * (n + 1) % mod * pw(2) % mod;
	ans += n * (n + 1) % mod * (n + n + 1) % mod * pw(6) % mod;
	printf("%lld\n", ans % mod);
	return 0;
}

F

一个长度为 \(n\) 的序列,每次可以选择满足 \((a_p - a_q)(a_r - a_q) \le 0\) 的一个三元组 \((p, q, r)\),从序列中删去元素 \(a_q\).求能将序列删成的长度最小值.

  • \(n \le 10^7\)
  • \(|a_i| \le 10^7\)

最后的序列不能再做删除操作,故必有任意的 \((a_p - a_q)(a_r - a_q) > 0\).这样的三元对是一个「山峰」形或「山谷」形,可以发现这样的形状很难在一个比较长的序列上,对任意三个点都成立…所以直接猜测最终序列长度是小常数.

分析一下最终序列的形态.首先序列长度是 \(1\) 或者 \(2\) 肯定不能再删.进而 \(n = 1\) 时答案必为 \(1\)\(n \ge 2\) 时答案必 \(\ge 2\)

如果序列长度是 \(3\),它是一个山谷或者山峰,即要么 \(a_1 < a_2 > a_3\),要么 \(a_1 > a_2 < a_3\)

序列长度可以是 \(4\) 吗?手玩几种情况,可以发现 \(a_2\)\(a_3\) 必须取到整个序列的最值(一个最大值,一个最小值),长度 \(4\) 才有可能.而长度 \(> 4\) 不可能.

再来分析原序列,要求尽量删到最小长度.发现 \(a_1\)\(a_n\) 不可被删除.上面的手玩启发我们找 \(a_2, a_3, \ldots, a_{n - 1}\) 这段序列的最值,设最大值为 \(M\) 最小值为 \(m\).如果 \(M > \max(a_1, a_n)\)\(M\) 一定删不掉.如果 \(m < \min(a_1, a_n)\)\(m\) 也一定不可.剩下的元素一定可以被删去.

#include <bits/stdc++.h>
int read() {
	int x = 0; bool f = true; char ch = getchar();
	for (; !isdigit(ch); ch = getchar()) if (ch == '-') f = false;
	for (; isdigit(ch); ch = getchar()) x = (x << 1) + (x << 3) + ch - '0';
	return f ? x : (~(x - 1));
}

const int N = (int)1e7 + 5;
int a[N];

int solve() {
	int n = read(), ans = 2;
	for (int i = 1; i <= n; ++i) a[i] = read();
	if (n == 1) return 1;
	int p = 0, q = 0;
	for (int i = 2; i <= n; ++i) if (a[i] > std :: max(a[1], a[n])) q = 1;
	for (int i = 2; i <= n; ++i) if (a[i] < std :: min(a[1], a[n])) p = 1;
	return ans + p + q;
}

signed main() {
	int T = read();
	while (T--) printf("%d\n", solve());
	return 0;
}

Q

环上的积木大赛

  • \(n \le 10^6\)
  • \(0 \le a_i \le 2000\)

将积木大赛操作逆序,即考虑将一个环状序列 \(a\) 进行若干区间 \(-1\) 操作将 \(a\) 消减为全 \(0\).设下标 \(+1\) 的方向为顺时针,\(-1\) 的方向为逆时针,认为下标有 \(n + 1 = 1\)\(1 - 1 = n\).结论:

  • \(S = \sum_{i = 1}^n \max(a_i - a_{i - 1}, 0)\),即序列 \(a\) 上的环状正差分之和.
  • \(M = \max a\),即序列 \(a\) 的最大值.

则答案为 \(\max (S, M)\).下面证明.

首先证明 \(\max(S, M)\) 是答案的下界.这是因为一次操作对序列的 \(S\)\(M\) 的影响都至多为减 \(1\),而最终全 \(0\) 态满足 \(M = S = 0\)

  • \(M\) 的影响至多减 \(1\) 比较平凡.
  • \(S\) 的影响至多减 \(1\) 是考虑区间减 \(1\) 对差分的影响一定是一个单点 \(+1\) 一个单点 \(-1\),故让 \(S\) 削减最多的策略即让这里的 \(+1\) 无影响(即属于负差分),\(-1\) 有影响(即属于正差分),这个影响最多让 \(S\)\(1\)

然后构造步数为 \(\max(S, M)\) 的方案.只需证明:对任何一个序列 \(a\),存在一步操作,使操作后的序列 \(a'\) 满足 \(\max(S, M)\) 比原先少 \(1\).细化一下得到:

  • \(M > S\),可以进行一步操作令 \(M \gets M - 1\),同时 \(S\) 不增.
  • \(M < S\),可以进行一步操作令 \(S \gets S - 1\),同时 \(M\) 不增(\(M\) 其实不可能增,所以这一点平凡).
  • \(M = S\),可以进行一步操作同时令 \(M \gets M - 1\)\(S \gets S - 1\)

下面分别构造满足条件的操作.

\(M > S\) 时令 \(M \gets M - 1\) 的策略是直接整环 \(-1\) 即可.此时 \(S\) 不变,故不增,完成构造.

这里需要证明整个环上没有 \(0\) 否则整环 \(-1\) 是不合法的.假设整个环上有 \(0\),则必有 \(S \ge M\),因为从 \(0\) 开始累积所有的正差分得到的结果 \(S\) 一定比序列最大值 \(M\) 要大(考虑差分的意义),这与前提 \(M > S\) 矛盾.

\(M < S\) 时令 \(S \gets S - 1\) 的策略.假设我们操作的区间为 \([L, R]\),则要满足 \(a_L > a_{L - 1}\)\(a_R > a_{R + 1}\).这样可以使得 \(-1\) 的是正差分,\(+ 1\) 的是负差分,从而 \(S \gets S - 1\)

这样的 \(L\)\(R\) 一定可以找到,即沿着顺时针走一遍环,一定有增有减.如果没有增减,说明整个环的元素一致,这种情况有 \(S = 0\),如果 \(M > 0\) 我们走 \(M > S\) 的全局 \(-1\) 路线,\(M = 0\) 说明目标已完成,无需进一步构造.

需要注意的是 \([L, R]\) 上不能有 \(0\).很好调整:只需找到任意满足 \(a_L > a_{L - 1}\)\(L\),然后从 \(L\) 顺时针走,直到走到 \(a_p = 0\),此时令 \(R = p - 1\) 即可,则 \([L, R]\) 上无 \(0\) 且也有 \(a_R > a_{R +1} = 0\).如果走了一圈没有零点,则设置任意满足 \(a_R > a_{R + 1}\) 的点即可,没有零点保证了合法性.

\(M = S\) 时令 \(M \gets M - 1\)\(S \gets S - 1\).分类讨论构造:

  • 如果 \(a\) 中有 \(0\),因为 \(S = M\),可以发现从 \(0\) 开始沿着环走一圈,元素一定单调不降,因为一旦降了一次,必有 \(M < S\)(考虑差分意义).所以 \(a\) 中的 \(0\) 构成一个连续段.对除了 \(0\) 连续段外的所有元素 \(-1\),即有 \(M \gets M - 1\)\(S \gets S - 1\)
  • 如果 \(a\) 中无 \(0\),则找到 \(a\) 中任意一个 \(< M\) 的极长段(除非所有元素 \(= M\),否则一定可以找到).这里极长的含义是这一段的两边都紧挨着 \(M\).然后,对这段的补集 \(-1\) 即可.所有的 \(M\) 都会被 \(-1\),故 \(M \gets M - 1\),同时 \(S \gets S - 1\),因为极长 \(< M\) 段爬到 \(M\) 的正差分减少了 \(1\),而另一边 \(M\) 降到 \(< M\) 段属于不影响 \(S\) 的负差分.

综上,我们完成了构造.

#include <bits/stdc++.h>
int read() {
	int x = 0; bool f = true; char ch = getchar();
	for (; !isdigit(ch); ch = getchar()) if (ch == '-') f = false;
	for (; isdigit(ch); ch = getchar()) x = (x << 1) + (x << 3) + ch - '0';
	return f ? x : (~(x - 1));
}

const int N = (int)1e6 + 5;
int a[N], d[N], s[N], t[N];

signed main() {
	int n = read(), ans = 0;
	for (int i = 1; i <= n; ++i) a[i] = read();
	a[0] = a[n];
	for (int i = 1; i <= n; ++i) ans += (d[i] = std :: max(0, a[i] - a[i - 1]));
	for (int i = 1; i <= n; ++i) ans = std :: max(ans, a[i]);
	printf("%d\n", ans);
	return 0;
}

O

给定一颗 \(n\) 个点的树,定义长度为 \(n\) 的点编号排列是好的,当且仅当排列的任意前缀在树上是一个连通块.问有多少个好排列,\(\mod 10^9 + 7\)

  • \(n \le 10^5\)

假设以 \(1\) 为根,排列从 \(1\) 开始,以 \(u\) 为根的子树是 \(T(u)\)

考虑 \(1\) 的每个儿子 \(u\) 时,相当于规定排列上,子树 \(T(u)\) 中的点,\(u\) 必须出现得最早.即对这 \(|T(u)|\) 个点的内部顺序提出了要求,对排列数的贡献是除以 \(|T(u)|\)

对每个点 \(u \ne 1\) 都考虑一遍这个规定,对于点 \(1\) 我们也视作整棵树中的点 \(1\) 必须出现在第一个,则综合考虑 \(n\) 个规定得到的条件和原条件是完全等价的.故以 \(1\) 为起点,答案是 \(n!\) 除以每个点子树的大小.枚举起点可以做到 \(\Theta(n^2)\)

考虑换根 dp,假设 \(T(r, u)\) 表示以 \(r\) 为根时 \(u\) 的子树.先计算点 \(1\) 的答案 \(f(1)\).以 \(1\) 为根,假设点 \(u\) 的答案 \(f(u)\) 被计算好,考虑转移到儿子 \(v\)\(f(v)\)\(f(u)\) 相比,所有子树几乎都不变,即几乎对所有 \(x\)\(T(u, x) = T(v, x)\).除了 \(x = u\)\(x = v\) 时.有

  • \(T(u, u) = n\)\(T(v, u) = V \setminus T(1, u)\)\(V\) 是树的点集).
  • \(T(u, v) = T(1, v)\)\(T(v, v) = n\)

\(f(v) = f(u) \times \dfrac{n - |T(1, u)|}{|T(1, v)|}\),所以可以 \(\Theta(n)\) 计算.

#include <bits/stdc++.h>
#define int long long
int read() {
	int x = 0; bool f = true; char ch = getchar();
	for (; !isdigit(ch); ch = getchar()) if (ch == '-') f = false;
	for (; isdigit(ch); ch = getchar()) x = (x << 1) + (x << 3) + ch - '0';
	return f ? x : (~(x - 1));
}

const int N = (int)2e5 + 5;
const int mod = (int)1e9 + 7;
std :: vector <int> T[N];
int siz[N], f[N], inv[N], n;

void dfs(int u, int fa) {
	siz[u] = 1;
	for (int v : T[u]) if (v != fa) {
		dfs(v, u);
		siz[u] += siz[v];
	}
	(f[1] *= inv[siz[u]]) %= mod;
}

void dfscg(int u, int fa) {
	for (int v : T[u]) if (v != fa) {
		f[v] = f[u] * siz[v] % mod * inv[n - siz[v]] % mod;
		dfscg(v, u);
	}
}

signed main() {
	n = read();
	int fac = 1;
	for (int i = 1; i <= n; ++i) (fac *= i) %= mod;
	inv[1] = 1;
	for (int i = 2; i <= n; ++i) inv[i] = mod - mod / i * inv[mod % i] % mod;
	for (int i = 1; i < n; ++i) {
		int u = read(), v = read();
		T[u].push_back(v);
		T[v].push_back(u);
	}

	f[1] = 1;
	dfs(1, 0); dfscg(1, 0);
	int ans = 0;
	// for (int u = 1; u <= n; ++u) std :: cout << f[u] << std :: endl;
	for (int u = 1; u <= n; ++u)
		(ans += fac * f[u] % mod) %= mod;
	printf("%lld\n", ans);
	return 0;
}

B

给定 \(n\) 张折线图,每张折线图有 \(m\) 个点,第 \(i\) 张折线图的第 \(j\) 个点坐标 \((j, y_{i, j})\).求最少用几张图纸,可以将 \(n\) 张折线图分配到图纸上,且图纸上的折线图无交.

  • \(n \le 100\)
  • \(m \le 30\)
  • \(|y_{i, j}| \le 10^4\)

一个图纸上的所有折线图无交,当且仅当这些折线图在每个横坐标上的纵坐标都是严格单调的.考虑建图,若两个折线图 \(p\)\(q\) 满足 \(p\) 一定在 \(q\) 的下方,连边 \(p \to q\).建图复杂度 \(\Theta(n^2 m)\)

一些折线图能画在一张纸上当且仅当这些折线图在 DAG 上构成了一条链.故变成 DAG 最小链覆盖问题,结论是等于 $n - $ 二分图最大匹配数.

#include <bits/stdc++.h>
int read() {
	int x = 0; bool f = true; char ch = getchar();
	for (; !isdigit(ch); ch = getchar()) if (ch == '-') f = false;
	for (; isdigit(ch); ch = getchar()) x = (x << 1) + (x << 3) + ch - '0';
	return f ? x : (~(x - 1));
}

const int N = 105, M = 35;
int a[N][M], n, m;

int cmp(int x, int y) {
	if (a[x][1] > a[y][1]) {
		for (int j = 2; j <= m; ++j)
			if (a[x][j] <= a[y][j])
				return 0;
		return 1;
	} else if (a[x][1] < a[y][1]) {
		for (int j = 2; j <= m; ++j)
			if (a[x][j] >= a[y][j])
				return 0;
		return -1;
	} else return 0;
}

std :: vector <int> G[N << 1];
int mat[N << 1], vis[N << 1];

bool dfs(int u, int t) {
	for (int v : G[u]) {
		if (vis[v] == t) continue;
		vis[v] = t;
		if (!mat[v] || dfs(mat[v], t)) {
			mat[v] = u; mat[u] = v;
			return true; 
		}
	}
	return false;
}

signed main() {
	n = read(); m = read();
	for (int i = 1; i <= n; ++i)
		for (int j = 1; j <= m; ++j)
			a[i][j] = read();
	for (int u = 1; u <= n; ++u)
		for (int v = u + 1; v <= n; ++v) {
			int o = cmp(u, v);
			if (!o) continue;
			if (o == 1) G[u].push_back(v + n);
			else G[v].push_back(u + n);
		}

	int ans = n;
	for (int u = 1; u <= n; ++u) if (dfs(u, u)) --ans;
	printf("%d\n", ans);
	return 0;
}

R

给定一棵 \(n\) 个点的树 \(T = (V, E)\)\(q\) 次询问.每次询问给定一个点集 \(S\),求 \(T\) 在点集 \(V \setminus S\) 上的导出子图的连通块数量.

  • \(n \le 10^6\)
  • \(q \le 10^6\)\(\sum |S| \le 10^6\)

首先需要注意树的导出子图一定是森林.森林的连通块数(即树的数量)就是点数减去边数,因为每多一棵树,边数与点数的差就会多 \(1\)

所求的导出子图点数是 \(n - |S|\),因此只需考虑一个问题:如何快速查询 \(V \setminus S\) 的导出子图边数?换言之,即满足 \(u, v \not \in S\) 的边 \((u, v)\) 的数量.

需要注意的是用以保证复杂度的是 \(\sum |S|\),这意味着我们要用 \(|S|\) 量级的时间复杂度回答单次询问,而不是 \(n - |S|\).因此这里考虑容斥,或者说正难则反.考虑计数满足一个点在 \(S\) 中的边数,然后用 \(n - 1\) 减去它即可.

先对 \(|S|\) 中的点计算一下度数和.此时两个端点都在 \(|S|\) 中的点会被统计两次,需要减去.两个端点都在 \(|S|\) 中仅当 \(u \in S\)\(fa_u \in S\),可以在 \(\Theta(|S|)\) 的时间复杂度解决.

#include <bits/stdc++.h>
int read() {
	int x = 0; bool f = true; char ch = getchar();
	for (; !isdigit(ch); ch = getchar()) if (ch == '-') f = false;
	for (; isdigit(ch); ch = getchar()) x = (x << 1) + (x << 3) + ch - '0';
	return f ? x : (~(x - 1));
}

const int N = (int)1e6 + 5;
std :: vector <int> T[N];
int fa[N];

void dfs(int u, int f) {
	fa[u] = f;
	for (int v : T[u]) if (v != f)
		dfs(v, u);
}

signed main() {
	int n = read(), q = read();
	for (int i = 1; i < n; ++i) {
		int u = read(), v = read();
		T[u].push_back(v);
		T[v].push_back(u);
	}

	dfs(1, 0);

	while (q--) {
		int k = read();
		std :: set <int> a;
		while (k--) a.insert(read());
		int sd = 0, se = 0;
		for (int x : a) sd += (int)T[x].size();
		for (int x : a) if (a.count(fa[x]))
				++se;
		printf("%d\n", sd - se - (int)a.size() + 1);
	}
	return 0;
}

H

给定 \(n\).有一个集合 \(S\),初始为 \(S = \{1, 2, 3, \ldots, n\}\).对于满足 \(0 \le a_i \le i\) 的非负整数序列 \(a\),定义 \(f(a)\) 为下述操作的方案数:

总共进行 \(n\) 次操作,第 \(i\) 次操作的参数为 \(a_i\)

  • \(a_i = 0\),这一次操作无任何效果.
  • 否则,令 \(S' = S \cap [a_i, i]\).根据后续说明的操作流程,可以推得一定有 \(i \in S'\)
  • 如果 \(S' = \{i\}\),则在 \(S\) 中删去元素 \(i\)
  • 如果 \(|S'| \ge 2\),则找到 \(S'\)最大的两个元素,并在两个元素中选择任一,从 \(S\) 中删除.

由于第二种情况选择的元素不唯一,会产生不同的删元素方案.两种方案不同,当且仅当某一步操作选择的元素不同.

求所有满足条件的序列 \(a\)\(f(a)\) 之和,\(\mod 998244353\)

  • \(n \le 1000\)

首先观察一下 \(f(a)\) 的形态.\(i\)\(1 \to n\) 考虑,故 \(S\) 中属于 \([a_i, i]\) 的元素里,\(i\) 一定还在且是最大的那个.可以考虑枚举次大值,是一个时间复杂度为 \(\Theta(n^2)\) 的结构.

另外可以发现 \(a_i\) 具有单调性.具体而言,将 \(a_i\) 调小(不考虑 \(0\)),原来成立的取元素方案一定仍然成立.换一个视角,我们第 \(i\) 步取了元素 \(x\),只需令 \(1 \le a_i \le x\),即 \(a_i\)\(x\) 种填法.这启示我们直接考虑对从 \(1 \to n\) 的取物品过程进行动态规划,而 \(a_i\) 的填法只是一个简单的系数添加工作.

\(f(i, j)\) 表示考虑了前 \(i\) 步操作,第 \(i\) 步操作后最大元素为 \(j\).若第 \(i\) 步操作后 \(S = \varnothing\),则 \(j = 0\)

这样设计状态的好处是:考虑第 \(f(i, \cdots)\)\(f(i - 1, j)\) 的转移过程,\(S'\) 中最大的两个元素被完全确定下来了,就是 \(\{j, i\}\)(若 \(j = 0\) 则无 \(j\)),这极大方便我们的转移.

考虑 \(f(i, j)\) 的转移方程.

  • \(j \ne i\),即第 \(i\) 步操作后最大元素 \(j \ne i\),说明第 \(i\) 步取了 \(i\),故 \(1 \le a_i \le i\)\(f(i, j) = f(i - 1, j) \times i\).可以适用 \(j = 0\) 的情形.
  • \(j = i\),则第 \(i\) 步操作可能是取了其它任意元素 \(k\),也可能是没有取.故

\[f(i, i) = \sum_{k = 0}^{i - 1} f(i - 1, k) \times (k + 1) \]

\(\Theta(n^2)\) 转移即可.

#include <bits/stdc++.h>
#define int long long
int read() {
	int x = 0; bool f = true; char ch = getchar();
	for (; !isdigit(ch); ch = getchar()) if (ch == '-') f = false;
	for (; isdigit(ch); ch = getchar()) x = (x << 1) + (x << 3) + ch - '0';
	return f ? x : (~(x - 1));
}

const int N = 1005;
const int mod = 998244353;
int f[N][N];

signed main() {
	int n = read();
	f[0][0] = 1;
	for (int i = 1; i <= n; ++i) {
		for (int j = 0; j < i; ++j) {
			f[i][j] = f[i - 1][j] * i % mod;
			(f[i][i] += f[i - 1][j] * (j + 1) % mod) %= mod;
		}
	}
	int ans = 0;
	for (int j = 0; j <= n; ++j) (ans += f[n][j]) %= mod;
	printf("%lld\n", ans);
	return 0;
}

J

给定正整数 \(n, m\),在平面直角坐标系上,定义合法整点为所有的 \((x, y)\),满足 \(0 \le x \le n\)\(0 \le y \le m\)\(x, y \in \N\)

\(q\) 次询问,每次询问给定一个合法整点 \((x, y)\).对于每次询问,回答有多少个正方形,满足其中一个顶点是 \((x, y)\),且其余三个顶点也是合法整点.两个正方形相同,当且仅当它们在坐标系上完全重合.

  • \(n, m \le 10^9\)
  • \(q \le 10^6\)

这类题有个套路是考虑正方形的横平竖直框架,即用最小的一个横平竖直框框上一个斜正方形,则问题转化为:有多少个横平竖直的正方形,边界上有 \((x, y)\)

\((X, Y, L)\) 描述一个左下顶点为 \((X, Y)\),边长为 \(L\) 的横平竖直正方形,则其合法当且仅当

  • \((X, Y)\) 是合法整点.
  • \((X + L, Y + L)\) 是合法整点.
  • \(L > 0\)

\(X \in [0, n - L]\)\(Y \in [0, m - L]\)

然后考虑分两种情况统计,一是 \((x, y)\)\((X, Y, L)\) 的边上,不与顶点重合,二是 \((x, y)\)\((X, Y, L)\) 的顶点上.

  • \((x, y)\)\((X, Y, L)\)下边界不可以与顶点重合.即 \(X + a = x \iff X = x - a\)\(Y = y\).其中 \((X, Y, L)\) 合法且 \(a \in [1, L - 1]\),设合法的 \((X, Y, L)\) 数量为 \(f(n, m, x, y)\)
  • \((x, y)\)\((X, Y, L)\)上边界不可以与顶点重合?根据对称性,将整个坐标系上下翻转,则问题变为下边界,故答案为 \(f(n, m, x, 2m - y)\)
  • 类似地,左边界的答案是 \(f(m, n, m - y, x)\)右边界的答案是 \(f(m, n, y, n - x)\)

二是顶点的情况,这个随便统计一下即可,只需要其作为左上,左下,右上,右下四个顶点时,\(L\) 最多可以伸到哪里.

所以现在重点关注 \(f(n, m, x, y)\) 的求法.考虑枚举 \(X = x - a\) 中的 \(a\),下界为 \(1\),上界应为 \(\min(x, m - y - 1)\).对于一个 \(a\),合法的 \(L\) 取值范围是 \([a + 1, \min(m - y, a + n - x)]\).这里的值可以通过画图得到(注意不要让 \((x, y)\) 落到端点).故

\[f(n, m, x, y) = \sum_{a = 1}^{\min(x, m - y - 1)} \min(m - y, a + n - x) - a \]

考虑将 \(a\) 并入 \(\min\)

\[f(n, m, x, y) = \sum_{a = 1}^{\min(x, m - y - 1)} \min(m - y - a, n - x) \]

简写一些参数,令 \(p = n - x\)\(q = m - y\),则

\[f(n, m, x, y) = \sum_{a = 1}^{\min(x, q - 1)} \min(q - a, p) \]

这个东西是明显可以 \(\Theta(1)\) 求解的,因为它是一个分段函数的前缀和,一段常值,一段一次.所以整个题 \(\Theta(q)\) 做完.

因为赛时思路跟上面的不太一样,先不放代码了.

A

给定一棵 \(n\) 个点的树,边和点均有权,点权为 \(a_u\)\(q\) 次询问,每次给定 \(x\)\(z\),求 \(\max_{1 \le y \le n} a_x + a_y + a_z - d(x, y) - d(y, z)\),其中 \(d(u, v)\) 是路径 \(u \leftrightsquigarrow v\) 的距离,即简单路径边权和.

  • \(n \le 10^5\)
  • \(a_u, w \le 10^9\).(\(w\) 是边权)

注意到 \(x\)\(z\) 确定,整个式子的很多部分是确定的.刻画一下 \(d(x, y) + d(y, z)\):从 \(y\) 向路径 \(x \leftrightsquigarrow z\) 的方向走,可以走到路径上的一个点 \(u\),这个点是唯一的(得益于树的高度路径唯一性).于是 \(d(x, y) + d(y, z) = d(x, z) + 2d(u, y)\).因此只需最大化 \(a_y - 2d(u, y)\)

这是什么意思呢?假设我们求出 \(f(u) = \max_{1 \le y \le n}( a_y - 2d(u, y))\),则问题转化为一个对 \(f\) 求链 max 的工作.由于 \(f\) 是静态的,倍增应对多次询问便有 \(\Theta(q \log n)\) 的复杂度.

考虑求 \(f\) 发现是一个可以换根 dp 的结构.具体而言,我们可以以 \(1\) 为根,先求一个 \(g(u) = \max_{y \in T(u)} (a_y - 2d(u, y))\),其中 \(T(u)\) 为以点 \(u\) 为根的子树.则 \(g(1) = f(1)\),而父子对 \((u, v)\) 上有

\[f(v) = \max(g(v), f(u) - 2d(u, v)) \]

这里的 \(d(u, v)\) 其实就是 \(u - v\) 树边的边权,\(f(u) - 2d(u, v)\) 的作用是将 \(V \setminus T(v)\) 部分全部考虑到,\(g(v)\) 的作用是将 \(T(v)\) 部分考虑到,\(V\) 是树的点集.

虽然 \(f(u)\) 对应的 \(\argmax\)(即 \(f(u)\) 所对应的 \(y\))可能属于 \(T(v)\) 而不是 \(V \setminus T(v)\),但对转移没有影响,因为它相当于相对 \(g(v)\) 多浪费了一些 \(d(u, v)\) 的距离,会被 \(g(v)\) 覆盖.

复杂度 \(\Theta((n + q) \log n)\)

#include <bits/stdc++.h>
#define int long long
int read() {
	int x = 0; bool f = true; char ch = getchar();
	for (; !isdigit(ch); ch = getchar()) if (ch == '-') f = false;
	for (; isdigit(ch); ch = getchar()) x = (x << 1) + (x << 3) + ch - '0';
	return f ? x : (~(x - 1));
}

const int N = (int)2e5 + 5, LG = 25;
typedef std :: pair <int, int> pii;
std :: vector <pii> T[N];
int a[N], dis[N], dep[N], far[N][LG];
int g[N], f[N], fmr[N][LG], n;

void dfs(int u, int f) {
	g[u] = a[u];
	far[u][0] = f;
	for (int i = 1; i <= 20; ++i)
		far[u][i] = far[far[u][i - 1]][i - 1];
	for (pii e : T[u]) if (e.first != f) {
		int v = e.first, w = e.second;
		dep[v] = dep[u] + 1;
		dis[v] = dis[u] + w;
		dfs(v, u);
		g[u] = std :: max(g[u], g[v] - 2 * w);
	}
}

void dfscg(int u) {
	// f[u] ok, -> f[v]
	for (int i = 1; i <= 20; ++i)
		fmr[u][i] = std :: max(fmr[u][i - 1], fmr[far[u][i - 1]][i - 1]);
	for (pii e : T[u]) if (e.first != far[u][0]) {
		int v = e.first, w = e.second;
		fmr[v][0] = f[v] = std :: max(g[v], f[u] - 2 * w);
		dfscg(v);
	}
}

int query(int u, int v) {
	int ext = a[u] + a[v] - dis[u] - dis[v]; // += 2 * dis[lca] + f
	if (dep[u] < dep[v]) std :: swap(u, v);
	int ans = -LONG_LONG_MAX;
	for (int i = 20; ~i; --i)
		if (dep[far[u][i]] >= dep[v]) {
			ans = std :: max(ans, fmr[u][i]);
			u = far[u][i];
		}
	
	if (u == v) return ext + 2 * dis[u] + std :: max(ans, f[u]);

	for (int i = 20; ~i; --i) if (far[u][i] != far[v][i]) {
		ans = std :: max({ans, fmr[u][i], fmr[v][i]});
		u = far[u][i]; v = far[v][i];
	}
	ans = std :: max({ans, f[u], f[v]});
	u = far[u][0]; v = far[v][0];
	ans = std :: max(ans, f[u]);
	return ext + 2 * dis[u] + ans;
}

signed main() {
	n = read(); int q = read();
	for (int u = 1; u <= n; ++u) a[u] = read();
	for (int i = 1; i < n; ++i) {
		int u = read(), v = read(), w = read();
		T[u].emplace_back(v, w);
		T[v].emplace_back(u, w);
	}

	dfs(1, 0);
	fmr[1][0] = f[1] = g[1];
	dfscg(1);
	while (q--) {
		int u = read(), v = read();
		printf("%lld\n", query(u, v));
	}
	return 0;
}

T

给定一个非常大的正整数 \(A\),保证存在唯一的 \(M < N \le 10^6\) 使得 \(A = \dfrac{N!}M\),多次询问给定 \(A\),求出 \(N\)\(M\)

  • \(l(A)\) 表示 \(A\) 的位数,有 \(\sum l(A) \le 31323380\)

这种比较神秘的题目一般是类似哈希的想法:舍弃刻画完整的信息,考虑通过部分信息锁定答案.注意到 \(A\) 的位数其实就是一个信息.设读入的 \(A\) 长度为 \(l\),则 \(\lg \dfrac{n!} m \in [l - 1, l)\).故 \(\lg n!\) 的一个下界是 \(l - 1\),据此找到 \(n\) 的下界 \(n'\)

然后考虑数字对 \(998244353\) 取模的结果 \(x\),则可以在取模意义下试图还原.具体而言,\(m = \dfrac{n!} A\),可以尝试模意义下 \(n!\) 的结果乘以 \(x\) 的逆元,还原得到的 \(m < n\) 则证明成功.

从上面找到的下界 \(n'\) 开始往上扫,直到扫到一个 \(n\) 使得还原的 \(m\) 合法停止扫描即可.由于保证有解,这个做法的时间复杂度不会太高.

#include <bits/stdc++.h>
#define int long long
int read() {
	int x = 0; bool f = true; char ch = getchar();
	for (; !isdigit(ch); ch = getchar()) if (ch == '-') f = false;
	for (; isdigit(ch); ch = getchar()) x = (x << 1) + (x << 3) + ch - '0';
	return f ? x : (~(x - 1));
}
inline std :: string rest() {
	std :: string s;
	char ch = getchar();
	for (; !isgraph(ch); ch = getchar()) ;
	for (; isgraph(ch); ch = getchar()) s.push_back(ch);
	return s;
}

const int N = (int)5e6 + 5;
int fac[N];
const int mod = 998244353;

int pw(int a, int p) {
	int ans = 1;
	for (; p; p >>= 1, (a *= a) %= mod)
		if (p & 1)
			(ans *= a) %= mod;
	return ans;
}

double g[N];

signed main() {
	fac[0] = 1;
	for (int i = 1; i <= (int)5e6; ++i) fac[i] = fac[i - 1] * i % mod;
	for (int i = 1; i <= (int)5e6; ++i) g[i] = g[i - 1] + log10((double)i);
	int T = read();
	while (T--) {
		std :: string s = rest();
		int x = 0;
		for (char ch : s)
			x = ((x * 10) + (ch ^ '0')) % mod;
		// (n - 1)! <= a <= n!
		// g[n - 1] <= a <= g[n]
		int stn = std :: lower_bound(g + 1, g + (int)5e6, (int)s.length() - 1) - g;
		for (int n = stn; ; ++n) {
			// std :: cout << n << std :: endl;
			int m = fac[n] * pw(x, mod - 2) % mod;
			if (m && m < n) {
				printf("%lld %lld\n", n, m);
				break;
			}
		}
	}
	return 0;
}

L

给定字符串 \(S\) 和一个字典 \(D\),问最少使用多少次 \(D\) 中的单词可以拼出 \(S\)单词可以重叠

  • \(|S| \le 3 \times 10^5\)
  • 字典中的单词长度和不超过 \(3 \times 10^5\)

\(S\) 的每个位置 \(i\),求以 \(i\) 结尾的最长单词长度 \(g_i\).因为在相同的出现位置,用最长的单词是最优的.\(g_i\) 的求解是 acam 的经典用例.

然后设计 dp,设 \(f(i)\) 表示拼出前缀 \(S[1\cdots i]\) 的最小代价,则 \(f_i = 1 + \min_{j = i - g_i}^{i - 1} f(j)\).线段树维护最小值即可.

复杂度 \(\Theta(|S|\log |S|)\)

#include <bits/stdc++.h>
int read() {
	int x = 0; bool f = true; char ch = getchar();
	for (; !isdigit(ch); ch = getchar()) if (ch == '-') f = false;
	for (; isdigit(ch); ch = getchar()) x = (x << 1) + (x << 3) + ch - '0';
	return f ? x : (~(x - 1));
}
std :: string rest() {
	std :: string s;
	char ch = getchar();
	for (; !isgraph(ch); ch = getchar()) ;
	for (; isgraph(ch); ch = getchar()) s.push_back(ch);
	return s;
}

const int N = (int)4e5 + 5, L = (int)4e5 + 5, C = 30;
int g[N], f[N];

namespace acam {
	struct node {
		int son[C];
		int fail;
	} t[L];

	int tnt = 0, gac[N];

	void insert(std :: string s) {
		int u = 0;
		for (char ch : s) {
			int c = ch - 'a';
			if (!t[u].son[c]) t[u].son[c] = ++tnt;
			u = t[u].son[c];
		}
		gac[u] = std :: max(gac[u], (int)s.length());
	}

	void build() {
		std :: queue <int> q;
		for (int c = 0; c < 26; ++c)
			if (t[0].son[c])
				q.push(t[0].son[c]);
		while (!q.empty()) {
			int u = q.front(); q.pop();
			gac[u] = std :: max(gac[u], gac[t[u].fail]);
			for (int c = 0; c < 26; ++c) {
				int x = t[t[u].fail].son[c];
				if (t[u].son[c]) {
					t[t[u].son[c]].fail = x;
					q.push(t[u].son[c]);
				} else t[u].son[c] = x;
			}
		}
	}
}

namespace segtree {
	struct node {
		int l, r, v;
	} t[N << 2];

	#define ls(p) p << 1
	#define rs(p) p << 1 | 1

	void build(int p, int l, int r) {
		t[p].l = l; t[p].r = r;
		if (l == r) {
			t[p].v = 1145141919;
			return ;
		}
		int mid = (l + r) >> 1;
		build(ls(p), l, mid);
		build(rs(p), mid + 1, r);
		t[p].v = std :: min(t[ls(p)].v, t[rs(p)].v);
	}

	void update(int p, int x, int v) {
		int l = t[p].l, r = t[p].r;
		// std :: cout << l << ' ' << r << std :: endl;
		if (l == r) {
			t[p].v = v;
			return ;
		}
		int mid = (l + r) >> 1;
		if (x <= mid) update(ls(p), x, v);
		else update(rs(p), x, v);
		t[p].v = std :: min(t[ls(p)].v, t[rs(p)].v);
	}

	int query(int p, int L, int R) {
		int l = t[p].l, r = t[p].r;
		if (l == L && R == r) return t[p].v;
		int mid = (l + r) >> 1;
		if (R <= mid) return query(ls(p), L, R);
		else if (L > mid) return query(rs(p), L, R);
		else return std :: min(
			query(ls(p), L, mid), query(rs(p), mid + 1, R)
		);
	}
}

signed main() {
	int m = read();
	std :: string s = rest(); int n = (int)s.length();
	s = " " + s;

	for (int i = 1; i <= m; ++i) acam :: insert(rest());
	acam :: build();
	for (int i = 1, u = 0; i <= n; ++i) {
		u = acam :: t[u].son[s[i] - 'a'];
		g[i] = acam :: gac[u];
	}

	segtree :: build(1, 0, n);
	segtree :: update(1, 0, f[0] = 0);
	for (int i = 1; i <= n; ++i) {
		f[i] = segtree :: query(1, i - g[i], i) + 1;
		segtree :: update(1, i, f[i]);
	}

	printf("%d\n", f[n] > n ? -1 : f[n]);
	return 0;
}

M

一个 \(2 \times n\) 的网格图,每个点有点权.对所有 \(s = (1, 1)\) 出发的哈密顿路径,若访问到点 \(u\) 走了 \(x\) 个单位长度,获得 \(\max(a_u - x, 0)\) 个收益.求总收益最大的哈密顿路径.

  • \(n \le 10^5\)
  • \(|a_u| \le \min(3n, 10^5)\)

手玩即可发现,\(2 \times n\) 的,从 \((1, 1)\) 出发的哈密顿路径形态比较有限,总共只有 \(n - 1\) 种.具体而言,我们可以先下-右-上-右走一段 zigzag,然后一直向右,换到另一行,再一直向左,走一个 U 形.考虑枚举 zigzag 和 U 的分界点.前面的代价可以直接累计着算,后面的代价不能枚举(否则 \(\Theta(n^2)\)),但是后面的代价可以经过一些计算(具体省略,因为细节比较繁杂,自己推更合适),划归成这种结构的问题:

  • 一个数列 \(a_i\),多次给出 \(x\),快速求解 \(\max(a_i + f(i) - x, 0)\) 之和.

如果我们支持快速查询所有满足 \(\ge x\)\(a_i + f(i)\)\(c\) 个,以及 \(\ge x\)\(a_i + f(i)\) 之和为 \(s\),则答案为 \(s - cx\)\(s\)\(c\) 的快速查询可用树状数组维护.

复杂度 \(\Theta(n \log n)\)

#include <bits/stdc++.h>
#define int long long
int read() {
	int x = 0; bool f = true; char ch = getchar();
	for (; !isdigit(ch); ch = getchar()) if (ch == '-') f = false;
	for (; isdigit(ch); ch = getchar()) x = (x << 1) + (x << 3) + ch - '0';
	return f ? x : (~(x - 1));
}

const int N = (int)1e5 + 5;
int a[2][N], pre[N], n;

struct fenwick {
	int t[N << 2];
	#define lowbit(x) x & (-x)
	void clear() {
		std :: memset(t, 0, sizeof(t));
	}
	void add(int x, int v) {
		if (x <= 0) return ;
		for (; x < (N << 2) - 5; x += lowbit(x))
			t[x] += v;
		return ;
	}
	int query(int x) {
		int ans = 0;
		for (; x; x -= lowbit(x))
			ans += t[x];
		return ans;
	}
	int qry(int x) {
		return query((N << 2) - 1) - query(x);
	}
} t[2][2][2];

int solve() {
	for (int r : {0, 1}) for (int k : {0, 1}) for (int tp : {0, 1}) t[r][k][tp].clear();
	n = read();
	for (int i : {0, 1}) for (int j = 0; j < n; ++j)
		a[i][j] = read();

	int ans = 0;
	for (int i = 1; i < n; ++i) {
		int t1 = (i - 1) << 1, t2 = t1 + 1;
		pre[i] = pre[i - 1];
		if (i & 1) pre[i] += std :: max(0LL, a[0][i - 1] - t1) + 
		std :: max(0LL, a[1][i - 1] - t2);
		else pre[i] += std :: max(0LL, a[0][i - 1] - t2) + 
		std :: max(0LL, a[1][i - 1] - t1);
	}

	for (int i = n - 1; ~i; --i) {
		for (int r : {0, 1})
			for (int k : {0, 1}) {
				int v = a[r][i] + (k ? i : -i);
				// std :: cout << i << ' ' <<  r << ' ' << k << ' ' << v << std :: endl;
				t[r][k][0].add(v, 1);
				t[r][k][1].add(v, v);
			}
		int r = (i & 1);
		int cr = t[r][0][1].qry(i) - i * t[r][0][0].qry(i);
		int cnr = t[r ^ 1][1][1].qry(i + 2 * n - 1) - (i + 2 * n - 1) * t[r ^ 1][1][0].qry(i + 2 * n - 1);
		// std :: cout << i << ' ' << pre[i] << ' ' << cr << ' ' << cnr << std :: endl;
		ans = std :: max(ans, pre[i] + cr + cnr);
	}
	return ans;
}

signed main() {
	int T = read();
	while (T--) printf("%lld\n", solve());
	return 0;
}

D

给定一张 \(n \times m\) 的 01 矩阵,对每个坐标 \((x, y)\),求有多少个以 \((x, y)\) 为左上角的,横纵比为 \(p : q\) 的长方形,使长方形的边界上全为 \(0\)

  • \(n, m \le 3000\)
  • \(p, q \le 5\)\(p \perp q\)

写的是一个 \(\Theta\left(\dfrac{n^3}w\right)\) 的做法(认为 \(n\)\(m\) 同阶).枚举 \(k\),累加所有横为 \(pk\),纵为 \(qk\) 的矩形.

\(rb_i\) 是长度为 \(m\) 的一个 bitset,\(rb_{i, j}\) 代表第 \(i\) 行第 \(j\) 列的元素,是否向右有长度超过 \(pk\) 的连续段.同理,设置 \(n\) 个长度为 \(m\) 的 bitset \(cb_i\)\(cb_{i, j}\) 代表第 \(i\) 行第 \(j\) 列是否有向下超过 \(qk\) 的连续段.

枚举第 \(i\) 行,则

std :: bitset <N> res = rb[i] & rb[i + k * p] & cb[i] & (cb[i] >> (k * q));

这里的 res 便直接算出了第 \(i\) 行的每个元素是否有 \(pk \times qk\) 的合法矩形.让这个 res 累加到答案矩阵的第 \(i\) 行即可.

注意我们不能手动枚举 res 的每一位,否则复杂度退化到 \(\Theta(n^3)\).正确的做法是将 ans 矩阵的每一行拆成 \(\Theta(\log n)\) 个 bitset,第 \(j\) 个 bitset 维护答案矩阵第 \(i\) 行的二进制第 \(j\) 位.然后直接对 bitset 上考虑进位做加法.具体可以看代码.

#include <bits/stdc++.h>
int read() {
	int x = 0; bool f = true; char ch = getchar();
	for (; !isdigit(ch); ch = getchar()) if (ch == '-') f = false;
	for (; isdigit(ch); ch = getchar()) x = (x << 1) + (x << 3) + ch - '0';
	return f ? x : (~(x - 1));
}

const int N = 3010;
int a[N][N], rnt[N][N], dnt[N][N];
std :: bitset <N> rb[N], cb[N];
std :: bitset <N> ans[N][15];
typedef std :: pair <int, int> pii;
std :: vector <pii> rs[N], cs[N];

signed main() {
	int n = read(), m = read(), p = read(), q = read();
	std :: swap(p, q);
	for (int i = 1; i <= n; ++i)
		for (int j = 1; j <= m; ++j)
			a[i][j] = read();
	for (int i = n; i; --i)
		for (int j = m; j; --j) {
			rnt[i][j] = a[i][j] ? 0 : rnt[i][j + 1] + 1;
			dnt[i][j] = a[i][j] ? 0 : dnt[i + 1][j] + 1;
			rs[rnt[i][j]].emplace_back(i, j);
			cs[dnt[i][j]].emplace_back(i, j);
		}
	
	int stk = std :: min((n - 1) / p, (m - 1) / q);
	for (int k = stk; k; --k) {
		for (int i = (k == stk ? n : (k + 1) * p); i >= k * p + 1; --i)
			for (pii p : cs[i])
				cb[p.first].set(p.second);
		for (int i = (k == stk ? m : (k + 1) * q); i >= k * q + 1; --i)
			for (pii p : rs[i])
				rb[p.first].set(p.second);
		for (int i = 1; i <= n - k * p; ++i) {
			// std :: cout << i << ' ' << i + k * p << std :: endl;
			std :: bitset <N> res = rb[i] & rb[i + k * p] & cb[i] & (cb[i] >> (k * q));
			if (res.none()) continue;
			for (int j = 0; j < 12; ++j) {
				std :: bitset <N> nxt = ans[i][j] & res;
				ans[i][j] ^= res;
				std :: swap(nxt, res);
				if (res.none()) break;
			}
		}
	}

	for (int i = 1; i <= n; ++i, puts(""))
		for (int j = 1; j <= m; ++j) {
			int v = 0;
			for (int k = 0; k < 12; ++k)
				if (ans[i][k].test(j))
					v |= (1 << k);
			printf("%d ", v);
		}
	return 0;
}

G

对于集合 \(S\),定义 \(\bigoplus(S)\)\(S\) 内元素的异或和,\(f(S)\)\(\max_{S' \subseteq S} \bigoplus(S')\).给定长度为 \(n\) 的序列 \(a\),求

\[\sum_{1 \le l \le r \le n} f(\{a_l, a_{l + 1}, \ldots, a_r\}) \]

  • \(n \le 10^5\)
  • \(0 \le a_i < 2^{30}\)

这个题很明显强于线性基,所以考虑线性基.对每个 \(r\) 计算贡献.令 \(k = \log a\)

线性基有一个性质是,一个集合 \(S\) 一直往里边加数 \(v \in [0, 2^k)\),则 \(S\) 的线性基最多变化 \(k\) 次,即总共最多出现 \(k + 1\) 个不同的线性基.这是因为线性基改变的唯一情况是升维,从 \(0\) 维到 \(k\) 维最多升 \(k\) 次.线性基最多只有 \(k + 1\) 个,因此 \(f(S)\) 的取值也最多只有 \(k + 1\) 种.

\(f(\{a_l, a_{l + 1}, \ldots, a_r\})\) 取值只有 \(k + 1\) 种.如果我们可以做到快速统计这 \(k + 1\) 种取值的分界线,即线性基维度变化的分界线,则要计算的只是 \(\Theta(nk)\) 量级个项的和,完全可以接受.

这个问题可以用前缀线性基解决.前缀线性基是在贪心构造线性基的基础上,加一步操作:记录基向量的对应来源的原序列下标,贪心换更右侧的.代码:

int b[B], bid[B];

void insert(int x, int v) {
	for (int i = 29; i >= 0; i--) if ((v >> i) & 1) {
		if (!b[i]) {
			b[i] = v;
			bid[i] = x;
			return ;
		} else if (bid[i] < x) {
			std :: swap(v, b[i]);
			std :: swap(x, bid[i]);
		}
		v ^= b[i];
	}
}

前缀线性基的板子题是 CF1100F Ivans and Burgers,可以参考.

故只需要扫到前缀 \(r\) 时,当前基对应的原序列下标就是这 \(k\) 个分界点.直接统计即可.复杂度 \(\Theta(nk^2)\)

#include <bits/stdc++.h>
#define int long long
const int B = 35;
int read() {
	int x = 0; bool f = true; char ch = getchar();
	for (; !isdigit(ch); ch = getchar()) if (ch == '-') f = false;
	for (; isdigit(ch); ch = getchar()) x = (x << 1) + (x << 3) + ch - '0';
	return f ? x : (~(x - 1));
}

int b[B], bid[B];

void insert(int x, int v) {
	for (int i = 29; i >= 0; i--) if ((v >> i) & 1) {
		if (!b[i]) {
			b[i] = v;
			bid[i] = x;
			return ;
		} else if (bid[i] < x) {
			std :: swap(v, b[i]);
			std :: swap(x, bid[i]);
		}
		v ^= b[i];
	}
}

const int N = (int)1e5 + 5;
int a[N];

signed main() {
	int n = read();
	for (int i = 1; i <= n; ++i) a[i] = read();
	int ans = 0;
	for (int r = 1; r <= n; ++r) {
		insert(r, a[r]);
		std :: vector <int> idxs;
		for (int i = 0; i < B; ++i) if (b[i])
			idxs.push_back(bid[i]);
		std :: sort(idxs.begin(), idxs.end(), std :: greater <int> ());
		for (int i = 0; i < (int)idxs.size(); ++i) {
			int con = idxs[i] - (i == (int)idxs.size() - 1 ? 0 : idxs[i + 1]), v = 0;
			for (int j = 29; ~j; --j)
				if (b[j] && bid[j] >= idxs[i])
					if ((v ^ b[j]) > v)
						v ^= b[j];
			ans += v * con;
		}
	}

	printf("%lld\n", ans);
	return 0;
}

P

给定一个 \(n\) 个点,\(m\) 条边的有向图,抽象为水管网络.边权 \(w_i\) 代表水流经这条管道的时间.有一些节点拥有若干前置节点,只有若干前置节点有水流入时,这个节点才会开放(即这个节点入边管道中的水才可流入这个节点).问从点 \(1\) 倒入无限水后,经过多长时间点 \(n\) 才会有水.

  • \(n \le 3 \times 10^3\)
  • \(m \le 7 \times 10^4\)
  • \(1 \le w \le 10^8\)

前置依赖关系很明显构成了拓扑结构,并且这个拓扑的贡献是这样的:对于依赖关系 \(u\) 依赖 \(v\),连边 \(v \to u\),则最短路 \(\mathrm{dis}(u)\) 会跟所有 \(u\) 的入点 \(v\)\(\mathrm{dis}(v)\) 取一个 max.用拓扑的思想维护这个过程即可.

注意前置依赖关系不保证无环(题目似乎只保证了无自环)?但是无所谓,在有环的有向图上做拓扑排序,环上的点永远不会被释放入队,这正好是期望的效果,因为依赖关系成环的点永远不可打开.

#include <bits/stdc++.h>
#define int long long
int read() {
  int x = 0; bool f = true; char ch = getchar();
  for (; !isdigit(ch); ch = getchar()) if (ch == '-') f = false;
  for (; isdigit(ch); ch = getchar()) x = (x << 1) + (x << 3) + ch - '0';
  return f ? x : (~(x - 1));
}

const int N = (int)3e3 + 5;
typedef std :: pair <int, int> pii;
std :: vector <pii> G[N];
std :: vector <int> D[N];
int n, m;
int dis[N], ind[N];

void dijkstra() {
  std :: memset(dis, 0x3f, sizeof(dis));
  dis[1] = 0;
  std :: priority_queue <pii> q;
  q.emplace(0, 1);

  auto update = [&](int u) {
    if (ind[u]) return ;
    q.emplace(-dis[u], u);
  };

  while (!q.empty()) {
    int d = q.top().first, u = q.top().second;
    q.pop();
    if (d + dis[u]) continue;
    for (pii e : G[u]) {
      int v = e.first, w = e.second;
      if (dis[v] > dis[u] + w) {
        dis[v] = dis[u] + w;
        update(v);
      }
    }
    for (int v : D[u]) {
      dis[v] = std :: max(dis[v], dis[u]);
      --ind[v];
      update(v);
    }
  }
}

signed main() {
  n = read(); m = read();
  for (int i = 1; i <= m; ++i) {
    int u = read(), v = read(), w = read();
    G[u].emplace_back(v, w);
  }

  for (int u = 1; u <= n; ++u) {
    ind[u] = read();
    for (int i = 1; i <= ind[u]; ++i) {
      int x = read();
      D[x].push_back(u);
    }
  }

  dijkstra();
  printf("%lld\n", dis[n]);
  return 0;
}

K

\(m\) 堆石子,每一堆石子的数量分别为 \(b_1, b_2, \ldots, b_m\).两个人轮流做以下博弈:

  • 选择一堆石子,扔掉正整数个石子.
  • 随后,可以选择将这堆石子丢弃,也可以选择将这堆石子合并到任一堆.

将最后一个石子取走的人胜利.

现有 \(n\) 堆石子,数量分别为 \(a_1, a_2, \ldots, a_n\)\(q\) 次询问给定 \([l, r]\),问 \([l, r]\) 内有多少个子段 \([l', r']\) 满足在段 \([l', r']\) 上做上面的博弈,先手必胜

  • \(n, q \le 2 \times 10^5\)
  • \(1 \le a_i \le 10^6\)

其实经验是看到这种博弈论和数据结构套皮的题不用害怕,看上去不可做,但数据结构侧往往真的只是一个套皮…其实数据结构侧的操作越复杂,说明博弈论的结论反而可能越简单,所以要相信自己.

结论是:如果石子堆数为奇数,先手必胜;如果石子堆数为偶数,等价于对每个石子堆石子数 \(-1\) 后做 nim 游戏,即序列平移 \(-1\) 后异或和不为 \(0\) 则先手必胜.

先说怎么统计.很明显这个从反面计数更方便,即算区间内有多少个偶数长度的,\(a_i - 1\) 异或和为 \(0\) 的子区间.对 \(a_i - 1\) 做前缀异或和 \(s_i\),问题转化为问 \([l - 1, r]\) 上有多少个 \(s\) 相等点对,并且 \(s\) 的下标同奇偶.奇偶分开做小 Z 的袜子即可,复杂度 \(\Theta(q \sqrt n)\)

现在最大的问题是这个结论为什么是对的,以及怎么想到的.其实后者比前者简单.

先说怎么想到的.首先堆数为 \(1\) 必胜.所以堆数为 \(2\) 时,谁让堆数为 \(1\) 就必败了.所以一定不会进行合并操作,那么这个问题和 nim 游戏就是等价的,只是终败态是 1 1 而不是 0 0(因为 1 1 时只能遗憾让堆数为 \(1\),其他状态都能苟活一下),所以是 \((a_i - 1)\) 的 nim,结论是异或和为 \(0\) 必败,否则必胜.

然后就是 \(n = 3\),根据 \(n = 2\) 的结论,我们会尽量让石子变成两堆 \(-1\) 后异或和为 \(0\) 的东西.粗糙手玩一下能发现这个一定能做到,找不到什么反例.可以直接猜 \(n = 3\) 必胜,则 \(n = 4\) 又变成了让堆数 \(-1\) 的人必败,故还是 \(-1\) 后的 nim…到这里可以直接大胆猜结论了.

严格证明的话是考虑为何奇数堆总能通过丢弃一些再合并的方式,让剩下的偶数堆 \(-1\) 后异或和为 \(0\)

首先不要考虑这个 \(-1\) 了,直接让所有石子数平移 \(-1\) 上考虑,即奇数长度的 \(a\) 可以通过删掉一个元素 \(x\),然后将 \(\le x\) 的值加给 \(a\) 的其它任一项,使得 \(a\) 的异或和为 \(0\)

这里有一个细节,为什么是 \(\le x\),这个等号为何可以取到?是因为我们对所有石子数平移 \(-1\) 了,故虽然原先必须对石子扔掉正整数个再合并,但这里的 \(-1\) 弥补了这一部分.

\(T\) 为初始时 \(a\) 的异或和.先假设 \(T \ne 0\),设 \(T\) 的二进制最高位 \(1\) 是第 \(d\) 位,并在 \(a\) 中找到一个第 \(d\) 位也为 \(1\) 的数 \(x\)(一定可以找到,否则异或不出 \(1\)).下面证明一定可以找到另一个数 \(y\),满足我们可以将 \(y \to y \oplus S\),其中 \(S = T \oplus x\) 表示除了 \(x\) 剩余项的异或和.这要求:

\[y \le y \oplus S \le y + x \]

\(y\) 不能下降,也不能增加 \(> x\) 的量.

因为 \(T\)\(x\) 的第 \(d\) 位为 \(1\),故 \(S\) 的第 \(d\) 位为 \(0\).对 \(S\)\(x\) 从高位到低位对比:

  • 对于高于 \(d\) 的二进制位,\(T\)\(0\),故 \(S = T \oplus x\) 指出 \(S\)\(x\) 在这些位完全相同.
  • 而在位 \(d\) 上,\(S\)\(0\)\(x\)\(1\)

\(S < x\).因此 \(y \oplus S \le y + S < y + x\) 自动成立,只需令 \(y \oplus S \ge y\)

\(S\) 的最高 \(1\) 位为第 \(p\) 位.由于 \(S\) 是除 \(x\) 外异或和,一共有偶数项,故除 \(x\) 外一定有一个数 \(y\) 在第 \(p\) 位为 \(0\)(否则如果纯 1,偶数想会导致这一位异或和为 \(0\)).只需选择这个 \(y\),则 \(y \oplus S\) 在高于 \(p\) 位与 \(y\) 相同,在第 \(p\) 位为 \(1\),高于 \(y\) 在第 \(p\) 位的 \(0\),有 \(y \oplus S > y\).我们就完成了 \(T \ne 0\) 的构造.

对于 \(T = 0\),移除 \(x\) 后异或和变为 \(x\),要令异或和重新为 \(0\),需令 \(y \gets x \oplus y\).要求

\[y \le y \oplus x \le y + x \]

右侧自动成立,所以我们只需找到一个点对 \((x, y)\) 满足 \(y \le y \oplus x\).取 \(x = \max a\),令 \(x\) 二进制最高位为 \(d\),则 \(a\) 不可能第 \(d\) 位全为 \(1\),否则奇数个 \(1\) 异或出 \(1\),不可能有 \(T = 0\).故取第 \(d\) 位为 \(0\) 的元素作为 \(y\),由于 \(x\)\(y\) 在高于第 \(d\) 位均为 \(0\),而在第 \(d\)\(y = 0\)\(x = 1\),有 \(y < y \oplus x\)

综上,我们完成了证明.但是好麻烦,所以赛场上还是猜结论性价比最高.

#include <bits/stdc++.h>
#define int long long
// 1 -> win
// 2 -> 一定不合并,nim game
// 3 -> 合并为 2, nim game
// 1 1 1 1 lose
// 1 1 1 2 win -> 1 1 1 1
// 2 2 2 2 lose -> 1 2 2 2 / 2 2 1 2 / 2 2 2 1
// odd -> win, even -> 1 1 1 1 lose, (ai - 1) nim
int read() {
	int x = 0; bool f = true; char ch = getchar();
	for (; !isdigit(ch); ch = getchar()) if (ch == '-') f = false;
	for (; isdigit(ch); ch = getchar()) x = (x << 1) + (x << 3) + ch - '0';
	return f ? x : (~(x - 1));
}

const int N = (int)2e5 + 5, B = 450;
const int A = 1 << 21;
struct node {
	int l, r, id, ans;
} qrys[N];
int n, m, k, ans;
int a[N], cnt[A][2];

void del(int x) {
	int v = a[x];
	ans -= cnt[v][x & 1] - 1;
	--cnt[v][x & 1];
}

void add(int x) {
	int v = a[x];
	ans += cnt[v][x & 1];
	++cnt[v][x & 1];
}

signed main() {
	n = read(); m = read();
	for (int i = 1; i <= n; ++i) a[i] = (read() - 1) ^ a[i - 1];
	for (int i = 1; i <= m; ++i) {
		int l = read() - 1, r = read();
		qrys[i] = (node){l, r, i};
	}
	std :: sort(qrys + 1, qrys + 1 + m, [](node p, node q) {
		if (p.l / B == q.l / B) return p.r < q.r;
		return p.l < q.l;
	});

	int l = 1, r = 0;
	for (int i = 1; i <= m; ++i) {
		int nxl = qrys[i].l, nxr = qrys[i].r;
		while (l > nxl) add(--l);
		while (r < nxr) add(++r);
		while (l < nxl) del(l++);
		while (r > nxr) del(r--);
		qrys[i].ans = (r - l + 1) * (r - l) / 2 - ans;
	}

	std :: sort(qrys + 1, qrys + 1 + m, [](node p, node q) {
		return p.id < q.id;
	});

	for (int i = 1; i <= m; ++i) printf("%lld\n", qrys[i].ans);
	return 0;
}
posted @ 2025-12-27 22:32  dbxxx  阅读(249)  评论(0)    收藏  举报