时间安排与比赛成绩
7:45 开题
7:50 - 8:00 看完T1,感觉不难。肚子疼,去上厕所
8:00 - 8:20 在厕所里想到了T1做法
8:25 - 8:50 写完了T1,一遍过样例。没有大样例,写了对拍,过拍了。
8:50 - 9:00 把剩下三道题面看了,感觉T3没什么思路,T4没太看懂题,T2感觉思路不难,写起来比较恶心。就先开T2了
9:00 - 10:30 把T2思路捋清后写了,写的很小心。写完后发现没过大样例,发现一个小细节出错了,改了之后就过了大样例。不太好拍就没拍
10:30 - 11:00 仔细阅读T4,发现实际上暴力很简单,果断写了50分暴力润了
11:00 - 11:10 还有一个小时写T3,发现有20分的特殊性质是很简单的,还有30分较小的数据规模仔细思考也可以完成。先写暴力。
11:10 - 11:40 暴力写完了,没过样例,有点慌
11:40 - 11:55 还没调出来!发现其他题还没交,先把其他题和特殊性质交了
11:55 - 11:58 调出来了!还剩1min交上了。
分数:
100 + 100 + 50 + 50 = 300
rk 3
没挂分的一场
题解
A. 荒野求生
分析:
实际上就是求区间
[
l
,
r
]
[l, r]
[l,r] 内大于数
x
x
x 的连续段数。
在线不好处理,考虑离线。
将询问按照
x
x
x 从大到小排序,那么每次就是加入一些位置。写一个并查集维护连通块的左右端点。再写两个树状数组分别维护前缀的左端点/右端点数量即可。
时间复杂度 O ( ( q + n ) × l o g 2 n ) O((q + n) \times log_2n) O((q+n)×log2n)
CODE:
#include<bits/stdc++.h>
using namespace std;
const int N = 2e5 + 10;
int n, m, h[N], idx[N], bin[N], L[N], R[N], ans[N];
struct Q {
int l, r, x, id;
}q[N];
struct BIT {
int c[N];
int lowbit(int x) {return x & -x;}
void add(int x, int y) {for(; x < N; x += lowbit(x)) c[x] += y;}
int ask(int x) {
int res = 0;
for(; x; x -= lowbit(x)) res += c[x];
return res;
}
}tl, tr;
bool cmp(int x, int y) {
return h[x] > h[y];
}
bool cmpq(Q a, Q b) {
return a.x > b.x;
}
int Find(int x) {
return (x == bin[x] ? x : bin[x] = Find(bin[x]));
}
void ins(int pos) {
int nl = pos, nr = pos, o = Find(pos);
if(pos != 1) { // 往左边合
int f1 = Find(pos - 1);
if(L[f1] != 0) { // 存在
int l = L[f1], r = R[f1];
tl.add(l, -1);
tr.add(r, -1);
nl = L[f1];
bin[f1] = o;
}
}
if(pos != n) { // 往右边合
int f2 = Find(pos + 1);
if(R[f2] != 0) { // 存在
int l = L[f2], r = R[f2];
tl.add(l, -1);
tr.add(r, -1);
nr = R[f2];
bin[f2] = o;
}
}
L[o] = nl, R[o] = nr;
tl.add(nl, 1); tr.add(nr, 1);
}
int main() {
scanf("%d%d", &n, &m);
for(int i = 1; i <= n; i ++ ) bin[i] = i;
for(int i = 1; i <= n; i ++ ) scanf("%d", &h[i]);
for(int i = 1; i <= n; i ++ ) idx[i] = i;
sort(idx + 1, idx + n + 1, cmp);
for(int i = 1; i <= m; i ++ ) {
scanf("%d%d%d", &q[i].l, &q[i].r, &q[i].x);
q[i].id = i;
}
sort(q + 1, q + m + 1, cmpq);
int j = 1;
for(int i = 1; i <= m; i ++ ) {
while(j <= n && h[idx[j]] > q[i].x) {
ins(idx[j]);
j ++;
}
ans[q[i].id] = tl.ask(q[i].r) - tr.ask(q[i].l - 1);
}
for(int i = 1; i <= m; i ++ ) {
printf("%d\n", ans[i]);
}
return 0;
}
B. 加法器
分析:
模拟题。考虑使用 线段树 维护。
如果原来的
a
a
a,
b
b
b,
c
c
c 数组是已知的,那么修改后这一位上的数字是好算的。可通过原来的
a
a
a,
b
b
b,
c
c
c 判断出当前位的上一位是否进位,然后再计算出修改后的值。
注意到修改当前位会对高位有影响,原因是因为 进位。可分成四种情况:对当前位 原来进位,现在不进位,原来进位,现在进位,原来不进位,现在进位,原来不进位,现在不进位。对于第二种和第四种情况,显然对高位是没有影响的。对于第一种和第三种情况,那么造成的影响是 一段连续的
9
9
9 无法进位 和 一段连续的
9
9
9 都进位。因此我们要快速找到 从某个位置
p
p
p 开始,向高位延伸,第一个
a
i
+
b
i
a_i + b_i
ai+bi 不为
9
9
9 的位置。还需要支持区间赋值操作。这可以在线段树上二分和区间赋值。
代码有点粪。
CODE:
#include<bits/stdc++.h>
using namespace std;
typedef pair< int, int > PII;
const int N = 1e6 + 10;
int n, q;
char num1[N], num2[N], num3[N];
int num[3][N];
int r, p, d;
struct SegmentTree {
int l, r, cnt; // cnt 代表区间里 a + b = 9 的个数
int c, tag; // c 代表区间里的数字, 查询要用
#define l(x) t[x].l
#define r(x) t[x].r
#define cnt(x) t[x].cnt
#define c(x) t[x].c
#define tag(x) t[x].tag
}t[N * 4];
void update(int p) {
cnt(p) = cnt(p << 1) + cnt(p << 1 | 1);
}
void spread(int p) {// 查询数字时用
if(tag(p) != -1) {
c(p << 1) = c(p << 1 | 1) = tag(p);
tag(p << 1) = tag(p << 1 | 1) = tag(p);
tag(p) = -1;
}
}
void build(int p, int l, int r) {
l(p) = l, r(p) = r;
tag(p) = -1;
if(l == r) {
return ;
}
int mid = (l + r >> 1);
build(p << 1, l, mid);
build(p << 1 | 1, mid + 1, r);
}
void change(int p, int l, int r, int c) { // 区间 [l, r] 赋值成 c
if(l <= l(p) && r >= r(p)) {
c(p) = c; tag(p) = c;
return ;
}
spread(p);
int mid = (l(p) + r(p) >> 1);
if(l <= mid) change(p << 1, l, r, c);
if(r > mid) change(p << 1 | 1, l, r, c);
}
void ins(int p, int pos, int c) {
if(l(p) == r(p)) {
cnt(p) += c;
return ;
}
int mid = (l(p) + r(p) >> 1);
if(pos <= mid) ins(p << 1, pos, c);
else ins(p << 1 | 1, pos, c);
update(p);
}
int query_number(int p, int pos) {
if(l(p) == r(p)) return c(p);
spread(p);
int mid = (l(p) + r(p) >> 1);
if(pos <= mid) return query_number(p << 1, pos);
else return query_number(p << 1 | 1, pos);
}
int Q(int p) {
if(l(p) == r(p)) return l(p);
int mid = (l(p) + r(p) >> 1);
if(cnt(p << 1 | 1) != (r(p << 1 | 1) - l(p << 1 | 1) + 1)) return Q(p << 1 | 1); // 先找右端点
else return Q(p << 1);
}
int query_pos(int p, int l, int r) {
if(l <= l(p) && r >= r(p)) {
if(cnt(p) == (r(p) - l(p) + 1)) return 0; // 全都是
else return Q(p);
}
int mid = (l(p) + r(p) >> 1);
if(r <= mid) return query_pos(p << 1, l, r);
else if(l > mid) return query_pos(p << 1 | 1, l, r);
else return max(query_pos(p << 1, l, r), query_pos(p << 1 | 1, l, r));
}
void Get() {
int c = 0, d = 0;
for(int i = n; i >= 1; i -- ) {
num[1][i] = (int)(num1[i] - '0');
num[2][i] = (int)(num2[i] - '0');
c = num[1][i] + num[2][i] + d;
int tmp;
tmp = (c % 10);
if(c >= 10) d = 1;
else d = 0;
if(num[1][i] + num[2][i] == 9) ins(1, i, 1);
change(1, i, i, tmp);
}
}
PII solve(int r, int p, int d) { // r行,第 p 位置, 改成 d
if(num[r][p] == d) return make_pair(query_number(1, p), 0); // 没有改变
else {
int res1 = 0, res2 = 1; // 初始时就有一个不同
bool f1 = 0, f2 = 0; // f1 表示原来是否能进位, f2 表示现在是否能进位
int t = (num[1][p] + num[2][p]) % 10; // 应该的数字
int g = query_number(1, p); // 实际的数字
int td = 0;
if(t != g) td = 1; // 进位了
if(num[1][p] + num[2][p] + td > 9) f1 = 1; // 原来能进位
int u = (num[3 - r][p] + d + td) % 10; // 改后的数字
if(num[3 - r][p] + d + td > 9) f2 = 1; // 现在能进位
if(u != g) res2 ++; // 这一位不同
res1 = u;
change(1, p, p, u); //改实际数字
if((num[1][p] + num[2][p] == 9) && (num[3 - r][p] + d != 9)) ins(1, p, -1);
else if((num[1][p] + num[2][p] != 9) && (num[3 - r][p] + d == 9)) ins(1, p, 1);
num[r][p] = d; // 把第 p 位改完
if(f1 && (!f2)) { // 原来能进位,现在不能进位
int tp = (p == 1 ? 0 : query_pos(1, 1, p - 1)); // 找到左边第一个不是 9 的位置
if(tp + 1 <= p - 1) {// c全部变成9
change(1, tp + 1, p - 1, 9);
res2 += ((p - 1) - (tp + 1) + 1);
}
if(tp != 0) { // tp 这个位置的c也要改变
int r = (num[1][tp] + num[2][tp]) % 10; // 原来应该是 r + 1,现在是r
change(1, tp, tp, r);
res2 ++;
}
}
else if((!f1) && f2) { // 原来不能进位,现在能进位
int tp = (p == 1 ? 0 : query_pos(1, 1, p - 1)); // 找到左边第一个不是 9 的位置
if(tp + 1 <= p - 1) { // c全部改成0
change(1, tp + 1, p - 1, 0);
res2 += ((p - 1) - (tp + 1) + 1);
}
if(tp != 0) {
int r = (num[1][tp] + num[2][tp]) % 10; // 原来的c是 r, 现在是 r + 1
change(1, tp, tp, r + 1);
res2 ++;
}
}
return make_pair(res1, res2);
}
}
int main() {
scanf("%d%d", &n, &q);
scanf("%s", num1 + 1);
scanf("%s", num2 + 1);
build(1, 1, n);
Get(); // 将 num3 处理出来 同时插入9
for(int i = 1; i <= q; i ++ ) {
scanf("%d%d%d", &r, &p, &d);
PII res = solve(r, p, d);
printf("%d %d\n", res.first, res.second);
}
return 0;
}
C. 树上染色
分析:
考场上没想到正解。
从暴力dp 开始思考。容易想到 按深度将点分类,每一层计算染色的最小费用。设考虑第
d
d
d 层,那么深度大于
d
d
d 的点显然不能用来染色,如果把它们删掉,那么这一层就成了叶子。所以设状态
f
i
f_i
fi 表示将
i
i
i 节点的子树中的叶子全部染色的最小花费。有转移:
f i = m i n { ∑ f s o n , a d − d e p i } f_i = min \left \{ \sum f_{son}, a_{d - dep_i}\right \} fi=min{∑fson,ad−depi}
这个转移是显然的,对于 i i i 子树内的叶子,要么在儿子中染完,要么在 i i i 号点用一次操作完成。这样时间复杂度是 O ( n 2 ) O(n^2) O(n2) 的。
考虑如果每次 d p dp dp 的复杂度都和 叶子的数量 成线性相关,那么复杂度就是对的。如果我们每次把叶子看作 关键点 的话,那么关键点的总数为 n n n。这看起来与 虚树 有关。
那么我们对每一层建出虚树,这相当于 对原来的树除有用点外进行了压缩。设 f a i fa_i fai 表示 i i i 号点在虚树上的父亲,转移有:
f i = m i n { ∑ f s o n , m i n j = d − d e p i d − d e p f a i − 1 a j } f_i = min \left \{ \sum f_{son},min_{j =d - dep_i}^{d - dep_{fa_i} -1} a_j\right \} fi=min{∑fson,minj=d−depid−depfai−1aj}
右边的式子可以用 s t st st 表处理。
时间复杂度 O ( n × l o g 2 n ) O(n \times log_2n) O(n×log2n)
CODE:
#include<bits/stdc++.h> // 每层分别求出答案。如果每层的复杂度和叶子数量线性相关,那么复杂度是对的。考虑建出虚树,然后dp
#define pb push_back
using namespace std; // 时间复杂度 O(nlogn)
typedef long long LL;
const LL INF = 0x3f3f3f3f3f3f3f3f;
const int N = 1e5 + 10;
int T, n, u, v, dep[N], dfn[N], rk, fat[N][21];
int Node[N * 2], len;
LL a[N], Min[N][21], dp[N];
vector< int > node[N];
struct edge {
int head[N], tot;
struct EDGE {
int v, last;
}E[N * 2];
void add(int u, int v) {
E[++ tot].v = v;
E[tot].last = head[u];
head[u] = tot;
}
}R, V;
void build_ST() {
for(int i = 0; i < n; i ++ ) Min[i][0] = a[i];
for(int i = 1; (1 << i) - 1 < n; i ++ )
for(int j = 0; j + (1 << i) - 1 < n; j ++ )
Min[j][i] = min(Min[j][i - 1], Min[j + (1 << (i - 1))][i - 1]);
}
int query(int l, int r) {
int k = log2(r - l + 1);
return min(Min[l][k], Min[r - (1 << k) + 1][k]);
}
void dfs(int x, int fa) {
dep[x] = dep[fa] + 1; dfn[x] = ++ rk;
fat[x][0] = fa; for(int i = 1; i <= 20; i ++ ) fat[x][i] = fat[fat[x][i - 1]][i - 1];
for(int i = R.head[x]; i; i = R.E[i].last) {
int v = R.E[i].v;
if(v == fa) continue;
dfs(v, x);
}
}
int Lca(int x, int y) {
if(dep[x] < dep[y]) swap(x, y);
for(int i = 20; i >= 0; i -- ) {
if(dep[fat[x][i]] >= dep[y]) x = fat[x][i];
}
if(x == y) return x;
for(int i = 20; i >= 0; i -- ) {
if(fat[x][i] != fat[y][i]) x = fat[x][i], y = fat[y][i];
}
return fat[x][0];
}
bool cmp(int x, int y) {
return dfn[x] < dfn[y];
}
void build_virtual_tree(int d) {
len = 0;
for(auto x : node[d]) Node[++ len] = x;
Node[++ len] = 1;
sort(Node + 1, Node + len + 1, cmp);
int o = len;
for(int i = 1; i < o; i ++ ) {
int lc = Lca(Node[i], Node[i + 1]);
Node[++ len] = lc;
}
sort(Node + 1, Node + len + 1, cmp);
len = unique(Node + 1, Node + len + 1) - (Node + 1);
for(int i = 1; i < len; i ++ ) {
int lc = Lca(Node[i], Node[i + 1]);
V.add(lc, Node[i + 1]); V.add(Node[i + 1], lc);
}
}
void DP(int x, int fa, int d) { // 本层深度
LL sum = 0; bool flag = 1;
for(int i = V.head[x]; i; i = V.E[i].last) {
int v = V.E[i].v;
if(v == fa) continue;
DP(v, x, d);
sum += dp[v];
flag = 0;
}
LL c = query(d - dep[x], x == 1 ? d - dep[x] : d - dep[fa] - 1);
if(!flag) c = min(c, sum);
dp[x] = c;
}
void Clear() {
V.tot = 0;
for(int i = 1; i <= len; i ++ ) V.head[Node[i]] = 0;
for(int i = 1; i <= len; i ++ ) dp[Node[i]] = INF;
}
void solve() {
scanf("%d", &n);
R.tot = 0; rk = 0;
for(int i = 1; i <= n; i ++ ) R.head[i] = 0;
for(int i = 0; i < n; i ++ ) scanf("%lld", &a[i]);
memset(dp, 0x3f, sizeof dp);
build_ST();
for(int i = 1; i < n; i ++ ) {
scanf("%d%d", &u, &v);
R.add(u, v); R.add(v, u);
}
dfs(1, 0);
for(int i = 1; i <= n; i ++ ) {
vector< int > tmp; swap(tmp, node[i]);
}
int maxdeep = 0;
for(int i = 1; i <= n; i ++ ) {
maxdeep = max(maxdeep, dep[i]);
node[dep[i]].pb(i);
}
LL res = 0;
for(int i = 1; i <= maxdeep; i ++ ) {
build_virtual_tree(i);
DP(1, 0, i);
res += dp[1];
Clear();
}
printf("%lld\n", res);
}
int main() {
scanf("%d", &T);
while(T -- ) {
solve();
}
return 0;
}
D. 挤电梯
分析:
数据结构好题。
首先不难想到最优的上电梯顺序一定是小在前,大的在后。
那么有一个很重要的性质是 对于一个数,如果它后面有一个比它更小的数,那么它出队时不会让别的数出队再入队。
基于这个性质,那么会导致别的元素出队再进队的数就是 后缀最小值。并且它们额外的贡献就是 前面比它大的数。考虑到它是后缀最小值,因此后面的数都比它大。所以它额外的贡献就是 比它大的数的个数
−
-
− 它后面的数的个数。
最后的答案就是 所有后缀最小值额外的贡献
+
+
+
n
n
n。
但是还有删除某一个元素的操作。发现删除操作不好维护后缀最小值,我们 离线后变成添加操作。那么每次添加的结果就是 它是后缀最小值并删掉一些原来的后缀最小值 或者 它不是后缀最小值,对原来的后缀最小值没有影响。
分情况讨论添加元素的贡献:
- 它是后缀最小值。那么一些元素不再是后缀最小值。删去这些元素的贡献后加上添加元素的贡献。
- 它不是后缀最小值。那么别的元素对它没有贡献,但是它对后面的比它小的后缀最小值有贡献。这是一个 二维偏序,但是发现 它前面的后缀最小值都比它小,因此贡献就是它后面的后缀最小值个数减去比它的后缀最小值个数。
上述操作可用 树状数组和 s e t set set 完成。
时间复杂度: O ( T × n l o g 2 n ) O(T \times nlog_2n) O(T×nlog2n)
CODE:
#include<bits/stdc++.h> // 数据结构好题
using namespace std; // 离线:删除操作变为添加操作
typedef long long LL;
const int N = 1e6 + 10;
int a[N], n, qc, p[N];
struct Q {
int x, id;
}q[N];
bool cmp(Q a, Q b) {return a.id > b.id;}
bool vis[N];
LL ans[N], res, S;
struct BIT {
int c[N];
int lowbit(int x) {return x & -x;}
void add(int x, int y) {for(; x < N; x += lowbit(x)) c[x] += y;}
int ask(int x) {
int res = 0;
for(; x; x -= lowbit(x)) res += c[x];
return res;
}
}T1, T2, T3, T4; // T1以位置为下标,T2以层数为下标 T3是位置, T4是值
set< int > s; // 用来存后缀最小值编号
LL query(int pos) { // p位置是后缀最小值,查p位置的贡献
return 1LL * ((T2.ask(n) - T2.ask(p[pos])) - (T1.ask(n) - T1.ask(pos)));
}
void ins(int x) { // 将编号为x的人插入
auto it = s.lower_bound(x);
bool f = 0;
if(it == s.end() || (p[(*it)] > p[x])) f = 1; // 是后缀最小值
if(f) { // 是后缀最小值
s.insert(x);
auto itr = s.lower_bound(x);
int val = -1;
if(itr != s.begin()) {
itr --;
for(; ; itr -- ) {
if(p[*itr] > p[x]) {
val = (*itr);
res -= query(*itr); // 减去贡献
T3.add((*itr), -1);
T4.add(p[*itr], -1);
}
if((itr == s.begin()) || (p[*itr] < p[x])) break;
}
if(val != -1) {
itr = s.lower_bound(x);
auto itl = s.lower_bound(val);
s.erase(itl, itr); // 删除
}
}
T1.add(x, 1);
T2.add(p[x], 1);
T3.add(x, 1);
T4.add(p[x], 1);
res += query(x); // 加上自己的贡献
}
else { // 不是后缀最小值
res += 1LL * ((T3.ask(n) - T3.ask(x)) - (T4.ask(n) - T4.ask(p[x])));
T1.add(x, 1);
T2.add(p[x], 1);
}
}
int main() {
scanf("%d%d", &n, &qc);
for(int i = 1; i <= n; i ++ ) {
scanf("%d", &p[i]);
}
for(int i = 1; i <= qc; i ++ ) {
int y;
scanf("%d", &y);
q[i] = (Q) {y, i};
vis[y] = 1;
}
sort(q + 1, q + qc + 1, cmp);
for(int i = n; i >= 1; i -- ) {
if(!vis[i]) ins(i), S ++;
}
for(int i = 1; i <= qc; i ++ ) {
ans[q[i].id] = res + S;
int x = q[i].x;
vis[x] = 0; ins(x); S ++;
}
ans[0] = res + S;
for(int i = 0; i <= qc; i ++ ) {
printf("%lld ", ans[i]);
}
return 0;
}