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\),它们的行动是统一的,而且之后只有两种情况:
- 所有被操作的点都跑到了同一个无穷远的位置上,且这个位置在初始时肯定是没有点的。假设有 \(c\) 个初始时数值不相同的点集被操作了,则此时答案就比 \(k = 0\)(即初始状态)时减少 \((c - 1)\),因为原来的 \(c\) 个位置上的点消失了,增加了一个无穷远位置上的点。
- 每个被操作的初始时数值不相同的点集,都移动到了一个初始时就有点的位置上,且这个位置上原有的点是不会被操作的。假设有 \(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\) 的大小进行分类:
- 如果 \(d > \sqrt{2^k}\),这意味着 \(m \le \sqrt{2^k}\),我们对 \(m\) 个下标进行每次只会递归进一个子区间的二分查找。
- 如果 \(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 ∓
}
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} \}\)。
你一开始有一个空串,你可以对这个串执行:
- 在串的左侧或右侧添加一个字符。
- 把串复制一份,翻转后接在原串的左侧或右侧。
问最少执行多少次操作可以变成目标串。有多组数据。
数据范围:\(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)\),评测链接。