2024西安铁一中集训DAY12 ---- 模拟赛(并查集 + 线段树二分 + 虚树优化DP + (离线 + 树状数组))

时间安排与比赛成绩

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,addepi}

        这个转移是显然的,对于 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{fsonminj=ddepiddepfai1aj}

        右边的式子可以用 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
        但是还有删除某一个元素的操作。发现删除操作不好维护后缀最小值,我们 离线后变成添加操作。那么每次添加的结果就是 它是后缀最小值并删掉一些原来的后缀最小值 或者 它不是后缀最小值,对原来的后缀最小值没有影响
        分情况讨论添加元素的贡献:

  1. 它是后缀最小值。那么一些元素不再是后缀最小值。删去这些元素的贡献后加上添加元素的贡献。
  2. 它不是后缀最小值。那么别的元素对它没有贡献,但是它对后面的比它小的后缀最小值有贡献。这是一个 二维偏序,但是发现 它前面的后缀最小值都比它小,因此贡献就是它后面的后缀最小值个数减去比它的后缀最小值个数。

        上述操作可用 树状数组和 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;
}
  • 3
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值