CF1083(Round #526 Div. 1) 简要题解
题目链接
https://codeforces.com/contest/1083
题解
A. The Fair Nut and the Best Path
可以忽略掉“任意时刻油量非负”这一条件,直接在原树上找一条点权和减去边权和最大的路径。因为若一种方案包含了一段从起点出发,到某个结点时油量为负数的路径,那么删掉这一段路径必然会使得最终答案更优。
这样直接在原树上做一遍 \(O(n)\) 的 dp 即可。
#include<bits/stdc++.h>
using namespace std;
const int N = 3e5 + 10;
void cmax(long long& x, long long y) {
if (x < y) {
x = y;
}
}
int n, w[N];
long long f[N], answer;
vector<pair<int, int>> graph[N];
void dfs(int u, int father) {
f[u] = w[u];
for (auto e : graph[u]) {
if (e.first != father) {
dfs(e.first, u);
cmax(answer, f[u] - e.second + f[e.first]);
cmax(f[u], w[u] - e.second + f[e.first]);
}
}
}
int main() {
scanf("%d", &n);
for (int i = 1; i <= n; ++i) {
scanf("%d", &w[i]);
cmax(answer, w[i]);
}
for (int i = 1; i < n; ++i) {
int u, v, c;
scanf("%d%d%d", &u, &v, &c);
graph[u].emplace_back(v, c);
graph[v].emplace_back(u, c);
}
dfs(1, 0);
printf("%lld\n", answer);
return 0;
}
B. The Fair Nut and Strings
考虑一个暴力的做法,将所有合法的字典序不小于 \(s\) 同时不大于 \(t\) 的字符串插入到一棵 trie 当中。显然,若 trie 的第 \(i\) 层共包含了 \(x_i\) 个结点,那么我们一定能取到 \(\min\{k, x_i\}\) 种不同的长度为 \(i\) 的前缀,因此答案即为 \(\sum_\limits{i = 1}^n \min\{k, x_i\}\)。\(x_i\) 可以直接递推,因此可以在 \(O(n)\) 的时间内完成答案的统计。
#include<bits/stdc++.h>
using namespace std;
const int N = 5e5 + 10;
int n, k;
char a[N], b[N];
int main() {
scanf("%d%d%s%s", &n, &k, a + 1, b + 1);
int all = 1;
long long answer = 0;
for (int i = 1; i <= n; ++i) {
all = min((long long) all << 1, (long long) INT_MAX);
if (a[i] == 'b') {
--all;
}
if (b[i] == 'a') {
--all;
}
answer += min(all, k);
}
printf("%lld\n", answer);
return 0;
}
C. Max Mex
令 \({\rm pos}_i(i \in [0, n - 1])\) 表示权值 \(i\) 所在结点的编号。考虑构建一棵线段树,线段树的每个结点 \([l, r]\) 记录 \({\rm pos}_l, {\rm pos}_{l + 1}, \cdots , {\rm pos}_r\) 这些结点在树上所在的最短的简单链的两个端点 \((u, v)\),特殊地,若这些结点无法位于同一条简单链上,那么记为 \((-1, -1)\)。
在线段树向上合并结点信息时,我们需要判断当前线段树结点的两个子结点所对应的链 \((u_1, v_1)\) 与 \((u_2, v_2)\) 是否能合并成一条新的简单链,即判断是否能从这四个结点中选出两个结点,使得另外的两个结点均在选出的两个结点间的路径上。记结点 \(u, v\) 在树上的最近公共祖先为 \(p\),那么一个结点 \(x\) 在结点 \(u, v\) 间的路径上当且仅当满足 \({\rm lca}_{u, x} = x\) 或 \({\rm lca}_{v, x} = x\),同时 \({\rm lca}_{x, anc} = p\)。使用 RMQ 求 \({\rm lca}\) 可以在 \(O(1)\) 的时间内完成判断,因此单次信息合并的时间复杂度为 \(O(1)\),但常数很大(最坏情况一次信息合并可能有 \(\binom{4}{2} \times 7 = 42\) 次 \({\rm lca}\) 的查询)。
注意若存在一个子结点的链信息为 \((-1, -1)\),那么显然当前线段树结点的链信息也应记为 \((-1, -1)\)。
每一次修改操作直接在线段树上单点修改即可。每一次询问操作即为求最大的 \(w\),使得 \({\rm pos}_0, {\rm pos}_1, \cdots ,{\rm pos}_w\) 位于同一条简单链上。可以直接在线段树上二分。单次操作的时间复杂度均为 \(O(\log n)\)。
总时间复杂度为 \(O((n + q) \log n)\)。
#include<bits/stdc++.h>
using namespace std;
const int N = 4e5 + 10;
int n, q, tt, p[N], pos[N], depth[N], firstp[N], logv[N], rmq_d[N][20], rmq_p[N][20];
vector<int> graph[N];
pair<int, int> nodes[N << 2];
void dfs(int u, int father) {
++tt;
firstp[u] = tt;
rmq_p[tt][0] = u;
rmq_d[tt][0] = depth[u];
for (auto v : graph[u]) {
if (v != father) {
depth[v] = depth[u] + 1;
dfs(v, u);
++tt;
rmq_p[tt][0] = u;
rmq_d[tt][0] = depth[u];
}
}
}
void rmq_init() {
for (int i = 2; i <= tt; ++i) {
logv[i] = logv[i >> 1] + 1;
}
for (int j = 1; (1 << j) <= tt; ++j) {
for (int i = 1; i + (1 << j) - 1 <= tt; ++i) {
int dl = rmq_d[i][j - 1];
int dr = rmq_d[i + (1 << j - 1)][j - 1];
rmq_d[i][j] = min(dl, dr);
rmq_p[i][j] = dl < dr ? rmq_p[i][j - 1] : rmq_p[i + (1 << j - 1)][j - 1];
}
}
}
int getlca(int u, int v) {
u = firstp[u];
v = firstp[v];
if (u > v) {
swap(u, v);
}
int k = logv[v - u + 1];
return rmq_d[u][k] < rmq_d[v - (1 << k) + 1][k] ? rmq_p[u][k] : rmq_p[v - (1 << k) + 1][k];
}
#define lo (o<<1)
#define ro (o<<1|1)
bool including(int u, int v, int a, int b) {
int lca = getlca(u, v);
if ((getlca(a, u) != a && getlca(a, v) != a) || getlca(a, lca) != lca) {
return false;
}
if ((getlca(b, u) != b && getlca(b, v) != b) || getlca(b, lca) != lca) {
return false;
}
return true;
}
pair<int, int> operator + (const pair<int, int>& a, const pair<int, int>& b) {
if (!~a.first || !~b.first) {
return {-1, -1};
}
if (including(a.first, a.second, b.first, b.second)) {
return {a.first, a.second};
}
if (including(a.first, b.first, a.second, b.second)) {
return {a.first, b.first};
}
if (including(a.first, b.second, a.second, b.first)) {
return {a.first, b.second};
}
if (including(a.second, b.first, a.first, b.second)) {
return {a.second, b.first};
}
if (including(a.second, b.second, a.first, b.first)) {
return {a.second, b.second};
}
if (including(b.first, b.second, a.first, a.second)) {
return {b.first, b.second};
}
return {-1, -1};
}
void build(int l, int r, int o) {
if (l == r) {
nodes[o] = {pos[l], pos[l]};
} else {
int mid = l + r >> 1;
build(l, mid, lo);
build(mid + 1, r, ro);
nodes[o] = nodes[lo] + nodes[ro];
}
}
void modify(int l, int r, int o, int p) {
if (l == r) {
nodes[o] = {pos[l], pos[l]};
} else {
int mid = l + r >> 1;
if (p <= mid) {
modify(l, mid, lo, p);
} else {
modify(mid + 1, r, ro, p);
}
nodes[o] = nodes[lo] + nodes[ro];
}
}
int query(int l, int r, int o, pair<int, int> result) {
if (l == r) {
result = result + nodes[o];
return ~result.first ? l : l - 1;
} else {
int mid = l + r >> 1;
if (!~(result + nodes[lo]).first) {
return query(l, mid, lo, result);
} else {
return query(mid + 1, r, ro, result + nodes[lo]);
}
}
}
int main() {
scanf("%d", &n);
for (int i = 1; i <= n; ++i) {
scanf("%d", &p[i]);
pos[p[i]] = i;
}
for (int i = 2; i <= n; ++i) {
int x;
scanf("%d", &x);
graph[i].push_back(x);
graph[x].push_back(i);
}
dfs(1, 0);
rmq_init();
build(0, n - 1, 1);
scanf("%d", &q);
while (q--) {
int type, i, j;
scanf("%d", &type);
if (type == 1) {
scanf("%d%d", &i, &j);
swap(p[i], p[j]);
swap(pos[p[i]], pos[p[j]]);
modify(0, n - 1, 1, p[i]);
modify(0, n - 1, 1, p[j]);
} else {
printf("%d\n", query(0, n - 1, 1, {pos[0], pos[0]}) + 1);
}
}
return 0;
}
D. The Fair Nut’s getting crazy
记 \({\rm pre}_i\) 为 \(a_i\) 在位置 \(i\) 之前最晚出现的位置(若不存在这样的位置,那么 \({\rm pre}_i = 0\)),\({\rm suf}_i\) 为 \(a_i\) 在位置 \(i\) 之后最早出现的位置(若不存在这样的位置,那么 \({\rm suf}_i = n + 1\))。
考虑枚举两个子段的交区间 \([i, j]\),记 \(l = \max_\limits{i \leq k \leq j}\{{\rm pre}_k\}, r = \min_\limits{i \leq k \leq j}\{{\rm suf}_k\}\),那么显然,右端点为 \(j\) 的子段的合法左端点在区间 \((l, i)\) 内,左端点为 \(i\) 的子段的合法右端点在区间 \((j, r)\) 内,因此两个子段的交区间为 \([i, j]\) 的合法方案数为 \((i - l - 1) \times (r - j - 1)\)。
式子 \((i - l - 1) \times (r - j - 1)\) 不好直接维护,考虑将其拆开,分别维护各项的值。我们顺次枚举所有右端点 \(j\),在这个过程中用线段树维护每个左端点对应的各项的值,那么对于每个右端点,我们只需要对合法的区间做一次区间统计即可。
总时间复杂度为 \(O(n \log n)\)。但由于我在每次移动右端点 \(j\) 时直接在原序列上二分来查找当前的 \({\rm pre}_j\) 与 \({\rm suf}_j\) 可更新的区间,因此下面代码的时间复杂度为 \(O(n \log^2 n)\)。反正过了不管了。
#include<bits/stdc++.h>
using namespace std;
const int N = 1e5 + 10, mod = 1e9 + 7;
void add(int& x, int y) {
x += y;
if (x >= mod) {
x -= mod;
}
}
void sub(int& x, int y) {
x -= y;
if (x < 0) {
x += mod;
}
}
int mul(int x, int y) {
return (long long) x * y % mod;
}
int n, a[N], pre[N], suf[N], pos[N];
struct info_t {
int maxl, minr, tagl, tagr, suml, sumr, sumlr, sumir, sumcoef;
info_t() {
tagl = -1;
tagr = -1;
}
info_t(int maxl, int minr, int suml, int sumr, int sumlr, int sumir, int sumcoef): maxl(maxl), minr(minr), suml(suml), sumr(sumr), sumlr(sumlr), sumir(sumir), sumcoef(sumcoef) {}
info_t operator + (info_t a) {
info_t result;
result.maxl = max(maxl, a.maxl);
result.minr = min(minr, a.minr);
result.suml = (suml + a.suml) % mod;
result.sumr = (sumr + a.sumr) % mod;
result.sumlr = (sumlr + a.sumlr) % mod;
result.sumir = (sumir + a.sumir) % mod;
result.sumcoef = (sumcoef + a.sumcoef) % mod;
return result;
}
} info[N << 2];
#define lo (o<<1)
#define ro (o<<1|1)
int samediff_sum(int l, int r) {
return ((long long) (l + r) * (r - l + 1) >> 1) % mod;
}
void cover_l(int l, int r, int o, int prep) {
info[o].tagl = prep;
info[o].maxl = prep;
info[o].suml = mul(r - l + 1, prep);
info[o].sumlr = mul(info[o].sumr, prep);
info[o].sumcoef = (info[o].suml - samediff_sum(l - 1, r - 1) + mod) % mod;
}
void cover_r(int l, int r, int o, int sufp) {
info[o].tagr = sufp;
info[o].minr = sufp;
info[o].sumr = mul(r - l + 1, sufp);
info[o].sumlr = mul(info[o].suml, sufp);
info[o].sumir = mul(sufp, samediff_sum(l, r));
}
void push_down(int l, int r, int o) {
int mid = l + r >> 1;
if (~info[o].tagl) {
cover_l(l, mid, lo, info[o].tagl);
cover_l(mid + 1, r, ro, info[o].tagl);
info[o].tagl = -1;
}
if (~info[o].tagr) {
cover_r(l, mid, lo, info[o].tagr);
cover_r(mid + 1, r, ro, info[o].tagr);
info[o].tagr = -1;
}
}
void modify(int l, int r, int o, int p, int prep, int sufp) {
if (l == r) {
info[o] = info_t(prep, sufp, prep, sufp, mul(prep, sufp), mul(l, sufp), (prep - l + 1 + mod) % mod);
} else {
int mid = l + r >> 1;
push_down(l, r, o);
if (p <= mid) {
modify(l, mid, lo, p, prep, sufp);
} else {
modify(mid + 1, r, ro, p, prep, sufp);
}
info[o] = info[lo] + info[ro];
}
}
void modify_max(int l, int r, int o, int ql, int qr, int prep) {
if (ql <= l && r <= qr) {
cover_l(l, r, o, prep);
} else {
int mid = l + r >> 1;
push_down(l, r, o);
if (ql <= mid) {
modify_max(l, mid, lo, ql, qr, prep);
} if (qr > mid) {
modify_max(mid + 1, r, ro, ql, qr, prep);
}
info[o] = info[lo] + info[ro];
}
}
void modify_min(int l, int r, int o, int ql, int qr, int sufp) {
if (ql <= l && r <= qr) {
cover_r(l, r, o, sufp);
} else {
int mid = l + r >> 1;
push_down(l, r, o);
if (ql <= mid) {
modify_min(l, mid, lo, ql, qr, sufp);
} if (qr > mid) {
modify_min(mid + 1, r, ro, ql, qr, sufp);
}
info[o] = info[lo] + info[ro];
}
}
info_t query(int l, int r, int o, int ql, int qr) {
if (ql <= l && r <= qr) {
return info[o];
} else {
int mid = l + r >> 1;
push_down(l, r, o);
if (qr <= mid) {
return query(l, mid, lo, ql, qr);
} else if (ql > mid) {
return query(mid + 1, r, ro, ql, qr);
} else {
return query(l, mid, lo, ql, qr) + query(mid + 1, r, ro, ql, qr);
}
}
}
int main() {
scanf("%d", &n);
vector<int> h;
for (int i = 1; i <= n; ++i) {
scanf("%d", &a[i]);
h.push_back(a[i]);
}
sort(h.begin(), h.end());
h.erase(unique(h.begin(), h.end()), h.end());
for (int i = 1; i <= n; ++i) {
a[i] = lower_bound(h.begin(), h.end(), a[i]) - h.begin();
}
for (int i = 1; i <= n; ++i) {
pre[i] = pos[a[i]] ? pos[a[i]] : 0;
pos[a[i]] = i;
}
memset(pos, 0, sizeof pos);
for (int i = n; i; --i) {
suf[i] = pos[a[i]] ? pos[a[i]] : n + 1;
pos[a[i]] = i;
}
int answer = 0;
for (int i = 1, j = 1; i <= n; ++i) {
for (; j <= pre[i]; ++j);
int l = j, r = i;
while (l != r) {
int mid = l + r >> 1;
if (query(1, n, 1, mid, i - 1).maxl < pre[i]) {
r = mid;
} else {
l = mid + 1;
}
}
if (l <= i - 1) {
modify_max(1, n, 1, l, i - 1, pre[i]);
}
l = j, r = i;
while (l != r) {
int mid = l + r >> 1;
if (query(1, n, 1, mid, i - 1).minr > suf[i]) {
r = mid;
} else {
l = mid + 1;
}
}
if (l <= i - 1) {
modify_min(1, n, 1, l, i - 1, suf[i]);
}
modify(1, n, 1, i, pre[i], suf[i]);
info_t result = query(1, n, 1, j, i);
add(answer, result.sumir);
sub(answer, result.sumlr);
add(answer, mul(i + 1, result.sumcoef));
sub(answer, result.sumr);
}
printf("%d\n", answer);
return 0;
}
E. The Fair Nut and Rectangles
斜率优化裸题。
将所有矩形按 \(x\) 排序之后,定义 \(f_i\) 表示最后一个选择的矩形为 \(i\) 的最大收益。转移即为 \(f_i = \max_\limits{j < i} \{f_j + y_i \times (x_i - x_j)\} - a_i\),即 \(f_i = \max_\limits{j < i}\{f_j - y_ix_j\} + x_iy_i - a_i\),将 \(y_i\) 看做斜率,\((x_i, f_i)\) 视为转移点,那么维护所有转移点构成的上凸包即可。由于斜率是单调下降的,因此可以直接用单调队列维护。时间复杂度为 \(O(n)\)。
这绝对是这套题里面最简单的一道题。
#include<bits/stdc++.h>
using namespace std;
const int N = 1e6 + 10;
struct matrix {
int x, y;
long long v;
} a[N];
int n, convex_hull[N];
long long f[N];
double slope(int i, int j) {
return (double) (f[i] - f[j]) / (a[i].x - a[j].x);
}
int main() {
scanf("%d", &n);
for (int i = 1; i <= n; ++i) {
scanf("%d%d%lld", &a[i].x, &a[i].y, &a[i].v);
}
sort(a + 1, a + 1 + n, [&] (const matrix& a, const matrix& b) {
return a.x < b.x;
});
int ql = 0, qr = 0;
long long answer = 0;
for (int i = 1; i <= n; ++i) {
for (; ql < qr && slope(convex_hull[ql], convex_hull[ql + 1]) >= a[i].y; ++ql);
int j = convex_hull[ql];
f[i] = f[j] + (long long) a[i].y * (a[i].x - a[j].x) - a[i].v;
answer = max(answer, f[i]);
for (; ql < qr && slope(convex_hull[qr], i) >= slope(convex_hull[qr - 1], i); --qr);
convex_hull[++qr] = i;
}
printf("%lld\n", answer);
return 0;
}
F. The Fair Nut and Amusing Xor
和 LOJ6500. 「雅礼集训 2018 Day2」操作 的主要思路类似的一个题。
考虑先将序列 \(a, b\) 差分,令 \(a'_i = a_i\ {\rm xor}\ a_{i - 1}, b'_i = b_i\ {\rm xor}\ b_{i - 1}\),新建序列 \(c\),满足 \(c_i = a'_i\ {\rm xor}\ b'_i\),那么每一次操作会使得序列 \(c\) 中两个下标差为 \(k\) 的元素异或上一个相同的值,我们的目的是利用最小的操作次数使得序列 \(c\) 的所有元素变为 \(0\)。
由于模 \(k\) 后值不相同的位置不会互相影响,因此我们将序列 \(c\) 的所有元素按照下标模 \(k\) 的值分组,下标模 \(k\) 的值相同的元素位于同一组。对于每一组,我们考虑从左至右依次将每一个不为 \(0\) 的元素消掉的过程,不难发现,该组的所有元素最终能为 \(0\),当且仅当所有元素的异或和为 \(0\)。同时,在保证能够使该组的所有元素最终为 \(0\) 的前提下,如果该组内有 \(x\) 个元素,那么我们需要的最小操作次数为 \(x - w\),其中 \(w\) 为该组内元素的前缀异或和为 \(0\) 的前缀数量,因为显然每存在一个前缀异或和为 \(0\) 的前缀,我们都可以省掉一步操作。
由于对于每次修改操作,最多只会修改序列 \(c\) 中的两个元素。在修改序列 \(c\) 中的一个元素后,我们需要重新维护对应组内的信息。因此,我们需要解决的根本问题是在修改完单个元素后快速求出对应组内有多少个前缀异或和为 \(0\) 的前缀。当 \(k > \sqrt n\) 时,每一组的元素较少,我们可以直接暴力维护;当 \(k \leq \sqrt n\) 时,我们可以对每组分块,由于序列的值域较小,因此我们可以直接开数组记录每个块内前缀异或和为某个值的前缀数量,修改时直接在块内暴力修改,之后再顺次扫一遍所有块更新答案即可。
时间复杂度为 \(O((n + q) \sqrt n)\)。
#include<bits/stdc++.h>
using namespace std;
const int N = 2e5 + 10, block = 400;
int n, k, q, a[N], b[N], c[N], endv[N], result[N], id[N], pos[N], num[N], total, answer;
vector<vector<int>> team[500];
vector<int> team_v[500];
void init(bool common) {
if (common) {
for (int i = 0; i < k; ++i) {
int value = 0;
for (int j = !i ? k : i; j <= n; j += k) {
value ^= c[j];
if (value) {
++result[i];
}
if (j + k > n) {
endv[i] = !value;
}
}
answer += result[i];
total += endv[i];
}
} else {
for (int i = 0; i < n; ++i) {
id[i] = i / block;
}
for (int i = 0; i < k; ++i) {
int t = 0;
for (int j = !i ? k : i; j <= n; j += k) {
pos[j] = t++;
}
num[i] = t;
vector<int> empty_vec(1 << 14, 0);
for (int j = 0; j <= id[t - 1]; ++j) {
team[i].push_back(empty_vec);
}
team_v[i].resize(team[i].size());
int value = 0;
for (int j = !i ? k : i; j <= n; j += k) {
value ^= c[j];
++team[i][id[pos[j]]][value];
if (j + k > n || id[pos[j]] != id[pos[j + k]]) {
team_v[i][id[pos[j]]] = value;
value = 0;
}
}
result[i] = num[i];
for (int j = 0; j < team[i].size(); ++j) {
result[i] -= team[i][j][value];
value ^= team_v[i][j];
}
endv[i] = !value;
answer += result[i];
total += endv[i];
}
}
}
void modify(bool common, int i, int p) {
total -= endv[i];
answer -= result[i];
if (common) {
result[i] = 0;
int value = 0;
for (int j = !i ? k : i; j <= n; j += k) {
value ^= c[j];
if (value) {
++result[i];
}
if (j + k > n) {
endv[i] = !value;
}
}
} else {
result[i] = num[i];
int ind = id[pos[p]];
fill(team[i][ind].begin(), team[i][ind].end(), 0);
int l = p, r = p;
for (; l > 0 && id[pos[l]] == ind; l -= k);
for (; r <= n && id[pos[r]] == ind; r += k);
l += k;
r -= k;
int value = 0;
for (int j = l; j <= r; j += k) {
value ^= c[j];
++team[i][ind][value];
}
team_v[i][ind] = value;
value = 0;
for (int j = 0; j < team[i].size(); ++j) {
result[i] -= team[i][j][value];
value ^= team_v[i][j];
}
endv[i] = !value;
}
answer += result[i];
total += endv[i];
}
int main() {
scanf("%d%d%d", &n, &k, &q);
for (int i = 1; i <= n; ++i) {
scanf("%d", &a[i]);
}
for (int i = 1; i <= n; ++i) {
scanf("%d", &b[i]);
}
++n;
for (int i = 1; i <= n; ++i) {
c[i] = a[i] ^ a[i - 1] ^ b[i] ^ b[i - 1];
}
init(k > block);
printf("%d\n", total == k ? answer : -1);
while (q--) {
char type[2];
int p, v;
scanf("%s%d%d", type, &p, &v);
if (*type == 'a') {
a[p] = v;
} else {
b[p] = v;
}
c[p] = a[p] ^ a[p - 1] ^ b[p] ^ b[p - 1];
++p;
c[p] = a[p] ^ a[p - 1] ^ b[p] ^ b[p - 1];
--p;
modify(k > block, p % k, p);
++p;
modify(k > block, p % k, p);
printf("%d\n", total == k ? answer : -1);
}
return 0;
}