【比赛题解】FJOI2022 题解
先开个档,有空再填。
D1T1. 区间子集问题
Description
给出 \(n\) 个区间 \([L_i, R_i]\),保证共 \(2n\) 个区间端点互不相同,且对于任意两个区间要么包含、要么无交。
对于每个区间 \([L_i, R_i]\),你都需要选出它的一个子区间 \([l_i, r_i]\),满足 \(L_i \leq l_i < r_i \leq R_i\),且任意两个子区间最多只有一个交点。你需要最大化 \(\sum(r_i - l_i)\) 的值。
数据范围:\(1 \leq n \leq 2000\),\(L_i, R_i\) 没给(FJOI 特色)。
时空限制:\(3000 \ \mathrm{ms} / 512 \ \mathrm{MiB}\)。
原题:CF 1510H。
Solution
注意到,任意两个区间要么包含、要么无交。将每个区间的儿子设为它的所有极大子区间,所有区间构成了一个森林关系。至于建树,将所有区间按照左端点排序,维护当前树的右链即可。
对于节点 \(u\),记其子树大小为 \(\mathrm{sze}_u\),则区间 \([L_u, R_u]\) 会被子树 \(u\) 内的区间划分成 \(2\mathrm{sze}_u - 1\) 个小区间。注意到,点 \(u\) 的一些祖先可能会回过头来选择子树 \(u\) 内的某个小区间,所以我们需要在点 \(u\) 的时候将代价提前计算。
考虑 dp。设 \(f(u, i, 0/1, 0/1)\) 表示:考虑了子树 \(u\) 内的小区间选取情况,有 \(i\) 个祖先选取了位于子树 \(u\) 内的小区间,尚未选取的前缀小区间是否有记在状态中(按下不表),尚未选取的后缀小区间是否有记在状态中(按下不表)。
现在要计算节点 \(u\) 的 dp 值,设节点 \(u\) 的儿子分别为 \(v_1, v_2, \cdots, v_k\),先考虑区间 \([L_{v_1}, R_{v_k}]\) 内的小区间选取情况,再特殊考虑区间 \([L_u, L_{v_1}]\) 与区间 \([R_{v_k}, R_u]\)。依次将 \(v_1, v_2, \cdots, v_k\) 的子树 dp 值合并上来,不妨设现在要合并子树 \(v_e\) 的 dp 值。有两种转移
- 某个 \(u\) 的祖先选取了区间 \([R_{v_{e - 1}}, L_{v_e}]\) 并上 \(v_{e - 1}\) 的后缀小区间与 \(v_{e}\) 的前缀小区间。
- 没有 \(u\) 的祖先在区间 \(v_{e - 1}\) 与 \(v_e\) 之间选取小区间。
区间 \([L_u, L_{v_1}]\) 与区间 \([R_{v_k}, R_u]\) 的选取与上述转移类似,交给读者。
特别地,在考虑了子树 \(u\) 内的小区间选取情况后,区间 \(u\) 也需要在子树 \(u\) 内选一个小区间,故
注意到 \(f(u, i, 0/1, 0/1)\) 中 \(i > \min(n, 2\mathrm{sze}_u - 1)\) 的状态是没有意义的。所以这就是一个树形背包,时间复杂度 \(\mathcal{O}(n^2)\)。
附上不是很有可读性的 range.cpp
#include <cstdio>
#include <cstring>
#include <algorithm>
#include <vector>
const int N = 1010;
const int inf = 0x3f3f3f3f;
void tense(int &x, const int &y) {
if (x < y) x = y;
}
int n;
struct range {
int l, r;
void input() { scanf("%d%d", &l, &r); }
bool operator < (const range &rhs) {
return l < rhs.l;
}
} a[N];
int top, stk[N];
std::vector<int> son[N];
int sze[N];
int sup(int u) { return std::min(sze[u] * 2 - 1, n); }
int f[N][N][2][2], g[N][2][2];
void dp(int u) {
if (son[u].size() == 0) {
sze[u] = 1, f[u][0][0][0] = a[u].r - a[u].l;
return;
}
int v;
v = son[u][0];
dp(v);
sze[u] = sze[v];
for (int i = 0, imax = sup(u); i <= imax; i ++)
for (int p = 0; p < 2; p ++)
for (int q = 0; q < 2; q ++)
f[u][i][p][q] = f[v][i][p][q];
for (int e = 1; e < (int)son[u].size(); e ++) {
v = son[u][e];
dp(v);
int imax = sup(u), jmax = sup(v), len = a[son[u][e]].l - a[son[u][e - 1]].r;
for (int i = 0; i <= std::min(imax + jmax + 1, n); i ++)
for (int p = 0; p < 2; p ++)
for (int q = 0; q < 2; q ++) g[i][p][q] = -inf;
for (int i = 0; i <= imax; i ++)
for (int j = 0; j <= std::min(jmax, n - i); j ++)
for (int pu = 0; pu < 2; pu ++)
for (int qu = 0; qu < 2; qu ++)
for (int pv = 0; pv < 2; pv ++)
for (int qv = 0; qv < 2; qv ++) {
int value = f[u][i][pu][qu] + f[v][j][pv][qv];
tense(g[i + j + 1][pu][qv], value + len);
if (!qu && !pv) tense(g[i + j][pu][qv], value);
}
for (int i = 0; i <= std::min(imax + jmax + 1, n); i ++)
for (int p = 0; p < 2; p ++)
for (int q = 0; q < 2; q ++) f[u][i][p][q] = g[i][p][q];
sze[u] += sze[v];
}
int imax, len;
// left
imax = sup(u), len = a[son[u].front()].l - a[u].l;
for (int i = 0; i <= imax + 1; i ++)
for (int p = 0; p < 2; p ++)
for (int q = 0; q < 2; q ++) g[i][p][q] = -inf;
for (int i = 0; i <= imax; i ++)
for (int p = 0; p < 2; p ++)
for (int q = 0; q < 2; q ++) {
int value = f[u][i][p][q];
tense(g[i][1][q], value + len), tense(g[i + 1][0][q], value + len);
if (!p) tense(g[i][0][q], value);
}
for (int i = 0; i <= imax + 1; i ++)
for (int p = 0; p < 2; p ++)
for (int q = 0; q < 2; q ++) f[u][i][p][q] = g[i][p][q];
// right
imax = sup(u) + 1, len = a[u].r - a[son[u].back()].r;
for (int i = 0; i <= imax + 1; i ++)
for (int p = 0; p < 2; p ++)
for (int q = 0; q < 2; q ++) g[i][p][q] = -inf;
for (int i = 0; i <= imax; i ++)
for (int p = 0; p < 2; p ++)
for (int q = 0; q < 2; q ++) {
int value = f[u][i][p][q];
tense(g[i][p][1], value + len), tense(g[i + 1][p][0], value + len);
if (!q) tense(g[i][p][0], value);
}
for (int i = 0; i <= imax + 1; i ++)
for (int p = 0; p < 2; p ++)
for (int q = 0; q < 2; q ++) f[u][i][p][q] = g[i][p][q];
sze[u] ++;
imax = sup(u);
for (int i = 0; i < imax; i ++)
for (int p = 0; p < 2; p ++)
for (int q = 0; q < 2; q ++) f[u][i][p][q] = f[u][i + 1][p][q];
}
int main() {
freopen("range.in", "r", stdin);
freopen("range.out", "w", stdout);
scanf("%d", &n);
a[0].l = 0, a[0].r = 1e9 + 1;
for (int i = 1; i <= n; i ++) a[i].input();
std::sort(a + 1, a + 1 + n);
stk[top = 1] = 0;
for (int i = 1; i <= n; i ++) {
while (a[stk[top]].r < a[i].r) top --;
son[stk[top]].push_back(i), stk[++ top] = i;
}
memset(f, -0x3f, sizeof(f));
int ans = 0;
for (int rt : son[0]) dp(rt), ans += f[rt][0][0][0];
printf("%d\n", ans);
return 0;
}
附上简单的 gen.cpp。
#include <cstdio>
#include <cstring>
#include <cmath>
#include <random>
#include <ctime>
#include <algorithm>
#include <vector>
typedef long long s64;
std::mt19937_64 mt_rnd((unsigned)time(0));
int R(int l, int r) { return abs(mt_rnd()) % (r - l + 1) + l; }
const int N = 1010;
int n = 1000, sz = 1e9 - 1000;
int posL, pos[N * 2];
int fa[N];
std::vector<int> son[N];
int dfsClock, Ldfn[N], Rdfn[N];
void dfs(int u) {
Ldfn[u] = ++ dfsClock;
for (int v : son[u]) dfs(v);
Rdfn[u] = ++ dfsClock;
}
int main() {
freopen("range.in", "w", stdout);
posL = n * 2 + 2;
for (int i = 1; i <= posL; i ++) pos[i] = R(1, sz);
std::sort(pos + 1, pos + 1 + posL);
for (int i = 1; i <= posL; i ++) pos[i] += i;
for (int i = 1; i <= n; i ++) fa[i] = R(0, i - 1), son[fa[i]].push_back(i);
dfs(0);
printf("%d\n", n);
for (int i = 1; i <= n; i ++) printf("%d %d\n", pos[Ldfn[i]], pos[Rdfn[i]]);
return 0;
}
D1T2. 最大平方数问题
Description
给出四个整数 \(a, b, c, n\),定义二次函数 \(f(x) = ax^2 + bx + c\),求 \(\prod_{i = 1}^n f(i)\) 的最大平方因子。对 \(998244353\) 取模。
数据范围:\(1 \leq a, n \leq 2 \times 10^5\),\(0 \leq b, c \leq 2 \times 10^5\)。
时空限制:\(3000 \ \mathrm{ms} / 512 \ \mathrm{MiB}\)。
原题:gym102896 F。
Solution
(待填 ...)
D1T3. 滚动雪人游戏问题
Description
给出一个长度为 \(n\) 的字符串,仅包含字符 A
、B
、C
。
每次操作,可以任意抽取两个不同的字符,将其替换成没出现过的那个字符后放回。直到场上只剩下一种字符后停止操作。
在保证操作次数最少的前提下,求剩下的字符是哪个,若不唯一输出 N
。
数据范围:\(1 \leq n \leq 10^4\)。
时空限制:\(1000 \ \mathrm{ms} / 512 \ \mathrm{MiB}\)。
Solution
结论
在场上至少有两种字符的情况下,一对同种字符可以用辅助字符通过两步的代价消掉。
证明
不失一般性,不妨设场上的字符为 AAB
,操作 A
、B
得到 AC
,操作 A
、C
得到 B
,此时一对同种字符就可以通过两步的代价消掉了。
可以枚举最终转化成的字符种类,计算将剩下两种字符都消去的代价。只需要在三种情况中取最小步数的即可。
不妨设剩下两种字符分别有 \(x, y(x < y)\) 个。
- 首先,对这两种字符进行 \(x\) 次操作,将公共部分均转化为最终的字符,剩下 \(y - x\) 个同种字符。
- 然后,对 \(y - x\) 的奇偶性(即 \(x, y\) 是否同奇偶性)进行讨论:
- 若 \(x, y\) 同奇偶性,则可以套用结论,用 \(y - x\) 步将剩下的 \(y - x\) 个同种字符消掉。整个过程的操作次数为 \(y\)。
- 若 \(x, y\) 异奇偶性,则可以套用结论,证明无法将所有字符转化为当前枚举的字符。
#include <cstdio>
#include <cstring>
#include <algorithm>
const int N = 10100;
const int inf = 0x3f3f3f3f;
int n;
char str[N];
int cnt[3];
char ch[3];
void work() {
scanf("%s", str + 1);
cnt[0] = cnt[1] = cnt[2] = 0;
for (int i = 1; i <= n; i ++) cnt[str[i] - 'A'] ++;
int min_step = inf;
int min_end = -1;
for (int i = 0; i <= 2; i ++) {
int p, q;
if (i == 0) p = 1, q = 2;
if (i == 1) p = 0, q = 2;
if (i == 2) p = 0, q = 1;
if (cnt[p] % 2 != cnt[q] % 2) continue;
int step = std::max(cnt[p], cnt[q]);
if (step < min_step) {
min_step = step;
min_end = i;
} else if (step == min_step) {
min_end = -1;
}
}
if (min_end == -1)
puts("N");
else
printf("%c\n", ch[min_end]);
}
int main() {
freopen("snow.in", "r", stdin);
freopen("snow.out", "w", stdout);
ch[0] = 'A', ch[1] = 'B', ch[2] = 'C';
while (scanf("%d", &n) != EOF)
work();
return 0;
}
D2T1. 重复函数问题
Description
给出一个长度为 \(n\) 的字符串 \(S\),字符串 \(S\) 仅包含小写字符。
定义重复函数 \(f_S(i)\) 表示:\(S\) 的最长子串 \(A\) 的长度,使得 \(S\) 可以被表示为 \(A \# A \# ... \# A \# A\) 的形式(\(\#\) 可以为任意串,包括空串,不要求每个 \(\#\) 都相同),且 \(A\) 在该表示中出现了 \(i\) 次。
对所有 \(2 \leq i \leq n\),求 \(f_S(i)\) 的值。
数据规模:\(1 \leq n \leq 10^6\)。
时空限制:\(1000 \ \mathrm{ms} / 512 \ \mathrm{MiB}\)。
Solution
不难想到,可以枚举每一个 border,去计算这个 border 在原串 \(S\) 中最多不重叠地出现了几次。只需要从前往后贪心考虑即可。
算法一
对原串 \(S\) 建 SAM,在 SAM 上用可持久化线段树合并维护每个状态的 \(\mathrm{endpos}\) 集合。
对于一个长度为 \(d\) 的 border,设当前该 border 在原串中匹配到的结尾位置为 \(x\)。则只需要在 \(\geq x + d\) 的位置上查找第一个在当前状态的 \(\mathrm{endpos}\) 集合里的元素即可,可以在线段树上二分。
计算一个长度为 \(d\) 的 border 在原串 \(S\) 中最多不重叠地出现了几次,需要的时间复杂度为 \(\mathcal{O}(\frac{n}{d}\log n)\)。
故时间复杂度为 \(\mathcal{O}(n \log^2 n)\)。
算法二
对原串 \(S\) 做一遍 Z Algorithm,设 \(Z_i\) 表示串 \(S\) 与串 \(s[i : n]\) 的 LCP 长度。
对于一个长度为 \(d\) 的 border,设当前该 border 在原串中匹配到的开头位置为 \(x\)。则只需要在 \(\geq x + d\) 的位置上查找第一个满足 \(Z_i \geq d\) 的元素即可,可以用并查集从小到大加点维护。
计算一个长度为 \(d\) 的 border 在原串 \(S\) 中最多不重叠地出现了几次,需要的时间复杂度为 \(\mathcal{O}(\frac{n}{d})\)。
故时间复杂度为 \(\mathcal{O}(n \log n)\)。
#include <cstdio>
#include <cstring>
#include <algorithm>
#include <vector>
using std::vector;
const int N = 1000100;
int n;
char s[N];
int Z[N];
void Z_init() {
Z[1] = 0;
for (int i = 2, l = 0, r = 0; i <= n; i ++) {
if (i <= r)
Z[i] = std::min(Z[i - l + 1], r - i + 1);
else
Z[i] = 0;
while (i + Z[i] <= n && s[1 + Z[i]] == s[i + Z[i]]) Z[i] ++;
if (i + Z[i] - 1 > r) {
l = i;
r = i + Z[i] - 1;
}
}
}
vector<int> pos[N];
int net[N];
int get_net(int x) { return net[x] == x ? x : net[x] = get_net(net[x]); }
int ans[N];
int main() {
freopen("repeat.in", "r", stdin);
freopen("repeat.out", "w", stdout);
scanf("%s", s + 1), n = strlen(s + 1);
Z_init();
for (int i = 2; i <= n; i ++) pos[Z[i]].push_back(i);
for (int i = 2; i <= n + 1; i ++) net[i] = i;
for (int d = 1; d <= n / 2; d ++) {
for (int x : pos[d - 1]) net[x] = x + 1;
if (Z[n - d + 1] == d) {
int seqL = 0;
for (int x = 1; x <= n - d + 1; x = get_net(x + d)) seqL ++;
ans[seqL] = d;
}
}
for (int i = n - 1; i >= 2; i --) ans[i] = std::max(ans[i], ans[i + 1]);
for (int i = 2; i <= n; i ++) printf("%d ", ans[i]);
return 0;
}
D2T2. 6G 网络问题
Description
给出一个正整数 \(n\),对于一个 \(n \times n\) 的网格图,每一个格点都可以选择是否涂色。求最多的涂色格点数,使得对于任意 \(4\) 个涂色点,不能组成平行于网格图边缘的矩形。
数据规模:\(1 \leq n \leq 10^9\)。
时空限制:\(1000 \ \mathrm{ms} / 512 \ \mathrm{MiB}\)。
OEIS:A072567。
Solution
Open Problem。
D2T3. 赛场监控问题
Description
首先,给出一个方向集合 \(S\),该集合为 右上、右下、左下、左上 四个方向组成的集合的非空子集之一。
给出三个正整数 \(n, r, c\),一个矩形区域被 \(r\) 条横线与 \(c\) 条竖线划分成 \((r + 1) \times (c + 1)\) 个格点。
给出 \(n\) 个监控,第 \(i\) 个监控的坐标为 \((x_i, y_i)\)。每个监控都可以在方向集合 \(S\) 中选择一个方向监视,该监控可以覆盖从监控坐标开始向监控方向扩展的一个极大矩形区域。
确定所有监控的监视方向,使得被监视到的格点数最大化。
数据规模:\(1 \leq \sum n \leq 10^5\),\(0 \leq x_i \leq r \leq 10^9\),\(0 \leq y_i \leq c \leq 10^9\)。
时空限制:\(1000 \ \mathrm{ms} / 512 \ \mathrm{MiB}\)。
Solution
(待填 ...)