IOI 2021 集训队作业胡扯「1, 50」

粉兔 AKIOI (详细揭秘)

粉兔做不完作业是怎么回事呢,小编也很好奇,但事实就是这样

粉兔 AKIOI 是假的

集训队作业官方网站:ioihw20.duck-ac.cn

题目列表

A (WF         2014) : A B C E F G H I J K L
B (WF         2015) : B E G H J K L M
C (WF         2016) : A B D F H I J K M
D (WF         2017) : A B D G H J K L
E (WF         2018) : C D E G H I J
F (WF         2019) : B C F G I J K
G (NEERC      2017) : F G H I J K L
H (NEERC      2016) : B C D G I K L M
I (NEERC      2015) : B C D H I J K L
J (NEERC      2014) : C D E G H I
K (NEERC      2013) : A C D E G H I K
L (CERC       2017) : B C D E I K L
M (CERC       2016) : B D E G I J L
N (CERC       2015) : C E F G I J L
O (CERC       2014) : A B E G J K L
P (CERC       2013) : A D E G H J
Q (NEERC,NSub 2017) : C D E F G H J
R (NEERC,NSub 2016) : B D E G H I J
S (NEERC,NSub 2015) : D F G I K
T (NEERC,NSub 2014) : C E F H K
U (NEERC,NSub 2013) : C H I J L

表格

试题一 完成情况 试题二 完成情况 试题三 完成情况
UC DG RB
HC IB FJ
GK UH QH
EE DJ IH
HL MI EJ
MB UI ED
AA BG ML
CF TE QG
PH UJ BB
CI SD NG
DK LD IJ
NC BJ FK
CH NJ RH
QF BE KI
AC PG HM
PJ ID EG
SI KC GI
OG CK DB
QE GH NI
OL KA MG
SG NL KH
QJ KG AB
KD IL NF
CM NE HD
DH EC BM
LC CD JI
DL ME PE 暂无题解
LI AI RJ
SF II HG
RE LL OA
FB QD DA
TC AE CB
GF AG JC
PA TH EH
AJ TK EI
UL AF SK
BH RD OK 暂无题解
FC JD OE
TF FF DD
CJ HK MJ
FG GL AL
FI IC QC
MD OJ HI
KK JH OB
BK GJ KE
CA IK LE
RI LK PD
JE LB HB
BL RG AH
AK GG JG

绿的表示主要没看题解,的表示主要看了题解。


FB. Beautiful Bridges

本题是在获得集训队训练资料前就已经做过的,故写在开头。

详细题解请见:ICPC World Finals 2019 题解评测链接

FG. First of Her Name

本题是在获得集训队训练资料前就已经做过的,故写在开头。

详细题解请见:ICPC World Finals 2019 题解评测链接

2020-10-13

AK. Surveillance

题意简述

有一个周长为 \(n\) 的圆,等距分割为 \(n\) 小段,按顺序编号为 \(1 \sim n\)

给定 \(k\) 个圆上的弧,每个覆盖一段连续的小段 \([a_i, b_i]\),问最少选取其中多少个弧可以覆盖整个圆,或报告不可能完成。

数据范围:\(3 \le n \le {10}^6\)\(1 \le k \le {10}^6\)

AC 代码
#include <cstdio>
#include <cstring>
#include <algorithm>

const int MN = 1000005, MB = 20;

int N, K, Lg[MN], g[MB + 1][MN];
inline void Cover(int l, int r, int v) {
	int j = Lg[r - l + 1];
	g[j][l] = std::max(g[j][l], v);
	r -= (1 << j) - 1, g[j][r] = std::max(g[j][r], v);
}

int main() {
	scanf("%d%d", &N, &K);
	memset(g, -1, sizeof g), Lg[0] = -1;
	for (int i = 1; i <= N; ++i) Lg[i] = Lg[i - 1] + !(i & (i - 1));
	for (int i = 1; i <= K; ++i) {
		int l, r;
		scanf("%d%d", &l, &r), --l, --r;
		if (l <= r) Cover(l, r, r);
		else Cover(0, r, r), Cover(l, N - 1, r + N);
	}
	for (int j = MB; j >= 1; --j)
		for (int i = 0; i <= N - (1 << j); ++i)
			g[j - 1][i] = std::max(g[j - 1][i], g[j][i]),
			g[j - 1][i + (1 << (j - 1))] = std::max(g[j - 1][i + (1 << (j - 1))], g[j][i]);
	for (int i = 0; i < N; ++i) {
		if (g[0][i] == -1) return puts("impossible"), 0;
		g[0][i] -= i - 1;
	}
	for (int j = 0; j < MB; ++j) {
		for (int i = 0; i < N; ++i)
			g[j + 1][i] = g[j][i] + g[j][(i + g[j][i]) % N];
		if (*std::max_element(g[j + 1], g[j + 1] + N) >= N) {
			int Ans = K;
			for (int i = 0; i < N; ++i) {
				int d = 0, c = 0;
				for (int k = j; k >= 0; --k)
					if (d + g[k][(i + d) % N] < N)
						d += g[k][(i + d) % N], c |= 1 << k;
				Ans = std::min(Ans, c);
			}
			printf("%d\n", Ans + 1);
			return 0;
		}
	}
	return 0;
}

〔「SCOI2015」国旗计划〕弱化版。

判断可行性很简单。

对于求出最小选取的弧的数量,考虑倍增,对于每个小段,求从此处出发使用 \(2^j\) 个区间最多能向前遍历多长的距离。

时间复杂度为 \(\mathcal O (k + n \log n)\)评测链接

2020-10-16

OL. Outer space invaders

题意简述

\(n\) 个物体,第 \(i\) 个物体在时刻 \(a_i\) 出现,在时刻 \(b_i\) 消失,权值为 \(d_i\)

在某个时刻,你可以用 \(R\) 的代价消灭所有权值 \(d_i \le R\) 的,且此时已经出现但还未消失的物体。

问消灭所有物体的最小代价之和,物体消失了不算被消灭。

数据范围:\(1 \le n \le 300\)\(1 \le a_i < b_i \le {10}^4\)\(1 \le d_i \le {10}^4\)

AC 代码
#include <cstdio>
#include <algorithm>

const int MN = 305;
const int Inf = 0x3f3f3f3f;

int N, a[MN], b[MN], d[MN];
int seq[MN * 2], cnt;
int f[MN * 2][MN * 2];

int main() {
	int Tests;
	scanf("%d", &Tests);
	while (Tests--) {
		scanf("%d", &N), cnt = 0;
		for (int i = 1; i <= N; ++i)
			scanf("%d%d%d", &a[i], &b[i], &d[i]),
			seq[++cnt] = a[i],
			seq[++cnt] = b[i] + 1;
		std::sort(seq + 1, seq + cnt + 1);
		cnt = std::unique(seq + 1, seq + cnt + 1) - seq - 1;
		for (int i = 1; i <= N; ++i)
			a[i] = std::lower_bound(seq + 1, seq + cnt + 1, a[i]) - seq,
			b[i] = std::lower_bound(seq + 1, seq + cnt + 1, b[i] + 1) - seq - 1;
		--cnt;
		for (int q = 1; q <= cnt; ++q) {
			for (int j = q; j <= cnt; ++j) {
				int i = j - q + 1, x = 0;
				for (int k = 1; k <= N; ++k)
					if (i <= a[k] && b[k] <= j)
						x = std::max(x, d[k]);
				f[i][j] = Inf;
				for (int k = i; k <= j; ++k)
					f[i][j] = std::min(f[i][j], f[i][k - 1] + x + f[k + 1][j]);
			}
		}
		printf("%d\n", f[1][cnt]);
	}
	return 0;
}

一开始容易想出一个贪心的做法,看到 \(n\) 这么小有点怀疑,果然贪心错了。

我们先离散化时间,把 \(a_i, b_i\) 的值域降至 \(\mathcal O (n)\)

我们考虑 \(R\) 最大的那次操作,一定是把 \(d_i\) 最大的物体消灭了,所以也肯定同时消灭了同时存在的其他物体。

所以我们在 \(d_i\) 最大的物体的 \([a_i, b_i]\) 中枚举一个执行这次消灭的位置,然后分成左右两个子问题。

子问题中仅包含出现时间段完全落在子问题区间中的物体,因为其他都肯定被消灭了。

所以变成了个区间 DP。

时间复杂度为 \(\mathcal O (n^3)\)评测链接

P.S. 有点像〔USACO 2019.12 Platinum T1. Greedy Pie Eaters〕,LOJ #3226,看起来不是个区间 DP 但是确实是?

GG. The Great Wall

题意简述

给定长度为 \(n\) 的三个数列 \(a, b, c\)(下标范围为 \(1 \sim n\)),一个整数 \(r\)(满足 \(1 \le r < n\)),和一个正整数 \(k\)

可以选取两个下标 \(x, y\) 满足 \(r \le x < y \le n\)

考虑两个长度为 \(r\) 的下标区间 \([x - r + 1, x]\)\([y - r + 1, y]\),并据此定义数列 \(h\)

  • 如果下标 \(i\) 不属于这两个区间,则 \(h_i = a_i\)
  • 如果下标 \(i\) 恰好属于一个区间,则 \(h_i = b_i\)
  • 如果下标 \(i\) 属于全部两个区间,则 \(h_i = c_i\)

最后,定义价值为 \(\displaystyle \sum_{i = 1}^{n} h_i\)

所有的 \(x, y\) 的选取方案有 \(\frac{(n - r) (n - r + 1)}{2}\) 种,求其中价值第 \(k\) 小的方案的价值。

数据范围:\(2 \le n \le 3 \times {10}^4\)\(1 \le k \le \frac{(n - r) (n - r + 1)}{2}\)\(1 \le a_i < b_i < c_i \le {10}^6\)

AC 代码
#include <cstdio>

typedef long long LL;
const int MN = 30005, MS = 3300005;

int N, R; LL K;
LL A[MN], B[MN], C[MN], Sum;

#define mid (((l + r) - ((l + r) & 1)) / 2)
#define ls lc[i], l, mid
#define rs rc[i], mid + 1, r
int lc[MS], rc[MS], num[MS], cnt;
int rt1[MN], rt2[MN];
LL Lim;
void Mdf(int &i, LL l, LL r, LL p, int x) {
	++cnt, lc[cnt] = lc[i], rc[cnt] = rc[i], num[cnt] = num[i] + x, i = cnt;
	if (l == r) return ;
	p <= mid ? Mdf(ls, p, x) : Mdf(rs, p, x);
}
int Qur(int i, LL l, LL r, LL p) {
	if (!i) return 0;
	if (r <= p) return num[i];
	return p <= mid ? Qur(ls, p) : num[lc[i]] + Qur(rs, p);
}
#undef mid
#undef ls
#undef rs

void Init() {
	Lim = B[N] + C[N];
	for (int y = N; y > R; --y) {
		Mdf(rt1[y - 1] = rt1[y], -Lim, Lim, B[y] - C[y - R], 1);
		if (y + R <= N) Mdf(rt1[y - 1], -Lim, Lim, B[y + R] - C[y], -1);
	}
	for (int z = N - R; z > R; --z)
		Mdf(rt2[z - 1] = rt2[z], -Lim, Lim, B[z + R] - B[z], 1);
}

inline LL calc(LL mid) {
	LL sum = 0;
	for (int x = R; x < N; ++x)
		sum += Qur(rt1[x], -Lim, Lim, mid - C[x] + B[x - R]);
	for (int x = R; x < N - R; ++x)
		sum += Qur(rt2[x], -Lim, Lim, mid - B[x] + B[x - R]);
	return sum;
}

int main() {
	scanf("%d%d%lld", &N, &R, &K);
	for (int i = 1; i <= N; ++i) scanf("%lld", &A[i]), Sum += A[i];
	for (int i = 1; i <= N; ++i) scanf("%lld", &B[i]), B[i] -= A[i];
	for (int i = 1; i <= N; ++i) scanf("%lld", &C[i]), C[i] -= A[i] + B[i];
	for (int i = 1; i <= N; ++i) B[i] += B[i - 1], C[i] += C[i - 1];
	Init();
	LL lb = 0, rb = B[N] + C[N], mid, ans = -1;
	while (lb <= rb) {
		mid = (lb + rb) / 2;
		if (calc(mid) >= K) ans = mid, rb = mid - 1;
		else lb = mid + 1;
	}
	printf("%lld\n", Sum + ans);
	return 0;
}

类似于〔「NOI2010」超级钢琴〕。

\(c_i\) 减去 \(a_i + b_i\),再令 \(b_i\) 减去 \(a_i\),令 \(\displaystyle \mathrm{Sum} = \sum_{i = 1}^{n} a_i\)

再令 \(\displaystyle {\mathrm{S}b}_k = \sum_{i = 1}^{k} b_i\)\(\displaystyle {\mathrm{S}c}_k = \sum_{i = 1}^{k} c_i\),即 \(b, c\) 的前缀和数列。

考虑如果 \([x - r + 1, x]\)\([y - r + 1, y]\) 有交或刚好有触碰,即 \(x < y \le x + r\)

  • 则价值为 \(\mathrm{Sum} + {\mathrm{S}b}_y - {\mathrm{S}b}_{x - r} + {\mathrm{S}c}_x - {\mathrm{S}c}_{y - r}\)
    变换一下变成 \(\mathrm{Sum} + ({\mathrm{S}c}_x - {\mathrm{S}b}_{x - r}) + ({\mathrm{S}b}_y - {\mathrm{S}c}_{y - r})\)

考虑如果 \([x - r + 1, x]\)\([y - r + 1, y]\) 无交且不刚好触碰,即 \(x + r < y\)

  • 则价值为 \(\mathrm{Sum} + ({\mathrm{S}b}_x - {\mathrm{S}b}_{x - r}) + ({\mathrm{S}b}_y - {\mathrm{S}b}_{y - r})\)

我们考虑二分答案 \(\mathrm{Sum} + \mathrm{mid}\),统计价值小于等于 \(\mathrm{Sum} + \mathrm{mid}\)\((x, y)\) 数对的数量。

对上述两种情况分别统计,我们固定 \(x\),统计符合条件的 \(y\) 的数量。

对于每个 \(x\),合法的 \(y\) 是一个区间,且对于 \(x\) 的连续变化,区间的变化量是 \(\mathcal O (1)\)

而且我们是需要在值域上求一维偏序,所以考虑以值域维度建数据结构。

可以发现主席树符合我们的要求,对两种情况分别建立对应的主席树,然后对于固定的 \(x\) 查询值域前缀上的 \(y\) 数量即可。

时间复杂度为 \(\mathcal O (n \log^2 (n v))\),其中 \(v\) 为值域,评测链接

P.S. 可以发现,此处没有必要使用主席树的,每次对二分的答案进行计算的时候扫描线即可,但是如果不做值域离散化的话,这种方法在时间和空间复杂度或常数上均没有显著改进,所以此处使用了主席树。

QE. Equal Numbers

题意简述

\(n\) 个数 \(a_1, a_2, \ldots , a_n\)。你可以对它们执行操作:选取任意一个数 \(a_i\),把它乘以任意正整数 \(x\),即 \(a_i \gets a_i \cdot x\)

请你对所有 \(k\)(满足 \(0 \le k \le n\))求出:执行 \(k\) 次操作后,这些数中不同的数值个数的最小值。

数据范围:\(1 \le n \le 3 \times {10}^5\)\(1 \le a_i \le {10}^6\)

AC 代码
#include <cstdio>
#include <algorithm>

const int MS = 1000005, MN = 300005;

int N, Lim, C, b[MS], d[MS];
int s1[MN], t1;
int s2[MN], t2;
int ans1[MN], ans2[MN];

int main() {
	scanf("%d", &N), Lim = 1000000;
	for (int i = 1, x; i <= N; ++i)
		scanf("%d", &x), ++b[x];
	for (int i = 1; i <= Lim; ++i)
		if (b[i]) ++C, s1[++t1] = b[i];
	for (int i = 1; i <= Lim; ++i)
		for (int j = i; j <= Lim; j += i)
			d[i] += b[j];
	for (int i = 1; i <= Lim; ++i)
		if (b[i] && d[i] > b[i]) s2[++t2] = b[i];
	if (C == 1) {
		for (int i = 0; i <= N; ++i) printf("1%c", " \n"[i == N]);
		return 0;
	}
	std::sort(s1 + 1, s1 + t1 + 1);
	std::sort(s2 + 1, s2 + t2 + 1);
	s1[1] += s1[2], --t1;
	for (int i = 2; i <= t1; ++i) s1[i] = s1[i + 1];
	for (int i = 1, p = 0; i <= t1; ++i) ++ans1[p += s1[i]];
	for (int i = 1, p = 0; i <= t2; ++i) ++ans2[p += s2[i]];
	for (int i = 1; i <= N; ++i) ans1[i] += ans1[i - 1], ans2[i] += ans2[i - 1];
	for (int i = 0; i <= N; ++i) printf("%d%c", C - std::max(ans1[i], ans2[i]), " \n"[i == N]);
	return 0;
}

在所有质因数维度分解后,这实际上是一个无限维偏序空间,可以把一个点移到大于它的位置上。

在这个空间中,初始有一些位置本来就有点了,还有一些位置初始时是空的。

\(k\) 一定时,显然不会对一个点操作多次(除非取 \(x = 1\) 进行操作)。

所以我们假定一个点一定只会操作最多一次,不允许 \(x = 1\) 的操作,把条件改成进行 \(\le k\) 次操作即可。

也就是有一个大小不超过 \(k\) 的点集会进行操作,其他的点都在原地不动,我们可以给出几个基本结论:

  • 初始时位置相同的点,如果其中一个被操作了,它们全体一定会一起被操作,且操作的 \(x\) 均相同。这是因为如果其中有不被操作的点,则全体都不被操作不会使答案更劣,如果全体都被操作了,令它们的操作的 \(x\) 相同,也不会使答案更劣。
  • 位置相同的一群点,被操作后有可能落到了初始时本来就有点的位置上,也可能落到初始时是空的位置上。
    • 如果有一群这样的点落到了初始时是空的位置上,则令它落到的位置变为一个无穷远点(各维度坐标足够大以保证在题目范围内的所有数值都能转移到它,比如 \(\operatorname{lcm}(1, 2, \ldots , {10}^6)\),是所有范围内的数的倍数),而且令所有被操作的点都落到同样的这个无穷远点上,不会使答案更劣。
    • 否则,所有被操作的点都落到了初始时就有点的位置上,而这个位置上原有的点一定不会被操作。这是因为如果它们被操作了,移动到其他点上去了,则之前的那个点也是可以移动到这个新位置上的,如果新位置上原有的点仍然被操作了,我们继续去找新的新位置,直至最终找到一个原有的点没有被操作的位置,把之前访问到的所有点集都移动到这个位置即可。上面的过程能够成立主要依靠:第一点是,转移关系是偏序,所以具有传递性;第二点是,点的数量是有限的,所以这个过程可以在有限步内结束。

也就是说,初始时 \(a_i\) 在数值上相同的一群 \(i\),它们的行动是统一的,而且之后只有两种情况:

  1. 所有被操作的点都跑到了同一个无穷远的位置上,且这个位置在初始时肯定是没有点的。假设有 \(c\) 个初始时数值不相同的点集被操作了,则此时答案就比 \(k = 0\)(即初始状态)时减少 \((c - 1)\),因为原来的 \(c\) 个位置上的点消失了,增加了一个无穷远位置上的点。
  2. 每个被操作的初始时数值不相同的点集,都移动到了一个初始时就有点的位置上,且这个位置上原有的点是不会被操作的。假设有 \(c\) 个初始时数值不相同的点集被操作了,则此时答案就比 \(k = 0\)(即初始状态)时减少 \(c\)。注意这个情况和第一种情况相比,有的初始点集是无法被操作的,也即那些无法转移到其他初始时就有点的位置上的点集,换句话说就是这个偏序关系上的「最大值」,在原问题中体现为数集 \(a\) 中没有它的倍数的那些数。

我们对这两种情况分别处理即可。可以开一个桶然后做一个倍数的统计,然后确定出能参与第二种情况的点集。之后再对两种情况中的可操作点集按照大小从小到大排序,分别维护两种情况的答案数组。最终输出时输出两个数组中对应位置的较小值即可。

时间复杂度为 \(\mathcal O (n \log n + v \log v)\),其中 \(v\) 是值域,评测链接

BJ. Tile Cutting

题意简述

如果有一个长为 \(x\) 宽为 \(y\) 的矩形,其中 \(x, y\) 是整数,你可以选择两个整数 \(a, b\) 满足 \(1 \le a < x\)\(1 \le b < y\)

然后在矩形长为 \(x\) 的边的左起 \(a\) 个单位长度,宽为 \(y\) 的边的左起 \(b\) 个单位长度处标记点,对四条边均是如此。

同时你需要保证这四个点是关于矩形的正中心,中心对称的。顺次连接四个点可以得到一个平行四边形。

记这个平行四边形的面积为 \(w\)。我们定义 \(f(w)\) 为能够得到面积为 \(x\) 的平行四边形的有序数对 \((x, y, a, b)\) 的数量。

\(Q\) 次询问,每次询问给出 \(l, r\),你需要求出 \(f(w)\)\(l \le w \le r\))的最大值,以及取到这个值的 \(w\) 的值,有多个 \(w\) 取最小的。

数据范围:\(1 \le Q \le 500\)\(1 \le l \le r \le 5 \times {10}^5\)

AC 代码
#include <cstdio>
#include <algorithm>

typedef long long LL;
const int Mod = 998244353;
const int G = 3, iG = 332748118;
const int MS = 1 << 20;

inline int qPow(int b, int e) {
	int a = 1;
	for (; e; e >>= 1, b = (LL)b * b % Mod)
		if (e & 1) a = (LL)a * b % Mod;
	return a;
}
inline int gInv(int b) { return qPow(b, Mod - 2); }

int Sz, InvSz, R[MS], ws[MS], MaxSz;
inline int getB(int N) { int Bt = 0; while (1 << Bt < N) ++Bt; return Bt; }
inline void InitFNTT(int N) {
	int Bt = getB(N);
	if (Sz == (1 << Bt)) return ;
	Sz = 1 << Bt, InvSz = Mod - (Mod - 1) / Sz;
	for (int i = 1; i < Sz; ++i) R[i] = R[i >> 1] >> 1 | (i & 1) << (Bt - 1);
	if (!MaxSz) MaxSz = 2, ws[1] = 1;
	while (MaxSz < Sz) {
		int w = qPow(G, (Mod - 1) / (2 * MaxSz));
		for (int i = MaxSz / 2; i < MaxSz; ++i)
			ws[i << 1] = ws[i], ws[i << 1 | 1] = (LL)ws[i] * w % Mod;
		MaxSz <<= 1;
	}
}

inline void FNTT(int *A, int Ty) {
	for (int i = 0; i < Sz; ++i) if (R[i] < i) std::swap(A[R[i]], A[i]);
	for (int j = 1, j2 = 2; j < Sz; j <<= 1, j2 <<= 1) {
		int X, Y;
		for (int i = 0; i < Sz; i += j2) {
			for (int k = 0; k < j; ++k) {
				X = A[i + k], Y = (LL)ws[j + k] * A[i + j + k] % Mod;
				A[i + k] -= (A[i + k] = X + Y) >= Mod ? Mod : 0;
				A[i + j + k] += (A[i + j + k] = X - Y) < 0 ? Mod : 0;
			}
		}
	}
	if (!~Ty) {
		for (int i = 0; i < Sz; ++i) A[i] = (LL)A[i] * InvSz % Mod;
		std::reverse(A + 1, A + Sz);
	}
}

inline void PolyConv(int *_A, int N, int *_B, int M, int *_C, int tN = -1) {
	if (tN == -1) tN = N + M - 1;
	static int A[MS], B[MS];
	InitFNTT(N + M - 1);
	for (int i = 0; i < N; ++i) A[i] = _A[i];
	for (int i = N; i < Sz; ++i) A[i] = 0;
	for (int i = 0; i < M; ++i) B[i] = _B[i];
	for (int i = M; i < Sz; ++i) B[i] = 0;
	FNTT(A, 1), FNTT(B, 1);
	for (int i = 0; i < Sz; ++i) A[i] = (LL)A[i] * B[i] % Mod;
	FNTT(A, -1);
	for (int i = 0; i < tN; ++i) _C[i] = A[i];
}

const int MN = 500005, MP = 41539;

bool ip[MN];
int p[MP], pc;
int h[MN], sig[MN], f[MN];
int ST[19][MN];
void Init(int N) {
	sig[1] = 1;
	for (int i = 2; i <= N; ++i) {
		if (!ip[i]) p[++pc] = i, sig[i] = 2, h[i] = 1;
		for (int j = 1, k; j <= pc; ++j) {
			if ((k = p[j] * i) > N) break;
			ip[k] = 1;
			if (i % p[j]) {
				sig[k] = sig[i] * 2;
				h[k] = i;
			} else {
				sig[k] = sig[h[i]] * (sig[i / h[i]] + 1);
				h[k] = h[i];
			}
		}
	}
	PolyConv(sig, N + 1, sig, N + 1, f, N + 1);
	for (int i = 1; i <= N; ++i) ST[0][i] = i;
	for (int j = 0; 2 << j <= N; ++j)
		for (int i = 2 << j; i <= N; ++i) {
			int p1 = ST[j][i - (1 << j)];
			int p2 = ST[j][i];
			ST[j + 1][i] = f[p1] >= f[p2] ? p1 : p2;
		}
}

inline int RMQ(int l, int r) {
	int j = 31 - __builtin_clz(r - l + 1);
	int p1 = ST[j][l + (1 << j) - 1];
	int p2 = ST[j][r];
	return f[p1] >= f[p2] ? p1 : p2;
}

int main() {
	Init(500000);
	int Tests;
	scanf("%d", &Tests);
	while (Tests--) {
		int l, r;
		scanf("%d%d", &l, &r);
		int ans = RMQ(l, r);
		printf("%d %d\n", ans, f[ans]);
	}
	return 0;
}

\(c = x - a\)\(d = y - b\),此时 \(w = (a + c) (b + d) - a d - b c = a b + c d\)。其中 \(a, b, c, d\) 都是正整数。

也就是说令 \(f(w)\)\(\mathbf{OGF}\)\(F\),则我们有 \(F = G^2\),其中 \(G\)\(\sigma_0 (w)\)(因数个数函数)的 \(\mathbf{OGF}\)

我们只需筛出因数个数函数,然后 NTT 做卷积,然后建立 ST 表进行 RMQ 的查询即可。

时间复杂度为 \(\mathcal O (v \log v + Q)\),其中 \(v\) 为值域即 \(\max r\)评测链接

2020-10-20

LI. Intrinsic Interval

题意简述

给定一个 \(1 \sim n\) 的排列 \([\pi_1, \pi_2, \ldots , \pi_n]\)。再给出 \(m\) 个询问,第 \(i\) 个询问给出 \(a_i, b_i\),你需要求出包含区间 \([a_i, b_i]\) 的最小连续段。

数据范围:\(1 \le n, m \le {10}^5\)

AC 代码
#include <cstdio>
#include <algorithm>
#include <vector>
#include <queue>

const int MN = 100005, MQ = 100005, MS = 1 << 18 | 7;

int N, Q, A[MN], iA[MN], lb[MQ], Ans1[MQ], Ans2[MQ];
std::vector<int> V[MN];

#define li (i << 1)
#define ri (li | 1)
#define mid ((l + r) >> 1)
#define ls li, l, mid
#define rs ri, mid + 1, r
int val[MS], pos[MS], tg[MS];
inline void P(int i, int x) {
	tg[i] += x, val[i] += x;
}
inline void Pushdown(int i) {
	if (tg[i]) P(li, tg[i]), P(ri, tg[i]), tg[i] = 0;
}
void Build(int i, int l, int r) {
	pos[i] = r;
	if (l == r) return ;
	Build(ls), Build(rs);
}
void Mdf(int i, int l, int r, int a, int b, int x) {
	if (r < a || b < l) return ;
	if (a <= l && r <= b) return P(i, x);
	Pushdown(i), Mdf(ls, a, b, x), Mdf(rs, a, b, x);
	if (val[ri] <= val[li]) val[i] = val[ri], pos[i] = pos[ri];
	else val[i] = val[li], pos[i] = pos[li];
}
int Qur(int i, int l, int r, int b) {
	if (b < l || val[i] > 1) return -1;
	if (pos[i] <= b) return pos[i];
	Pushdown(i);
	int p = Qur(rs, b);
	if (p != -1) return p;
	return Qur(ls, b);
}

struct Cmp {
	bool operator () (int i, int j) {
		return lb[i] < lb[j];
	}
};
std::priority_queue<int, std::vector<int>, Cmp> pq;

int main() {
	scanf("%d", &N);
	for (int i = 1; i <= N; ++i) scanf("%d", &iA[i]), A[iA[i]] = i;
	scanf("%d", &Q);
	for (int i = 1, r; i <= Q; ++i) scanf("%d%d", &lb[i], &r), V[r].push_back(i);
	Build(1, 1, N);
	for (int i = 1; i <= N; ++i) {
		int p = iA[i];
		Mdf(1, 1, N, A[p - 1] < i ? A[p - 1] + 1 : 1, i, 1);
		if (p <= N && A[p + 1] < i) Mdf(1, 1, N, 1, A[p + 1], -1);
		for (int j : V[i]) pq.push(j);
		while (!pq.empty()) {
			int j = pq.top();
			int l = Qur(1, 1, N, lb[j]);
			if (l == -1) break;
			pq.pop(), Ans1[j] = l, Ans2[j] = i;
		}
	}
	for (int i = 1; i <= Q; ++i) printf("%d %d\n", Ans1[i], Ans2[i]);
	return 0;
}

析合树可做,但我不会。

我们考虑用线段树求连续段的方法,有两种方法:一种是扫描线维护两个单调栈;另一种是在值域上扫描,固定值域区间右端点,维护每个值域区间左端点对应形成的段数,在每个段的最左侧统计就是维护相邻两个位置,左边在值域区间外而右边在值域区间内的位置个数。

我喜欢用后者,因为两个单调栈很不好玩。

但是在值域上做扫描线真的可以去求下标区间上的相关问题吗?

没关系,只要先把排列 \(\pi\) 变成它的逆就行了,也就是说我们对 \(\pi^{-1}\) 执行上述算法,并且把 \([a_i, b_i]\) 看成在值域上的区间。

我们注意到关于连续段的一个性质,也就是如果两个连续段相交但不相包含,则它们的交也是一个连续段。

通过这个性质可知,答案是唯一的,而且假设答案为 \([l, r]\),我们还有 \(r\) 是所有包含 \([a_i, b_i]\) 的连续段中最靠左的右端点。

我们提前把区间 \([a_i, b_i]\) 挂在它的右端点 \(b_i\) 处。在扫描线的时候扫到 \(b_i\) 后,就把 \(a_i\) 加进一个大根堆中。

尝试回答询问时,我们按 \(a_i\) 从大到小从大根堆中取出元素,然后在线段树上查值域区间左端点在 \([1, a_i]\) 之间时是否存在连续段,如果存在,返回左端点最靠右的一个。此时如果返回的是存在,则该询问 \(i\) 的答案就是 \([l, r]\),其中 \(l\) 为返回值,\(r\) 为当前扫描线扫到的右端点位置。这种方法最终表现常数较小,可能是因为线段树的访问次数较少。

时间复杂度为 \(\mathcal O (n \log n + m \log m)\)评测链接

2020-10-21

IK. King’s Inspection

题意简述

给定一张有 \(n\) 个点,\(m\) 条有向边的有向图,问是否存在哈密顿圈。如果有,输出一种方案。

数据范围:\(1 \le n \le {10}^5\)\(0 \le m \le n + 20\)

AC 代码
#include <cstdio>
#include <vector>

const int MN = 100005, MD = 25;

int N, M;
std::vector<int> G[MN];
int f[MN], dis[MN], vis[MN];

int Solve(int u, int c) {
	if (vis[u]) return c - vis[u];
	vis[u] = c;
	if ((int)G[u].size() > 1)
		return f[u] = u, dis[u] = 1, 0;
	int d = Solve(G[u][0], c + 1);
	if (f[G[u][0]]) {
		f[u] = f[G[u][0]];
		dis[u] = dis[G[u][0]] + 1;
		return 0;
	}
	return d;
}

int stk[MD], tp;

int cho[MN], vis2[MN], cnt;
inline bool check() {
	++cnt;
	int u = stk[1], len = 0;
	while (1) {
		if (vis2[u] == cnt)
			return u == stk[1] && len == N;
		vis2[u] = cnt;
		len += dis[G[u][cho[u]]];
		u = f[G[u][cho[u]]];
	}
}
bool DFS(int st) {
	if (st > tp) return check();
	int u = stk[st];
	for (int j = 0; j < (int)G[u].size(); ++j) {
		cho[u] = j;
		if (DFS(st + 1)) return 1;
	}
	return 0;
}

int main() {
	scanf("%d%d", &N, &M);
	for (int i = 1, x, y; i <= M; ++i) {
		scanf("%d%d", &x, &y);
		G[x].push_back(y);
	}
	for (int i = 1; i <= N; ++i) if (G[i].empty())
		return puts("There is no route, Karl!"), 0;
	for (int i = 1; i <= N; ++i) if (!f[i]) {
		int d = Solve(i, 1);
		if (d) {
			if (d != N) return puts("There is no route, Karl!"), 0;
			printf("1 ");
			for (int x = G[1][0]; x != 1; x = G[x][0])
				printf("%d ", x);
			puts("1");
			return 0;
		}
	}
	for (int i = 1; i <= N; ++i)
		if ((int)G[i].size() > 1)
			stk[++tp] = i;
	if (DFS(1)) {
		printf("1 ");
		for (int x = G[1][cho[1]]; x != 1; x = G[x][cho[x]])
			printf("%d ", x);
		puts("1");
		return 0;
	}
	puts("There is no route, Karl!");
	return 0;
}

这题就非常搞笑了,注意到一个哈密顿圈已经有 \(n\) 条边了,最多多出 \(20\) 条。

我们先判断是不是每个点都有出度,如果是的话,出度 \(\ge 2\) 的点就只有最多 \(20\) 个了。

我们先对出度 \(= 1\) 的点进行一个缩点,以降低后面的复杂度。

然后对那最多 \(20\) 个点进行枚举出边的 DFS,不难证明这部分最多 \(2^{20}\) 种情况。

枚举完之后的 check 因为已经缩点过了,所以只要 \(\mathcal O (m - n)\) 次运算就能判断。

时间复杂度为 \(\mathcal O (n + (m - n) 2^{m - n})\)评测链接

2020-10-22

DD. Money for Nothing

题意简述

给出数轴上 \(m\) 个左端点和 \(n\) 个右端点。

\(i\) 个左端点的位置为 \(d_i\),权值为 \(p_i\)

\(i\) 个右端点的位置为 \(e_i\),权值为 \(q_i\)

你需要选取一个左端点 \(x\) 和一个右端点 \(y\),满足 \(d_x < e_y\),并获得 \((e_y - d_x) (q_y - p_x)\) 的收益。

请求出能够获得收益的最大值,如果无法进行选择或最大收益为负数,则输出 \(0\)

数据范围:\(1 \le n, m \le 5 \times {10}^5\)

AC 代码
#include <cstdio>
#include <algorithm>

typedef long long LL;
const int MN = 500005;

int M, N;
int p[MN], d[MN], zm[MN], tM;
int q[MN], e[MN], zn[MN], tN;
LL Ans;

inline LL Calc(int x, int y) {
	return (LL)(e[zn[y]] - d[zm[x]]) * (q[zn[y]] - p[zm[x]]);
}
void Solve(int l, int r, int a, int b) {
	if (r < l) return ;
	int mid = (l + r) / 2, pos = 0;
	for (int i = a; i <= b; ++i)
		if (!pos || Calc(mid, i) > Calc(mid, pos))
			pos = i;
	if (e[zn[pos]] > d[zm[mid]] && q[zn[pos]] > p[zm[mid]])
		Ans = std::max(Ans, Calc(mid, pos));
	Solve(l, mid - 1, a, pos);
	Solve(mid + 1, r, pos, b);
}

int main() {
	scanf("%d%d", &M, &N);
	for (int i = 1; i <= M; ++i) scanf("%d%d", &p[i], &d[i]), zm[i] = i;
	for (int i = 1; i <= N; ++i) scanf("%d%d", &q[i], &e[i]), zn[i] = i;
	std::sort(zm + 1, zm + M + 1, [](int i, int j) {
		return d[i] == d[j] ? p[i] < p[j] : d[i] < d[j];
	});
	std::sort(zn + 1, zn + N + 1, [](int i, int j) {
		return e[i] == e[j] ? q[i] > q[j] : e[i] > e[j];
	});
	tM = 1, tN = 1;
	for (int i = 2; i <= M; ++i) if (p[zm[i]] < p[zm[tM]]) zm[++tM] = zm[i];
	for (int i = 2; i <= N; ++i) if (q[zn[i]] > q[zn[tN]]) zn[++tN] = zn[i];
	M = tM, N = tN;
	std::reverse(zn + 1, zn + N + 1);
	Solve(1, M, 1, N);
	printf("%lld\n", Ans);
	return 0;
}

我们把每个左端点看成坐标系中的 \((d_i, p_i)\),右端点看成坐标系中的 \((e_i, q_i)\)

也就是求选择一个左端点作为左下角,一个右端点作为右上角,中间形成的矩形的最大面积。

我们首先发现,如果一个左端点 \(i\) 满足:存在一个左端点 \(j \ne i\) 使得 \(d_j \le d_i\)\(p_j \le p_i\),也即 \(j\)\(i\) 的左下角,那么这个左端点 \(i\) 是可以被删去的。

同理如果一个右端点 \(i\) 的右上角有其他的右端点,它也是可以被删去的。

我们把无用的点删去之后,所有左端点就保证形成一个左上到右下的链状,所有右端点也一样。

这时候其实是有一个决策单调性的套路的:

  • 我们将所有左端点按照横坐标排序,然后按顺序编号,所有右端点也一样编号。
  • 我们先做一个假设:所有左端点都是能匹配上至少一个右端点的,也即对于每个左端点,都至少有一个右端点在它的右上角。
  • 那么每个左端点是有一个最优右端点选取位置的,记作 \(g_i = j\),表示左端点 \(i\) 在右端点为 \(j\) 时取到最大面积。
  • 我们可以证明一定有 \(g_{i - 1} \le g_i\),因为如果 \(g_{i - 1} > g_i\) 可以导出矛盾:
    • 考虑 \(g_{i - 1} = j\)\(g_i = k\),且 \(k < j\)
    • \((e_k - d_{i - 1}) (q_k - p_{i - 1}) < (e_j - d_{i - 1}) (q_j - p_{i - 1})\)
    • \((e_k - d_i) (q_k - p_i) > (e_j - d_i) (q_j - p_i)\)
    • 两式相减:\(e_k p_i + d_i q_k - e_k p_{i - 1} - d_{i - 1} q_k < e_j p_i + d_i q_j - e_j p_{i - 1} - d_{i - 1} q_j\)
    • 变换一下:\((e_j - e_k) (p_{i - 1} - p_i) < (d_i - d_{i - 1}) (q_j - q_k)\)
    • 左边是两个正数相乘,是正数,右边是正数乘负数,是负数,导出矛盾。
  • 上面的证明稍微有点不严谨的地方:比如 \(g_i\) 不唯一的时候怎么办,可以证明此时后文中的算法也同样成立,在此不展开。
  • 接下来我们考虑删去那个假设,如果某个左端点没有合法的右端点进行匹配怎么办?
  • 注意到,如果按照那个公式,一个左端点如果匹配左上角或者右下角的右端点,贡献都是负的。
  • 只有匹配左下角的右端点的时候,贡献又会变回正数,但是这个时候我们不能把贡献计入答案。
  • 此时只需注意两点:
    • 上面的论证是不依靠左端点和右端点之间的关系的:只依靠了左端点或右端点内部的大小关系。
    • 一个左端点是不可能同时匹配到右上角和左下角的两个右端点的。
  • 所以计算得到 \(g_i\) 之后,如果 \(g_i\) 的贡献是负数,则说明没有右上角或者左下角的右端点,不用管。
  • 如果 \(g_i\) 的贡献是正数,但是取到的是右下角的右端点,则不更新答案也不会漏算,因为不可能匹配到合法的右端点。
  • 所以只有当我们检查到右端点 \(g_i\) 是严格在 \(i\) 的右上方的时候才去更新答案,这样不会漏算,而且 \(g\) 序列还满足性质。

不过这并没有什么用,因为我们算 \(g\) 序列的时间复杂度仍然不可接受。

这个时候就直接用分治求决策单调性的算法就行了。

我们考虑 \(m\) 个左端点中一半的位置 \(\mathrm{mid} \approx m / 2\),然后用 \(\mathcal O (n)\) 的时间在所有右端点中暴力遍历,求出 \(g_{\mathrm{mid}}\)

然后递归进 \([1, \mathrm{mid} - 1]\)\([\mathrm{mid} + 1, m]\) 两个子区间,它们右端点的遍历范围也分别缩短为 \([1, g_{\mathrm{mid}}]\)\([g_{\mathrm{mid}}, n]\)

这样只会递归 \(\log m\) 层,每层的总时间复杂度是 \(\mathcal O (n)\) 的。

时间复杂度为 \(\mathcal O (n \log n + m \log m)\)评测链接

2020-10-23

BL. Weather Report

题意简述

有一个 \(n\) 位四进制数,每一位都有 \(p_i\) 的概率为 \(i\)\(0 \le i < 4\))。

总共可以产生 \(4^n\) 个不同的数 ,但是每一个产生的概率不同。

你需要给每个数分配一个编码,每个编码是一个二进制序列。

你需要保证没有任何两个二进制序列,满足其中一个是另一个的前缀,也就是说这是一套前缀编码。

你需要求出当使用最优编码方案时,这个四进制数按规则随机产生时,对应的编码的最短期望长度。

数据范围:\(1 \le n \le 20\)

AC 代码
#include <cstdio>
#include <queue>

typedef long long LL;
typedef double r80;

int N;
r80 pr[5];

struct dat {
	r80 val;
	LL cnt;
	dat() {}
	dat(r80 v, LL c) : val(v), cnt(c) {}
	friend bool operator < (dat x, dat y) {
		return x.val > y.val;
	}
};
std::priority_queue<dat> pq;

int main() {
	scanf("%d", &N);
	for (int i = 1; i <= 4; ++i) scanf("%lf", &pr[i]);
	LL c1 = 1;
	r80 z1 = 1;
	for (int i = 1; i <= N; ++i) c1 *= i;
	for (int a = 0; a <= N; ++a, z1 *= pr[1], c1 /= a) {
		LL c2 = c1;
		r80 z2 = z1;
		for (int b = 0; b <= N - a; ++b, z2 *= pr[2], c2 /= b) {
			LL c3 = c2;
			r80 z3 = z2;
			for (int c = 0; c <= N - a - b; ++c, z3 *= pr[3], c3 /= c) {
				LL c4 = c3;
				r80 z4 = z3;
				for (int d = 1; d <= N - a - b - c; ++d)
					z4 *= pr[4], c4 /= d;
				pq.push(dat(z4, c4));
			}
		}
	}
	printf("%d\n", (int)pq.size());
	int mx = 0, cc = 0;
	r80 Ans = 0;
	while (!pq.empty()) {
		mx = mx < (int)pq.size() ? (int)pq.size() : mx;
		++cc;
		dat p = pq.top(); pq.pop();
		if (p.cnt >= 2)
			Ans += (2 * p.val) * (p.cnt / 2),
			pq.push(dat(2 * p.val, p.cnt / 2));
		if (p.cnt % 2 == 1) {
			if (pq.empty()) break;
			dat q = pq.top(); pq.pop();
			Ans += p.val + q.val;
			pq.push(dat(p.val + q.val, 1));
			if (q.cnt >= 2)
				pq.push(dat(q.val, q.cnt - 1));
		}
	}
	printf("%.10f\n", Ans);
	printf("%d, %d\n", mx, cc);
	return 0;
}

这显然是一个哈夫曼编码的问题,但是我们并不能生成所有 \(4^n\) 个数去直接模拟哈夫曼编码的过程。

注意到不同的出现概率最多只有 \(\displaystyle \binom{n + 3}{3}\) 种,我们把相同的概率合并进行加速即可。

时间复杂度为 \(\mathcal O (n^4 \log n)\)评测链接

RG. Gangsters in Central City

题意简述

有一棵 \(n\)\(n \ge 2\))个点的以 \(1\) 号点为根的有根树。本题中叶子结点很重要,显然根不是叶子结点。

有一些叶子结点是黑色的,其它所有结点都是白色的。

你需要切断一些边,使得根(\(1\) 号点)所在的连通块均是白色结点,即不和黑色的叶子连通。

你需要最小化切断的边的数量,同时尽量最小化与根不连通的白色叶子数量。

\(q\) 次操作,每次改变一个叶子结点的颜色,你需要在每次改变后回答上述两个你需要最小化的值。

初始时没有叶子结点是黑色的。

数据范围:\(2 \le n \le {10}^5\)\(1 \le q \le {10}^5\)

AC 代码
#include <cstdio>
#include <algorithm>
#include <vector>
#include <set>

const int MN = 100005;

int N, par[MN];
std::vector<int> G[MN];

int bel[MN], dep[MN], siz[MN], cnt[MN], pref[MN], top[MN], dfn[MN], idf[MN], dfc;
void DFS0(int u, int b) {
	bel[u] = b, siz[u] = 1, cnt[u] = G[u].empty();
	for (int v : G[u]) {
		dep[v] = dep[u] + 1;
		DFS0(v, b ? b : v);
		siz[u] += siz[v];
		cnt[u] += cnt[v];
		if (siz[pref[u]] < siz[v]) pref[u] = v;
	}
}
void DFS1(int u, int t) {
	idf[dfn[u] = ++dfc] = u, top[u] = t;
	if (pref[u]) DFS1(pref[u], t);
	for (int v : G[u]) if (v != pref[u])
		DFS1(v, v);
}
inline int LCA(int u, int v) {
	while (top[u] != top[v]) {
		if (dep[top[u]] < dep[top[v]]) std::swap(u, v);
		u = par[top[u]];
	}
	return dep[u] < dep[v] ? u : v;
}

std::set<int> st[MN];
inline int calc(int x) {
	if (st[x].empty()) return 0;
	return cnt[LCA(idf[*st[x].begin()], idf[*--st[x].end()])];
}

int main() {
	int Q;
	scanf("%d%d", &N, &Q);
	for (int i = 2; i <= N; ++i)
		scanf("%d", &par[i]),
		G[par[i]].push_back(i);
	DFS0(1, 0), DFS1(1, 1);
	int ans0 = 0, sum = 0, num = 0;
	for (int q = 1; q <= Q; ++q) {
		int x;
		scanf("%*s%d", &x);
		sum -= calc(bel[x]);
		if (st[bel[x]].count(dfn[x])) {
			st[bel[x]].erase(dfn[x]);
			--num;
			if (st[bel[x]].empty()) --ans0;
		} else {
			if (st[bel[x]].empty()) ++ans0;
			st[bel[x]].insert(dfn[x]);
			++num;
		}
		sum += calc(bel[x]);
		printf("%d %d\n", ans0, sum - num);
	}
	return 0;
}

考虑根的每棵子树,如果某棵子树中有黑色结点,则我们直接切断连向这棵子树的边即可。

也就是说第一个答案,就是含有黑色结点的子树数量。

考虑第二个答案,我们已知每个含有黑色结点的子树都需要花费恰好 \(1\) 条边的代价去切断。

显然我们在其中所有黑色结点的 LCA 处切断即可。

预处理每个点的子树中叶子的个数,回答询问时用 std::set 动态维护 DFS 序最小和最大的两个结点,然后根据它们算出 LCA,进一步求出修改的结点所在的那棵根下接的子树中答案的改变量。

时间复杂度为 \(\mathcal O (n + q \log n)\)评测链接

2020-10-24

LC. Cumulative Code

题意简述

有一棵 \(k\)\(k \ge 2\))层的满二叉树,共有 \(2^k - 1\) 个结点,以 \(1\) 号点为根。

如果 \(i\) 号点不是叶子,则它的左孩子的编号为 \(2 i\),右孩子的编号为 \(2 i + 1\)

令这棵树的 Prüfer 序列(Prüfer 序列的定义不再展开)为 \([p_1, p_2, \ldots , p_{2^k - 3}]\)

\(q\) 次询问,每次询问给定正整数 \(a, d, m\),求 \(\displaystyle \sum_{i = 0}^{m - 1} p_{a + i \cdot d}\)

数据范围:\(2 \le k \le 30\)\(1 \le q \le 300\)

AC 代码
#include <cstdio>

typedef long long LL;

int Calc0_0(int k, int rt, int pos) {
	if (pos == (1 << (k - 1)) - 2) return rt;
	if (pos == (1 << k) - 3) return rt;
	if (pos < (1 << (k - 1)) - 2)
		return Calc0_0(k - 1, rt << 1, pos);
	return Calc0_0(k - 1, rt << 1 | 1, pos - ((1 << (k - 1)) - 1));
}
int Calc0(int k, int rt, int pos) {
	if (k == 2) return rt;
	if (pos < (1 << (k - 1))) {
		if (pos == (1 << (k - 1)) - 1) return rt << 1 | 1;
		if (pos == (1 << (k - 1)) - 2) return rt;
		return Calc0_0(k - 1, rt << 1, pos);
	}
	return Calc0(k - 1, rt << 1 | 1, pos - (1 << (k - 1)));
}

LL f[31][32767], g[31][32767];
void Calc1_1(int k, int d, int rem) {
	if (f[k][rem] != -1) return ;
	if (rem >= (1 << k) - 2) return f[k][rem] = g[k][rem] = 0, void();
	LL retf = 0, retg = 0;
	if (((1 << (k - 1)) - 2) % d == rem) ++retf;
	if (((1 << k) - 3) % d == rem) ++retf;
	Calc1_1(k - 1, d, rem);
	retf += 2 * f[k - 1][rem], retg += g[k - 1][rem];
	int rem2 = ((rem - ((1 << (k - 1)) - 1)) % d + d) % d;
	Calc1_1(k - 1, d, rem2);
	retf += 2 * f[k - 1][rem2], retg += f[k - 1][rem2] + g[k - 1][rem2];
	f[k][rem] = retf, g[k][rem] = retg;
}
LL Calc1_0(int k, int rt, int d, int pos) {
	if (pos % d >= (1 << k) - 2) return 0;
	if (pos < (1 << (k - 1)) - 2)
		return Calc1_0(k - 1, rt << 1, d, pos);
	Calc1_1(k - 1, d, pos % d);
	LL ret = f[k - 1][pos % d] * (rt << 1) + g[k - 1][pos % d];
	if (pos >= (1 << (k - 1)) - 2 && ((1 << (k - 1)) - 2) % d == pos % d)
		ret += rt;
	if (pos == (1 << (k - 1)) - 2) return ret;
	int pos2 = pos - ((1 << (k - 1)) - 1);
	if (pos < (1 << k) - 3)
		return ret + Calc1_0(k - 1, rt << 1 | 1, d, pos2);
	Calc1_1(k - 1, d, pos2 % d);
	ret += f[k - 1][pos2 % d] * (rt << 1 | 1) + g[k - 1][pos2 % d];
	if (pos == (1 << k) - 3) ret += rt;
	return ret;
}
LL Calc1(int k, int rt, int d, int pos) {
	if (k == 2) return rt;
	if (pos < (1 << (k - 1)) - 2)
		return Calc1_0(k - 1, rt << 1, d, pos);
	LL ret = 0;
	if (pos >= (1 << (k - 1)) - 1 && ((1 << (k - 1)) - 1) % d == pos % d)
		ret += rt << 1 | 1;
	if (pos >= (1 << (k - 1)) - 2 && ((1 << (k - 1)) - 2) % d == pos % d)
		ret += rt;
	Calc1_1(k - 1, d, pos % d);
	ret += f[k - 1][pos % d] * (rt << 1) + g[k - 1][pos % d];
	if (pos >= 1 << (k - 1))
		ret += Calc1(k - 1, rt << 1 | 1, d, pos - (1 << (k - 1)));
	return ret;
}

int main() {
	int K, Q;
	scanf("%d%d", &K, &Q);
	for (int q = 1; q <= Q; ++q) {
		int a, d, m;
		scanf("%d%d%d", &a, &d, &m);
		if (m <= d) {
			LL Ans = 0;
			for (int i = 0; i < m; ++i)
				Ans += Calc0(K, 1, a + i * d - 1);
			printf("%lld\n", Ans);
		} else {
			for (int j = 1; j <= K; ++j)
				for (int i = 0; i < d; ++i)
					f[j][i] = -1;
			LL Ans = Calc1(K, 1, d, a + (m - 1) * d - 1);
			if (a - d - 1 >= 0) Ans -= Calc1(K, 1, d, a - d - 1);
			printf("%lld\n", Ans);
		}
	}
	return 0;
}

通过观察 \(k = 5\) 时的 Prüfer 序列,我们能找到一些规律:

[[[8,8],4,[9,9],4],2,[[10,10],5,[11,11],5],2],(1,3) |
	[[12,12],6,[13,13],6],(3,7) |
		[14,14],(7,15) |
			{15}

\(j\)\(k - 1\) 逐渐减小至 \(2\),Prüfer 序列中依次添加长为 \(2^j\) 的序列,最后在末尾添加 \(2^{k - 1} - 1\)

其中的每个 \(j\) 对应的序列,最后都会有两个数,而前面是长为 \(2^j - 2\) 的按层级递归下去的序列。

这个 Prüfer 序列并没有合适的通项公式,但是我们仍能找到一些能帮助我们简化计算的规律。

观察序列中重复出现的,用方括号 [] 括起来的部分,它们的每一项是有着 \(f_i r + g_i\) 的形式的,更具体地说:

  • 对于包含元素个数相同的方括号 [] 内部,它们在对应位置上的 \(f_i\)\(g_i\) 是对应相同的,而只有 \(r\) 不同。

例如 [[8,8],4,[9,9],4][[12,12],6,[13,13],6] 是只有 \(r\) 有区别的:

  • 它们的 \((f_i, g_i)\) 是:\([[(2, 0), (2, 0)], (1, 0), [(2, 1), (2, 1)], (1, 0)]\)
  • 而前者的 \(r = 4\),后者的 \(r = 6\)

接下来我们考虑设计本题的具体算法。

主要思想是分治,可以以模 \(d\) 的余数为一个参数,递归到更小的结构中进行计算。

但是我们注意到,递归进 \(j\) 层后,可能的递归树大小也达到了 \(\mathcal O \!\left( 2^j \right)\) 的级别,递归进底层是无法接受的。

我们同时注意到,递归的层数越深,对应的区间长度也越小,如果 \(d\) 比区间长度大,许多区间内是不存在需要求和的下标的。

但是如果 \(d\) 比较小,在很深的层数才比对应的区间长度大,我们又反过来发现递归树的大小为 \(\mathcal O \!\left( 2^j \right)\) 已经远大于 \(d\) 了。
此时根据抽屉原理,一定有许多相同的余数被重复计算了,通过记忆化搜索免除重复的计算,复杂度本应为 \(\mathcal O \!\left( \min \!\left\{ 2^j, d \right\} \right)\)

而如果 \(d\) 比较大,在递归到深处时已经有很多区间没有必要再递归下去了,同样减小了复杂度。

抛开复杂的分析,我们直接按照 \(d\) 的大小进行分类:

  1. 如果 \(d > \sqrt{2^k}\),这意味着 \(m \le \sqrt{2^k}\),我们对 \(m\) 个下标进行每次只会递归进一个子区间的二分查找。
  2. 如果 \(d \le \sqrt{2^k}\),我们采用分治算法,每层按照模 \(d\) 的余数分类,最多只会有 \(d\) 的计算量。

其中第一种的复杂度是 \(\mathcal O (m k)\) 的,第二种的复杂度是 \(\mathcal O (k d)\) 的,当 \(m \le d\) 的时候使用第一种,否则使用第二种即可。

本题处理递归过程中的细节较多,具体实现见代码。

时间复杂度为 \(\mathcal O \!\left( q k 2^{k / 2} \right)\)评测链接

2020-10-29

RI. Integral Polygons

题意简述

给定一个有 \(n\) 个顶点的格点凸多边形。按顺序给出这些点的坐标 \((x_i, y_i)\)

你需要求出,连接两个不相邻顶点,可以把这个凸多边形切割成两个整数面积的部分的方案数。

数据范围:\(3 \le n \le 2 \times {10}^5\)

AC 代码
#include <cstdio>

typedef long long LL;
const int MN = 200005;

int N, x[MN], y[MN], s[MN];
int buk[8];
LL Ans;

int main() {
	scanf("%d", &N);
	for (int i = 1; i <= N; ++i) {
		scanf("%d%d", &x[i], &y[i]);
		x[i] &= 1, y[i] &= 1;
		if (i >= 2) s[i] = s[i - 1] ^ (x[i] & y[i - 1]) ^ (y[i] & x[i - 1]);
		++buk[x[i] << 2 | y[i] << 1 | s[i]];
	}
	if (s[N] ^ (x[1] & y[N]) ^ (y[1] & x[N])) return puts("0"), 0;
	for (int a = 0; a < 8; ++a)
		for (int b = 0; b < 4; ++b)
			Ans += (LL)buk[a] * buk[b << 1 ^ (a >> 2 & b & 1) ^ (a >> 1 & b >> 1) ^ (a & 1)];
	printf("%lld\n", (Ans - 3 * N) / 2);
	return 0;
}

先判这个凸多边形的面积是不是整数,如果不是输出 \(0\) 跑路。

根据叉积算面积那套理论,等价于切割出的其中一半的所有相邻点对叉积之和是偶数。

所以我们也只考虑坐标值模 \(2\) 的结果。

假设连接的两点编号为 \(p\)\(q\)\(p < q\)),我们就是要 \(\displaystyle \left[ (x_q y_p - y_q x_p) + \sum_{i = p + 1}^{q} (x_{i - 1} y_i - y_{i - 1} x_i) \right]\) 是偶数。

我们记 \(\displaystyle s_p = \left[ \sum_{i = 2}^{p} (x_{i - 1} y_i - y_{i - 1} x_i) \right] \bmod 2\)

原式也就是要让 \((x_q y_p + y_q x_p + s_q + s_p) \bmod 2 = 0\)

那么每个点 \(p\) 仅由这三个量 \(x_p, y_p, s_p\) 的奇偶性确定,也就只有 \(8\) 类点了。随便做。

时间复杂度为 \(\mathcal O (n)\)评测链接

2020-10-30

MB. Bipartite Blanket

题意简述

给定一张 \(n + m\) 个点的二分图,左部有 \(n\) 个点,右部有 \(m\) 个点。即 \(G = \langle V, E \rangle\),其中 \(V = L + R\)

每个点有点权,左部第 \(i\) 个点的点权为 \(a_i\),右部第 \(i\) 个点的点权为 \(b_i\)

定义一个点集(为这 \(n + m\) 个点的子集)合法,当且仅当这个点集被一个匹配覆盖,即点集中的每个点都有相邻的匹配边。

给定一个权值 \(t\),求出点权和 \(\ge t\) 的合法点集的个数。

数据范围:\(1 \le n, m \le 20\)

AC 代码
#include <cstdio>
#include <algorithm>

typedef long long LL;
const int MN = 20;

int N, M;
int wA[MN], wB[MN];
int a[MN], b[MN];
int count[1 << MN];
int f[1 << MN], v[1 << MN], g[1 << MN];
int seqA[1 << MN | 7], cA;
int seqB[1 << MN | 7], cB;

void Solve(int n, int *w, int *c, int *seq, int &t) {
	for (int i = 0; i < n; ++i) f[1 << i] = w[i], v[1 << i] = c[i];
	for (int i = 0; i < 1 << n; ++i) {
		if (count[i] >= 2)
			f[i] = f[i & -i] | f[i - (i & -i)],
			v[i] = v[i & -i] + v[i - (i & -i)];
		g[i] = count[f[i]] < count[i];
	}
	for (int j = 0; j < n; ++j)
		for (int i = 0; i < 1 << n; ++i)
			if (~i >> j & 1)
				g[i | 1 << j] |= g[i];
	for (int i = 0; i < 1 << n; ++i) if (!g[i])
		seq[++t] = v[i];
}

int main() {
	scanf("%d%d", &N, &M);
	for (int i = 0; i < N; ++i) {
		for (int j = 0; j < M; ++j) {
			int x;
			scanf("%1d", &x);
			wA[i] |= x << j;
			wB[j] |= x << i;
		}
	}
	for (int i = 1; i < 1 << 20; ++i)
		count[i] = count[i >> 1] + (i & 1);
	for (int i = 0; i < N; ++i) scanf("%d", &a[i]);
	for (int i = 0; i < M; ++i) scanf("%d", &b[i]);
	Solve(N, wA, a, seqA, cA);
	Solve(M, wB, b, seqB, cB);
	std::sort(seqA + 1, seqA + cA + 1);
	std::sort(seqB + 1, seqB + cB + 1);
	int t;
	scanf("%d", &t);
	LL Ans = 0;
	int j = cB + 1;
	for (int i = 1; i <= cA; ++i) {
		while (j > 1 && seqA[i] + seqB[j - 1] >= t) --j;
		Ans += cB - j + 1;
	}
	printf("%lld\n", Ans);
	return 0;
}

我们仅需注意到一个关键性质:

  • 根据 Hall 定理,我们知道,给定左部点的一个子集 \(S \subseteq L\),存在一个左部点为它们的匹配,
    当且仅当:对于所有的 \(T \subseteq S\),均有 \(|\operatorname{adj}(T)| \ge |T|\)。而对于右部点的情况,此定理是对称的。
  • 其中 \(\displaystyle \operatorname{adj}(T) = \bigcup_{u \in T} \operatorname{adj}(u)\),而 \(\operatorname{adj}(u) = \{ v \mid (u, v) \in E \}\)。即 \(S\) 的每个子集在右侧连接的点的个数均不小于子集大小。
  • 然而我们需要判断整个点集的某个子集是否存在一个匹配能覆盖之,这该如何是好呢?
  • 给出结论:考虑一个子集 \(S = S_1 + S_2\),其中 \(S_1 \subseteq L\)\(S_2 \subseteq R\)
    则存在一个匹配能覆盖 \(S\),当且仅当存在以 \(S_1\) 为左部点的匹配,也存在以 \(S_2\) 为右部点的匹配。
  • 对于这个结论的证明,我们先求出以 \(S_1\) 为左部点的匹配和以 \(S_2\) 为右部点的匹配。
  • 把前者的边染红色,后者的边染蓝色。且前者的边从左部指向右部,后者的边从右部指向左部。
  • 然后把它们都添加进一张新图中,显然每个点的入度和出度都最多为 \(1\),且边是红蓝交错的。
  • 也就是说这张图形成了若干个环和链(以及孤立点),而有出度的点都是属于 \(S\) 的关键点。
  • 对于环上的边,显然每个环都是偶环,取一半即可。对于链上的边,从链首贪心取,取不到链尾没关系,因为链尾不关键。

也就是说,只要 \(S_1 \subseteq L\) 合法,而且 \(S_2 \subseteq R\) 合法,则 \(S = S_1 + S_2\) 也是合法的。

我们仅需求出所有合法的 \(S_1\)\(S_2\)(用 FWT),然后把它们的点权和排序,做双指针即可。

时间复杂度为 \(\mathcal O (n 2^n + m 2^m)\)评测链接

RB. Boys and Girls

题意简述

一个长度为 \(n\)\(n \ge 2\))的环形数组,每个元素为字符 \(\texttt{B}\) 或字符 \(\texttt{G}\)

\(x\) 为两侧的元素中存在至少一个字符 \(\texttt{B}\) 的元素个数。

\(y\) 为两侧的元素中存在至少一个字符 \(\texttt{G}\) 的元素个数。

给出 \(x, y\),请你还原出这个数组,或报告无解。

数据范围:\(2 \le n \le {10}^5\)

AC 代码

文件「RB.cpp」:

#include <cstdio>

int N, x, y;

int main() {
	scanf("%d%d%d", &N, &x, &y);
	if ((N ^ x ^ y) & 1) return puts("Impossible"), 0;
	if (x + y < N) return puts("Impossible"), 0;
	if (x + y == N) {
		if (x && y && x != y) return puts("Impossible"), 0;
		if (!x) for (int i = 1; i <= N; ++i) printf("G");
		if (!y) for (int i = 1; i <= N; ++i) printf("B");
		if (x == y) for (int i = 1; i <= N; ++i) printf(i & 1 ? "B" : "G");
		return puts(""), 0;
	}
	if (x == y && x + y == N + 2) return puts("Impossible"), 0;
	if (N % 4 && x == N && y == N) return puts("Impossible"), 0;
	if (x + y == N + 2) {
		int z = x < y ? x : y;
		for (int i = 1; i <= N; ++i) printf("%c", "BG"[(i <= 2 * z - 3 && (i & 1)) ^ (x < y)]);
		return puts(""), 0;
	}
	int c = (x + y - N - 2) / 4;
	int d = (x + y - N) % 4 != 0;
	int l = N - y + 2;
	int r = y - 4 * c + d - 2;
	if (d && x == N) --l, ++r;
	for (int i = 1; i <= l; ++i) printf("B");
	if (d && x == N) {
		printf("GBB");
		for (int i = 2; i <= c; ++i) printf("GGBB");
	} else
		for (int i = 1; i <= c; ++i) printf(d && i == c ? "GGB" : "GGBB");
	for (int i = 1; i <= r; ++i) printf("G");
	return 0;
}

文件「RB_t.cpp」:

#include <cstdio>
#include <iostream>
#include <string>

const int MN = 17;

int N, x, y;
char S[MN];
std::string f[MN][MN];
int g[MN][MN];

void DFS(int i) {
	if (i > N) {
		S[0] = S[N];
		S[N + 1] = S[1];
		int C1 = 0, C2 = 0, C3 = 0;
		for (int j = 1; j <= N; ++j)
			if (S[j - 1] == 'B' || S[j + 1] == 'B') ++C1;
		for (int j = 1; j <= N; ++j)
			if (S[j - 1] == 'G' || S[j + 1] == 'G') ++C2;
		for (int j = 1; j <= N; ++j)
			if (S[j] != S[j - 1] && S[j] != S[j + 1]) ++C3;
		S[0] = '\0';
		S[N + 1] = '\0';
		if (f[C1][C2] == "" || C3 < g[C1][C2])
			f[C1][C2] = S + 1,
			g[C1][C2] = C3;
		return ;
	}
	S[i] = 'B';
	DFS(i + 1);
	S[i] = 'G';
	DFS(i + 1);
}

int main() {
	scanf("%d", &N);
	DFS(1);
	for (int i = 0; i <= N; ++i) {
		for (int j = 0; j <= N; ++j) {
			if (f[i][j] == "") {
				for (int k = 1; k <= N; ++k) printf(" ");
			} else std::cout << f[i][j];
			if (j == N) puts("");
		}
	}
	return 0;
}

这题主要是找规律,没什么特别的东西。

我先观察到形如 \(\texttt{BBGGBBGGBBGGBBGG}\) 的数组可以让 \(x\)\(y\) 达到最值,也就是 \(x = y = n\),但是此时 \(n\) 必须是 \(4\) 的倍数。

然后其他的就没发现什么东西了。

然后我就写了一个找规律的程序,见上面「AC 代码」中的「RB_t.cpp」。用它跑了一下一些 \(n\) 比较小的情况。

我后面又推出这样一个规律:在特判数组全是一种元素之后,令 \(p\)\(\texttt{G}\) 的连续段的数量(显然 \(\texttt{B}\) 的连续段的数量也同样为 \(p\))。
则有 \(x = \# \texttt{B} + 2 p - d\),其中 \(d\)\(\texttt{G}\) \(\texttt{B}\)长度为 \(\boldsymbol{1}\) 的连续段的数量,注意是 \(\texttt{G}\)\(\texttt{B}\) 均可,而且必须是长度为 \(1\) 的连续段。
\(y\) 也是对称的,有 \(y = \# \texttt{G} + 2 p - d\)。这两个式子中 \(\# \texttt{B}\) 就表示数组中字符 \(\texttt{B}\) 的数量,\(\# \texttt{G}\) 同理。

我感觉 \(d\) 比较大的时候很烦,所以想把 \(d\) 控制在 \(0\)\(1\),所以「RB_t.cpp」中我优先选择 \(d\) 最小的,其次才是字典序最小。

然后就是观察打出来的表,找到规律后就可以直接输出表中对应的字符数组了。

可以观察一下打表程序在 \(n = 10, 11, 12\) 时的情况,对找规律有所帮助。

时间复杂度为 \(\mathcal O (n)\)评测链接

2020-10-31

GK. Knapsack Cryptosystem

题意简述

题面中描述了一种基于背包计数问题的公钥加密系统(维基百科链接):

接收方生成一个长度为 \(n\) 的数组 \([a_1, a_2, \ldots , a_n]\) 和一个数 \(q\),必须满足:

  • 数组 \(a\) 中的每个值以及 \(q\) 都必须是正整数。
  • 数组 \(a\) 中的每个值必须大于之前所有值之和,并且 \(q\) 必须大于数组 \(a\) 中的所有数之和。
    即必须满足 \(\displaystyle a_p > \sum_{i = 1}^{p - 1} a_i\),以及 \(\displaystyle q > \sum_{i = 1}^{n} a_i\)

然后生成一个与 \(q\) 互质的正整数 \(r\),再生成一个长度为 \(n\) 的数组 \(b\),满足 \(b_i = (a_i \cdot r) \bmod q\)

此时 \(n\)\(q\)\(b\) 数组即是公钥,但是 \(a\) 数组和 \(r\) 是私钥。

发送方想要发送一个 \(n\) 比特的信息 \(y\),其中 \(y_i\) 为信息的第 \(i\) 位(\(i\)\(1\) 编号至 \(n\))的值(为 \(0\)\(1\))。

为了发送信息 \(y\),发送方计算 \(\displaystyle s = \left( \sum_{i = 1}^{n} y_i b_i \right) \bmod q\)。即把 \(y\) 中为 \(1\) 的比特位对应的 \(b\) 值求和,再对 \(q\) 取余。

接收方为了还原信息,只需令 \(s' = \left( s \cdot r^{-1}_q \right) \bmod q\),然后贪心依次用 \(a_n, a_{n - 1}, \ldots , a_1\) 去填充 \(s'\) 即可。

身为攻击方的你,在不知道 \(a\) 数组和 \(r\) 的情况下,需要根据 \(n\)\(q\)\(b\) 数组和 \(s\) 破译出信息 \(y\)

数据范围:\(q = 2^{64}\),在此限制下必然有 \(n \le \log_2 q = 64\)

AC 代码
#include <cstdio>
#include <cstring>
#include <climits>
#include <vector>

typedef unsigned long long ULL;
const int MN = 65;

int N, M;
ULL B[MN], A[MN], S, Ans;

struct myHashMap {
	static const int MS = 1 << 19 | 7, Mod = 19260817;
	int head[Mod], nxt[MS], val[MS], tot;
	ULL key[MS];
	myHashMap() { tot = 0, memset(head, 0, sizeof head); }
	inline int getHash(ULL x) {
		return x ^= x << 5, x ^= x >> 17, (x ^ x << 28) % Mod;
	}
	inline void insert(ULL x, int y) {
		int z = getHash(x);
		nxt[++tot] = head[z];
		key[tot] = x, val[tot] = y;
		head[z] = tot;
	}
	inline int get(ULL x) {
		int z = getHash(x);
		for (int i = head[z]; i; i = nxt[i])
			if (key[i] == x) return val[i];
		return -1;
	}
};

int main() {
	scanf("%d", &N);
	for (int i = 1; i <= N; ++i) scanf("%llu", &B[i]);
	scanf("%llu", &S);
	if (N > 39) {
		int k = 0;
		ULL w = B[1], r = 1;
		while (~w & 1) ++k, w >>= 1;
		for (int j = 0; j < 63; ++j, r *= w, w *= w) ;
		for (ULL x = 1; x < 1llu << (64 - N - k + 1); x += 2) {
			ULL v = x * r;
			for (ULL t = 0; t < 1llu << k; ++t) {
				if (t) v += 1llu << (64 - k);
				ULL Sum = A[1] = B[1] * v;
				int ok = 1;
				for (int i = 2; i <= N; ++i) {
					A[i] = B[i] * v;
					if (Sum > ULLONG_MAX - A[i] || A[i] <= Sum) {
						ok = 0;
						break;
					} else Sum += A[i];
				}
				if (ok) {
					S *= v;
					for (int i = N; i >= 1; --i) if (S >= A[i])
						S -= A[i], Ans |= 1llu << (i - 1);
					goto done;
				}
			}
		}
		done: ;
	} else {
		myHashMap &mp = *new myHashMap;
		M = N / 2;
		for (int X = 0; X < 1 << M; ++X) {
			ULL Sum = 0;
			for (int i = 0; i < M; ++i)
				if (X >> i & 1) Sum += B[i + 1];
			mp.insert(Sum, X);
		}
		for (int X = 0; X < 1 << (N - M); ++X) {
			ULL Sum = 0;
			for (int i = 0; i < N - M; ++i)
				if (X >> i & 1) Sum += B[M + i + 1];
			int Y = mp.get(S - Sum);
			if (Y != -1) {
				Ans = (ULL)X << M | Y;
				break;
			}
		}
		delete &mp;
	}
	for (int i = 0; i < N; ++i) printf("%llu", Ans >> i & 1);
	return 0;
}

\(k = \log_2 q = 64\)

我们不妨考虑 \(n \le 42\) 的情况:

  • 此时我们只需枚举 \(y\) 的前 \(\lfloor n / 2 \rfloor\) 位,把它们产生的贡献(\(y_i b_i\) 之和)存储到哈希表中去。
  • 然后枚举 \(y\) 的后 \(\lceil n / 2 \rceil\) 位,用 \(s\) 做减法计算出前 \(\lfloor n / 2 \rfloor\) 位应产生的贡献,在哈希表中查找是否存在对应贡献。
  • 这是经典的折半搜索,时间复杂度可以做到 \(\mathcal O \!\left( \sqrt{2^n} \right)\)

但是此时无法处理 \(n \le 64\) 的情况。

我们考虑当 \(n\) 较大时,\(a_1\) 的取值范围的限制会越来越紧,根据 \(\displaystyle \sum a_i < q\) 的限制,我们可以推出 \(1 \le a_1 < 2^{k - n + 1}\)

这启发我们当 \(n\) 较大时,枚举 \(a_1\) 然后计算出 \(r\) 进而还原整个 \(a\) 数组,因为 \(a_i = \left( b_i \cdot r^{-1}_q \right) \bmod q\)

那么 \(r\)\(r^{-1}_q\) 如何计算呢?我们有 \(a_1 \cdot r \equiv b_1 \pmod{q}\),换句话说就是 \(b_1 \cdot r^{-1}_q \equiv a_1 \pmod{q}\)

解关于 \(x\) 的同余方程 \(b_1 \cdot x \equiv a_1 \pmod{q}\) 即可。范围在 \([0, q)\) 内,且与 \(q\) 互质的 \(x\) 的取值均可以作为 \(r^{-1}_q\) 的值。

我们应注意到 \(b_1\) 不一定与 \(q\) 互质,而又有 \(q = 2^{64}\),所以有可能 \(x\) 的解为 \(x \equiv x_0 \pmod{q / 2^{\alpha}}\)

更准确地说,\(\alpha = \nu_2(b_1)\),即 \(b_1\) 中质因子 \(2\) 的次数。同时需要注意的是我们枚举 \(a_1\) 时也需要保证 \(\nu_2(a_1) = \nu_2(b_1)\)

所以 \(a_1\) 的枚举复杂度是 \(2^{k - n + 1} / 2^{\alpha}\),而每个 \(a_1\)\(x\) 的枚举复杂度为 \(q / (q / 2^{\alpha}) = 2^{\alpha}\)。相乘得到 \(2^{k - n + 1}\)

再乘上内部的 \(\mathcal O (n)\) 的计算量,这部分的复杂度为 \(\mathcal O \!\left( n 2^{k - n + 1} \right)\)

而对于 \(n\) 较小时,我们使用折半搜索的方法,有 \(\mathcal O \!\left( \sqrt{2^n} \right)\) 的复杂度。

进行复杂度平衡,我们可以考虑当 \(n \le \frac{2}{3} k\) 时用折半搜索,当 \(n > \frac{2}{3} k\) 时用枚举 \(a_1\) 的方法。

时间复杂度为 \(\mathcal O \!\left( n \sqrt[3]{q} \right)\)评测链接

2020-11-02

NF. Frightful Formula

题意简述

定义一个 \(n\)\(n\) 列的矩阵 \(F\),行数和列数从 \(1\) 编号至 \(n\),其中第 \(i\) 行第 \(j\) 列的元素记作 \(F[i, j]\)

给定数组 \([l_1, l_2, \ldots , l_n]\) 表示 \(F[i, 1] = l_i\)

给定数组 \([t_1, t_2, \ldots , t_n]\) 表示 \(F[1, i] = t_i\)

即矩阵 \(F\) 中,第 \(1\) 行和第 \(1\) 列的数均已确定,此处保证 \(l_1 = t_1\)

给定参数 \(a, b, c\),对于 \(F[i, j]\)\(2 \le i, j \le n\))有 \(F[i, j] = a \cdot F[i, j - 1] + b \cdot F[i - 1, j] + c\)

请求出 \(F[n, n] \bmod p\),其中 \(p= {10}^6 + 3\)

数据范围:\(2 \le n \le 2 \times {10}^5\)

AC 代码
#include <cstdio>

typedef long long LL;
const int Mod = 1000003;
const int MN = 200005;

inline int qPow(int b, int e) {
	int a = 1;
	for (; e; e >>= 1, b = (LL)b * b % Mod)
		if (e & 1) a = (LL)a * b % Mod;
	return a;
}

int Fac[MN * 2], iFac[MN * 2];
void Init(int N) {
	Fac[0] = 1;
	for (int i = 1; i <= N; ++i) Fac[i] = (LL)Fac[i - 1] * i % Mod;
	iFac[N] = qPow(Fac[N], Mod - 2);
	for (int i = N; i >= 1; --i) iFac[i - 1] = (LL)iFac[i] * i % Mod;
}
inline int Binom(int N, int M) {
	return (LL)Fac[N] * iFac[M] % Mod * iFac[N - M] % Mod;
}

int N, A, B, C, Ans;
int Apow[MN], Bpow[MN];

int main() {
	scanf("%d%d%d%d", &N, &A, &B, &C);
	Init(2 * N - 3);
	Apow[0] = Bpow[0] = 1;
	for (int i = 1; i < N; ++i)
		Apow[i] = (LL)Apow[i - 1] * A % Mod,
		Bpow[i] = (LL)Bpow[i - 1] * B % Mod;
	for (int i = 1, x; i <= N; ++i) {
		scanf("%d", &x);
		if (i > 1)
			Ans = (Ans + (LL)Apow[N - 1] * Bpow[N - i] % Mod * Binom(2 * N - i - 2, N - 2) % Mod * x) % Mod;
	}
	for (int i = 1, x; i <= N; ++i) {
		scanf("%d", &x);
		if (i > 1)
			Ans = (Ans + (LL)Apow[N - i] * Bpow[N - 1] % Mod * Binom(2 * N - i - 2, N - 2) % Mod * x) % Mod;
	}
	int Coef = C;
	Ans = (Ans + C) % Mod;
	for (int i = 0; i < 2 * N - 4; ++i) {
		Coef = (LL)Coef * (A + B) % Mod;
		if (i >= N - 2) {
			int X = (LL)C * (Mod - Binom(i, N - 2)) % Mod;
			Coef = (Coef + (LL)Apow[N - 1] * Bpow[i - N + 2] % Mod * X) % Mod;
			Coef = (Coef + (LL)Apow[i - N + 2] * Bpow[N - 1] % Mod * X) % Mod;
		}
		Ans = (Ans + Coef) % Mod;
	}
	printf("%d\n", Ans);
	return 0;
}

这是一个类似杨辉三角的转移形式,具有线性性,也就是可以拆分计算贡献。

所以我们知道对于 \(2 \le i \le n\),第 \(1\) 列的 \(F[i, 1] = l_i\) 对答案产生的贡献为 \(\displaystyle l_i \times \binom{2 n - i - 2}{n - 2} a^{n - 1} b^{n - i}\)

类似地,对于 \(2 \le i \le n\),第 \(1\) 行的 \(F[1, i] = t_i\) 对答案产生的贡献为 \(\displaystyle t_i \times \binom{2 n - i - 2}{n - 2} a^{n - i} b^{n - 1}\)

为什么其中的组合数不是 \(\displaystyle \binom{2 n - i - 1}{n - 1}\)?这是因为比如第 \(1\) 列的 \(F[i, 1]\) 在第一次转移时只能往右不能往下,后续转移则无限制。

接下来我们只需考虑 \(c\) 对答案产生的总贡献。例如在位置 \(F[n, n]\) 处加上的 \(c\) 产生的贡献为 \(1\)

我们很容易发现,对于 \(2 \le i, j \le n\),在位置 \(F[i, j]\) 处加上的 \(c\) 产生的贡献为 \(\displaystyle \binom{2 n - i - j}{n - i} a^{n - j} b^{n - i}\)

变换一下枚举变量,我们要求的就是 \(\displaystyle \sum_{i = 0}^{n - 2} \sum_{j = 0}^{n - 2} \binom{i + j}{i} a^i b^j\)。看起来是很简单的形式,但是实际上却没有那么简单。

把组合数拆分一下可以看出这是一个卷积的形式,可以用多项式乘法(任意模数)解决,但是我们有更优的做法。

假设我们要求的是 \(\displaystyle \sum_{k = 0}^{2 n - 4} \sum_{i = 0}^{k} \binom{k}{i} a^i b^{k - i}\)

\(j = k - i\),这与答案的区别在于,在 \((i, j)\) 的意义上,这是等腰直角三角形的形状,而答案是正方形。

然而这个新的式子却可以做到与 \(n\) 无关的时间复杂度。

这是因为对于一个 \(k\),第二层求和号的值恰好为 \({(a + b)}^k\),对所有 \(k\)\(0\)\(2 n - 4\) 进行等比数列求和,可以做到 \(\mathcal O (\log p)\)

但是同样的方法在答案的式子中并不成立?其实并非如此。

注意到当 \(k \le n - 2\) 时,新的式子中的 \((i, j)\) 值也会在答案中计算到,但是当 \(k > n - 2\) 时就并非如此了。

我们考虑按照 \(k = i + j\) 的取值逐渐递增的顺序计算答案,固定 \(k\) 值时的答案记作 \(g_k\)

则当 \(0 \le k \le n - 2\) 时,有 \(g_k = {(a + b)}^k\)。但是当 \(n - 2 \le k < 2 n - 4\) 时,我们想要通过 \(g_k\) 去计算 \(g_{k + 1}\)

考虑组合意义(格路计数),\(g_k\) 推向 \(g_{k + 1}\) 时,在最左下和最右上两侧,有两个路径跑出了正方形范围。

我们只需在 \(\mathcal O (1)\) 的时间内把多余的贡献减掉即可,以最左下角为例,即减去 \(\displaystyle \binom{k}{n - 2} a^{n - 1} b^{k - n + 2}\)

时间复杂度为 \(\mathcal O (n)\)评测链接

2020-11-03

TK. Kebab House

题意简述

\(n\) 种物品排成一排,第 \(i\) 种物品有 \(q_i\) 个。显然,总共有 \(\displaystyle \sum_{i = 1}^{n} q_i\) 个物品。

这些物品按照从左到右的顺序排列,也就是先有 \(q_1\) 个物品 \(1\),再有 \(q_2\) 个物品 \(2\),以此类推,相同的物品形成一个连续段。

这些物品是有编号的,从左到右依次编号为 \(\displaystyle 1 \sim \left( \sum_{i = 1}^{n} q_i \right)\)

然后发生了一个事件,每个物品都有可能消失,但是两个消失的物品之间的间距至少为 \(t\)
也就是不可能出现编号为 \(x, y\)\(x < y\))的两个物品都消失了,而且还满足 \(y - x \le t\)

在消失后,第 \(i\) 种物品必须至少保留下 \(x_i\) 个,如果不足 \(x_i\) 个则这种物品消失的方案就是非法的。

请你求出合法的物品消失的方案的数量,两个方案不同当且仅当存在一个物品恰好在其中一种方案中消失了。

数据范围:\(1 \le n \le 1000\)\(0 \le t \le 100\)\(1 \le q_i \le 250\)\(0 \le x_i \le q_i\)

AC 代码
#include <cstdio>
#include <algorithm>

typedef long long LL;
const int Mod = 1000000007;
const int MN = 1005, MT = 105, MW = 255;

inline void Sub(int &x, int y) { x -= y, x += x >> 31 & Mod; }
inline void Add(int &x, int y) { Sub(x, Mod - y); }
inline int qPow(int b, int e) {
	int a = 1;
	for (; e; e >>= 1, b = (LL)b * b % Mod)
		if (e & 1) a = (LL)a * b % Mod;
	return a;
}

int Fac[MW], iFac[MW], Coef[MW][MW];
inline int Binom(int N, int M) {
	if (N < 0 || M < 0 || M > N) return 0;
	return (LL)Fac[N] * iFac[M] % Mod * iFac[N - M] % Mod;
}
inline void Init(int N, int t) {
	Fac[0] = 1;
	for (int i = 1; i <= N; ++i) Fac[i] = (LL)Fac[i - 1] * i % Mod;
	iFac[N] = qPow(Fac[N], Mod - 2);
	for (int i = N; i >= 1; --i) iFac[i - 1] = (LL)iFac[i] * i % Mod;
	for (int i = 0; i <= N; ++i)
		for (int j = 0; j < N; ++j)
			Coef[i][j + 1] = (Coef[i][j] + Binom(i - j * t, j)) % Mod;
}

int N, T, q[MN], x[MN];
int _f[2][MT], *f, *g;

int main() {
	scanf("%d%d", &N, &T);
	Init(250, T);
	for (int i = 1; i <= N; ++i) scanf("%d%d", &q[i], &x[i]), x[i] = q[i] - x[i];
	f = _f[0], g = _f[1];
	f[T] = 1;
	for (int i = 1; i <= N; ++i) {
		std::swap(f, g);
		for (int j = 0; j <= T; ++j) f[j] = 0;
		for (int j = 0; j <= T; ++j)
			Add(f[std::min(j + q[i], T)], g[j]);
		for (int j = 0; j < q[i]; ++j) {
			for (int k = 0; k <= T; ++k) {
				int len = q[i] + k - j - T - 1;
				if (len < 0) continue;
				Add(f[std::min(j, T)], (LL)g[k] * Coef[len][x[i]] % Mod);
			}
		}
	}
	int Ans = 0;
	for (int j = 0; j <= T; ++j) Add(Ans, f[j]);
	printf("%d\n", Ans);
	return 0;
}

一个非常简单的 DP 计数题,因为数据范围比较小所以好多种做法都能过。

每种物品形成了一段,我们可以考虑按段转移。

\(\mathrm{f}[i][j]\) 表示考虑了前 \(i\) 段,此时最后一个消失的物品与末尾的距离为 \(j\),如果 \(j > t\) 可以看作 \(j = t\)

也就是 \(i\) 的取值范围是 \([0, n]\),而 \(j\) 的取值范围是 \([0, t]\)。把 \(j > t\) 看作 \(j = t\) 是因为更大的 \(j\) 在后续转移的效果上是相同的。

我们考虑转移,假设当前考虑 \(\mathrm{f}[i - 1][\ast]\) 转移到 \(\mathrm{f}[i][\ast]\),令 \(x'_i = q_i - x_i\),即消失的第 \(i\) 种物品数量的最大值。

如果第 \(i\) 种物品中没有消失的,直接从 \(\mathrm{f}[i - 1][j]\)\(1\) 的系数转移到 \(\mathrm{f}[i][\min \{ j + q_i, t \}]\)

否则第 \(i\) 种物品中有消失的,枚举最后一个消失的物品与末尾的距离 \(j\),此处为了方便考虑可以让 \(j > t\),所以取值范围是 \([0, q_i)\)

然后再枚举一个 \(k \in [0, t]\),从 \(\mathrm{f}[i - 1][k]\) 转移而来,以某个系数转移到 \(\mathrm{f}[i][\min \{ j, t \}]\)

关于这个系数的计算,我们考虑:除了已经钦定的最后一个消失的物品,还有最多 \(x'_i - 1\) 个物品可以消失。

而我们可以考虑每个物品占用了 \((t + 1)\) 个相邻的位置:它本身,以及它右侧的 \(t\) 个物品。

我们只需求出可用的空间的数量,也就是求出右端点和左端点即可。

右端点就是 \(q_i - j - 1\) 这个位置,而左端点是 \(t - k + 1\),它们之间的位置数量是 \(\mathrm{len} = q_i + k - j - t - 1\)

在这 \(\mathrm{len}\) 个位置中填入 \(0 \sim (x'_i - 1)\) 个占用 \((t + 1)\) 个位置的物品,方案数为:\(\displaystyle \sum_{c = 0}^{x'_i - 1} \binom{\mathrm{len} - c t}{c}\)。这就是转移系数了。

这个式子要计算很多遍,但是注意到 \(\mathrm{len}\)\(x'_i\) 分别只有 \(\mathcal O (q)\) 种取值,所有可能的结果可以在 \(\mathcal O (q^2)\) 的时间预处理。

时间复杂度为 \(\mathcal O (q^2 + n q t)\)评测链接

2020-11-04

OG. Virus synthesis

题意简述

给定一个长度为 \(n\) 的目标字符串,字符集为 \(\{ \texttt{A}, \texttt{C}, \texttt{G}, \texttt{T} \}\)

你一开始有一个空串,你可以对这个串执行:

  1. 在串的左侧或右侧添加一个字符。
  2. 把串复制一份,翻转后接在原串的左侧或右侧。

问最少执行多少次操作可以变成目标串。有多组数据。

数据范围:\(n \le {10}^5\)\(\sum n \le 3 \times {10}^7\)

AC 代码
#include <cstdio>
#include <cstring>
#include <vector>

const int MN = 100005, Sig = 4;

int N;
char S[MN];
inline int C2I(char c) {
	return c == 'A' ? 0 : c == 'C' ? 1 : c == 'G' ? 2 : 3;
}

struct PalindromeTree { // Only Even Palindrome
	char *s;
	int ch[MN][Sig], par[MN], len[MN], fail[MN], fail2[MN], cnt, now, length;
	inline void Newnode(int l) {
		len[++cnt] = l;
		memset(ch[cnt], 0, Sig * sizeof(int));
	}
	inline int getFail(int k) {
		while (k && s[length - 2 * len[k] - 1] != s[length]) k = fail[k];
		return k;
	}
	inline void Build(char *str) {
		s = str, length = 0, cnt = 0, Newnode(0);
		fail[1] = 0, now = 1;
		for (int i = 1; s[i]; ++i) {
			++length;
			now = getFail(now);
			if (!now) { now = 1; continue; }
			int j = C2I(s[i]);
			if (!ch[now][j]) {
				Newnode(len[now] + 1);
				ch[now][j] = cnt, par[cnt] = now;
				int k = getFail(fail[now]);
				if (k) {
					fail[cnt] = ch[k][j];
					int k2 = getFail(fail2[now]);
					if ((len[k2] + 1) * 2 > len[cnt]) k2 = getFail(fail[k2]);
					fail2[cnt] = ch[k2][j];
				} else fail[cnt] = fail2[cnt] = 1;
			}
			now = ch[now][j];
		}
	}
	int f[MN];
	inline int Solve() {
		int ans = 0;
		for (int i = 2; i <= cnt; ++i) {
			if (len[i] == 1) f[i] = 0;
			else f[i] = std::max(f[par[i]] + 1, f[fail2[i]] + len[i] - 1);
			ans = std::max(ans, f[i]);
		}
		return length - ans;
	}
} T;

int main() {
	int Tests;
	scanf("%d", &Tests);
	while (Tests--) {
		scanf("%s", S + 1), N = strlen(S + 1);
		T.Build(S);
		printf("%d\n", T.Solve());
	}
	return 0;
}

如果全用第一种操作,就要花费 \(n\) 的代价。每使用一次第二种操作,且操作前串长为 \(k\),就能节省 \(k - 1\) 的代价。

我们想要最大化节省的代价,也即最大化 \(\sum (k - 1)\),其中 \(k\) 为每次第二种操作前的串长。

我们注意到第二种操作后一定形成一个偶回文串,且这个偶回文串是目标串的子串。

通过回文自动机那套理论,我们知道一个长度为 \(n\) 的串的本质不同回文子串的个数是 \(\le n\) 的,偶回文串自然更少。

我们可以考虑枚举最后一次执行第二种操作后得到的偶回文串,然后考虑在所有偶回文串上做 DP。

\(\mathrm{f}[s]\) 表示从空串得到 \(s\) 能够节省的最多代价,其中 \(s\) 必须是一个目标串的偶回文子串,且最后一步必须是第二种操作。

其他的所有无关状态和转移方式是不必要去记录的,不难发现这种转移形式就已经涵盖了最优解。

我们考虑 \(\mathrm{f}[s_0]\) 能转移到 \(\mathrm{f}[s_1]\),当且仅当 \(s_0\)\(s_1\)一半的子串,也就是 \(s_0\)\(s_1\) 中的出现位置必须是不跨越中点的。
然后我们才有 \(\mathrm{f}[s_1] = \max \{ \mathrm{f}[s_0] \} + \mathrm{len}[s_1] / 2 - 1\)

这种「\(s_0\)\(s_1\)一半的子串」的关系该如何刻画呢?直接转移显然是不可取的。

我们注意到如果 \(s_0\)\(s_1\) 的左半边出现了,则在右半边也必然出现,我们只考虑在右半边出现的情况。

如果 \(s_0\)\(s_1\) 的右半边中出现的位置,右端点是与 \(s_1\) 的右端点相同的,也就是说 \(s_0\) 实际上是 \(s_1\) 的后缀。
这就可以通过跳回文自动机的 \(\mathrm{fail}\) 链进行转移,只需跳到最长的长度不超过一半的偶回文后缀即可。

如果 \(s_0\) 不是 \(s_1\) 的后缀该如何是好?注意到如果我们逐渐去掉 \(s_1\) 的左右两端的字符,总会有一个时刻 \(s_0\) 变成了 \(s_1\) 的后缀。
这时我们考虑新的 \(s_1\) 和原本的 \(s_1\) 在 DP 值上的差距,可以发现恰好差了一端去掉的字符的数量。
从另一个角度看,可以理解为从回文自动机中的双亲结点转移而来,意义是在第二种操作前先执行第一种操作,以节省代价。

也就是 \(\mathrm{f}[s] = \max \{ \mathrm{f}[\mathrm{par}[s]] + 1, \mathrm{f}[\mathrm{fail2}[s]] + \mathrm{len}[s] / 2 - 1 \}\)
其中 \(\mathrm{par}[s]\) 为回文自动机中的双亲结点,\(\mathrm{fail2}[s]\) 为长度不超过一半的最长偶回文后缀。

回文自动机在维护 \(\mathrm{fail}\) 时的时间复杂度是线性的,原因与 KMP 类似。

再维护一个 \(\mathrm{fail2}\) 会如何呢?实际上我们只需从 \(\mathrm{fail2}[\mathrm{par}[s]]\) 转移,通过势能分析同样可以得到时间复杂度是均摊 \(\mathcal O (1)\) 的。

代码中写的是只存储偶回文子串的回文自动机,且 \(\mathrm{len}\) 数组存储的是回文串长度的一半。

时间复杂度为 \(\mathcal O (n)\)评测链接

2020-11-05

BK. Tours

题意简述

给定一张 \(n\) 个点 \(m\) 条边的无向图,第 \(i\) 条边连接结点 \(a_i\)\(b_i\),图中不存在重边或自环。保证图中存在至少一个简单环。

假设有 \(k\) 种颜色,每条边都会被染其中一种颜色。还必须满足每个简单环中,每种颜色的边的数量都相同。

请你求出所有可能满足条件的 \(k\) 值。

数据范围:\(1 \le n \le 2000\)\(1 \le m \le 2000\)

AC 代码
#include <cstdio>
#include <random>
#include <chrono>
#include <algorithm>
#include <vector>
#include <unordered_map>

std::mt19937_64 rng(std::chrono::steady_clock::now().time_since_epoch().count());

int gcd(int a, int b) { return b ? gcd(b, a % b) : a; }

typedef unsigned long long ULL;
const int MN = 2005, MM = 2005;

int N, M, eu[MM], ev[MM];
std::vector<int> G[MN];

int vis[MN], par[MN], evis[MM], epar[MN], idfn[MN], dfc;
void DFS(int u) {
	vis[u] = 1;
	for (int e : G[u]) {
		int v = eu[e] ^ ev[e] ^ u;
		if (vis[v]) continue;
		par[v] = u;
		epar[v] = e;
		evis[e] = 1;
		DFS(v);
	}
	idfn[++dfc] = u;
}

ULL val[MN], eval[MM];
std::unordered_map<ULL, int> mp;
int main() {
	scanf("%d%d", &N, &M);
	for (int i = 1; i <= M; ++i)
		scanf("%d%d", &eu[i], &ev[i]),
		G[eu[i]].push_back(i),
		G[ev[i]].push_back(i);
	for (int i = 1; i <= N; ++i) if (!vis[i]) DFS(i);
	for (int i = 1; i <= M; ++i) if (!evis[i])
		eval[i] = rng(),
		val[eu[i]] ^= eval[i],
		val[ev[i]] ^= eval[i];
	for (int i = 1, u; i <= N; ++i) if (par[u = idfn[i]])
		val[par[u]] ^= eval[epar[u]] = val[u];
	for (int i = 1; i <= M; ++i) if (eval[i]) ++mp[eval[i]];
	int Ans = 0;
	for (auto p : mp) Ans = gcd(Ans, p.second);
	for (int x = 1; x <= Ans; ++x) if (Ans % x == 0) printf("%d%c", x, " \n"[x == Ans]);
	return 0;
}

在此我仅叙述算法流程,不给出所有严谨证明。
严谨证明可参见虞皓翔的 OI 中转站中的题解。在此提供一个存档链接(挂靠在博客园文件下),题解中提及的二级链接不存档。

考察所有环都不相交的情况,自然地,\(k\) 应是每个环长度的因数,也就是 \(k\) 应是所有环长度的最大公因数(\(\gcd\))的因数。
此条件是必要且充分的,因为可以对每个 \(k\) 构造解,只需在每个环上对边按顺序从颜色 \(1 \sim k\) 循环染色,不难验证满足条件。

对于两个环在某条边处相交的情况(前文的不相交也是指边不相交),我们注意到这两个环的边集的对称差(异或)形成的新导出子图(边导出子图)仍然是若干个环的不交并,故必然满足颜色数量相同的条件。
把其中涉及到的边分成三类:(1) 属于环 \(A\) 但不属于环 \(B\) 的;(2) 属于环 \(B\) 但不属于环 \(A\) 的;(3) 既属于环 \(A\) 又属于环 \(B\) 的。
将第 \(i\) 类中的某种特定颜色的边的数量记作 \(c_i\),可以根据三个条件列出关于 \(c_1, c_2, c_3\) 的满秩的线性方程组。由此得出:这三类边也都分别满足每种颜色的边的数量相同的条件。

我们可以得到:任意两个环的交、并、以及差(边集作差,对应前文中的 (1) 和 (2) 两类),也都是满足条件的。

任意三个环的交又如何呢?上述过程是无法类推的,我们只能仅凭某种 intuition,去断言任意数目的环的交/并/差都满足条件。
(只提供思路不提供证明,证明见 yhx)

此断言提供了 \(k\) 取值的必要性,即 \(k\) 需要是所有的「任意数目的环的交/并/差」的边集大小的 \(\gcd\) 的因数。
而充分性相对容易理解,因为每个环都可以表示成这些极小边集的不交并,所以对每个极小边集进行均匀分配颜色的染色即可。

接下来我们需要计算每个极小边集的大小,首先有结论:

  • 两条边同属于一个极小边集,当且仅当包含其中一条边的所有环,与包含另一条边的所有环,完全相同。

充分性:显然如果完全相同,就不可能有任何环的边集之间的运算将它们分离。
必要性:如果存在一个环包含其中一条边而不包含另一条,使用集合差运算将它们分离。

所以我们判断两条非桥边是否同属于一个极小边集,可以通过断开其中一条,查看另一条是否变成了桥边的方法判断。

在算法中我们可以随便指定一条边断开,然后求新增的桥边,这些边与断开的那条就同属于一个极小边集。

用 Tarjan 算法求桥边,这个算法的时间复杂度可以做到 \(\mathcal O (m (n + m))\)

接下来提供另一种求极小边集的方法:

考虑类似「『BZOJ3569 : DZY Loves Chinese II』(Claris 题解)」的做法,求出生成森林后,给每个非树边赋一个随机权值,而树边的权值等于所有经过它的非树边的权值的异或和。

在假设所有非树边权值线性无关(看作 \(\mathbb{F}_2\) 中的向量)的前提下,有结论:每个权值相等(且非零)的边等价类都是极小边集。

这是因为:对于每条非树边,它与它经过的所有树边组成一个环。而如果两条边(称为关键边)权值相等,就说明经过它们的非树边相同。而一个环是否经过某条边,是可以根据这个环经过的「经过了这条边的非树边」的数量的奇偶性确定的,如果为奇数就是经过了这条边。所以,既然经过这两条关键边的非树边相同,一个环如果经过了其中一条关键边——也就是经过了奇数次的那些非树边,同理也会经过另一条关键边。反之亦然。结合前文的「包含两条边的所有环相同」的结论即证。

直接实现所有非树边权值线性无关的算法是不可接受的,所以只能通过随机化去实现,但这就造成非树边权值可能线性相关。

在非树边权值可能线性相关的情况下,也许可以分析出错误率,但 yhx 的题解也没有提到。

最后,把每条非树边的权值异或到经过的树边上的这个过程,由于异或的特殊性,可以通过树上差分–前缀和在线性时间内实现。

时间复杂度为 \(\mathcal O (n + m)\)评测链接

posted @ 2020-10-12 10:26  粉兔  阅读(7336)  评论(1编辑  收藏  举报