GDOI2022普及组解题总结


前情:游记

而这篇是为了进一步总结做题相关的。

D1T2 数列游戏

把两个元素间的空隙看做一个端点,一次合并相当于不允许取一个端点。

很明显的思路是要让原来异或和为 \(0\) 的区间取不到,也就是合并掉存在异或和为 \(0\) 的区间的左右端点,那现在的问题就是找到这些区间。

注意到满足要求的区间拥有传递性(两个端点重合的区间合并起来也满足要求),于是可以维护一个并查集,如果一个区间的异或和为 \(0\),就把它的左右端点合并起来。同一个并查集中的任意两个元素中至多保留一个,答案就显然了。

有段代码今生难忘。

for(int i = 1; i <= n; i++){
    int u = find(i); if(vis[u]) continue; //然后呢,你不标记走过吗
    ans += siz[u] - 1;
}

正解不对拍,爆零两行泪。

点击查看非考场代码
#include 
#include 
#include 
#define UI unsigned int
using namespace std;
const int M = 100005;
int n, a[M], fa[M], siz[M], ans; UI x[M];
map t; bool vis[M];
int find(int u) {return fa[u] == u ? u : fa[u] = find(fa[u]);}
void merge(int u, int v){
	u = find(u); v = find(v);
	if(u != v) fa[u] = v, siz[v] += siz[u];
} 
int main(){
	scanf("%d", &n); t[0] = 0;
	for(int i = 1; i <= n; i++) fa[i] = i, siz[i] = 1;
	for(int i = 1; i <= n; i++){
		scanf("%d", &a[i]); x[i] = (x[i-1] ^ a[i]);
		auto iter = t.find(x[i]);
		if(iter != t.end()) merge(iter->second, i);
		t[x[i]] = i;
	}
	if(find(0) == find(n)) {puts("-1"); return 0;}
	for(int i = 1; i <= n; i++){
		int u = find(i); if(vis[u]) continue;
		ans += siz[u] - 1; vis[u] = 1;
	}
	printf("%d\n", n - ans);
}
/*
4
5 5 7 2
*/

D1T3

很有趣的一道题。赛时写的以为错解实际上是对的。

观察式子,发现当点数增加时必须让 \(\max(w_1,\cdots,w_m)\) 小下来,所以下推非当前最大节点是无意义的。

于是优先队列(或 set)就能轻松维护了。动态维护一个集合,每次取出非叶子中权值最大的并下推,途中记录最小值,即得。

点击查看代码
#include 
#include 
#include 
using namespace std;
const int M = 100005;
struct edge{
	int to, nxt;
}e[M << 1];
int head[M], cnt1;
void link(int u, int v){
	e[++cnt1] = {v, head[u]}; head[u] = cnt1;
}
int w[M], n, f[M]; bool l[M]; long long ans = 1e18;
struct dat{
	int t;
	bool operator < (const dat &tmp) const{ //for set 
		if(l[t] && !l[tmp.t]) return 0;
		if(w[t] != w[tmp.t]) return w[t] > w[tmp.t];
		return t < tmp.t;
	}
}; 
set q;
void calc(){
	while(1){
		int u = q.begin() -> t;
		q.erase({u});
		ans = min(ans, 1ll * w[u] * ((long long)q.size() + 1));
		for(int i = head[u]; i; i = e[i].nxt)
			q.insert({e[i].to});
		if(!l[u]) break; //如果全是叶子就应返回了
	}
}
int main(){
	scanf("%d", &n);
	for(int i = 1; i <= n; i++) scanf("%d", &w[i]);
	for(int i = 2; i <= n; i++)
		scanf("%d", &f[i]), link(f[i], i), l[f[i]] = 1;
	q.insert({1}); ans = w[1];
	calc();
	printf("%lld\n", ans);
}
/*
5
10 7 3 3 2
1 1 2 2
*/

D1T4 小学生计数题

两天唯一讲解没听懂的题……

后来自己去补了补。

枚举公差。这是一个复杂度为 \(O(n \ln n)\) 的调和级数枚举。

枚举完后,枚举末尾。与末尾对应的开头也是一段区间,这个可以用逆元前缀和弄出来。

这就结束了。

总结:完全不知道“调和级数枚举”的存在,一心在想 \(dp\),但其实该想到以前求出的答案可以被后面利用,进而自己逼近这个思想。

D2T1

只要找不是 \(n,n-1,n-2\) 约数的数,从反面找, \(\varphi(x)\) 套容斥就结束了。

D2T2

一次打开只能多浏览一个网页,关闭能关闭不止一个,容易想到往长儿子放一定不劣。

两遍 \(dfs\) 结束了。(赛时)

但是赛时讨论错了一些细节,还是必须细心些

如果讨论对可以发现就是 \(n\) + 叶子数。

D2T3

大模拟,无话可说。

1h20min 没拿到分,码力尚需加强 /kk

D2T4

好玩度仅次于 D1T4 的题。

先想暴力,发现可以用一个 bool 数组记录用前 \(i\) 个操作能否到达某个格子,然后就顺水推舟发现可以直接算每个格子最早可以几次操作到达,这个能用 dij 维护,提前预处理每个操作之后第一个每种操作,就能过了。

但是细节还是不少,写+调也花了 1h,最后的代码将也就 \(70\) 行(没怎么压行),赛后发现又挂在细节上了。

if(vx <= 0 || vx > n || vy <= 0 || vy > m) return 0; //走出去是 <1,你是不是多项式用 0 和 n 用傻了

惊险的故事:离结束 10min 时发现自己多测没清空……

点击查看代码
#include 
#include 
#include 
#define inf 1000000005
using namespace std;
const int M = 505, N = 100005;
int dis[M][M], g[N][4], T; //dis:到达每个位置最少到什么时候
//g:每条指令后面的第一个某种指令在什么时候 
int n, m, l, sx, sy; char s[N], a[M][M];
int dx[4] = {-1, 1, 0, 0}, dy[4] = {0, 0, -1, 1};
int get(char c){
	switch(c){
		case 'W' : return 0;
		case 'S' : return 1;
		case 'A' : return 2;
		case 'D' : return 3;
		default : return 114514; //不会 warning了 
	}
}
struct dat{
	int x, y, dis;
	bool operator < (const dat &t) const{
		return dis > t.dis;
	}
};
priority_queue q; bool vis[M][M];
bool dfs(int sx, int sy){
//	printf("in %d %d\n", sx, sy);
	for(int i = 1; i <= n; i++)
		for(int j = 1; j <= m; j++)
			dis[i][j] = inf, vis[i][j] = 0;
	dis[sx][sy] = 0; q.push({sx, sy, 0});
	while(!q.empty()){
		int ux = q.top().x, uy = q.top().y; q.pop();
//		printf("ux = %d uy = %d d = %d\n", ux, uy, dis[ux][uy]);
		if(vis[ux][uy]) continue; vis[ux][uy] = 1;
		int d = dis[ux][uy];
		for(int i = 0; i < 4; i++){
			if(!g[d][i]) continue;
			int vx = ux + dx[i], vy = uy + dy[i];
			if(vx <= 0 || vx > n || vy <= 0 || vy > m) return 0;
			if(a[vx][vy] == '*') continue;
//			printf("vx = %d vy = %d g[%d][%d] = %d\n", vx, vy, d, i, g[d][i]);
			if(g[d][i] < dis[vx][vy])
				dis[vx][vy] = g[d][i], q.push({vx, vy, dis[vx][vy]});
		}
	}
	return 1;
}
void clear(){
	memset(dis, 0, sizeof(dis)); memset(g, 0, sizeof(g));
	memset(s, 0, sizeof(s)); memset(a, 0, sizeof(a));
}
int main(){
	scanf("%d", &T);
	while(T--){
		scanf("%s", s+1); l = strlen(s+1);
		int p[4] = {0, 0, 0, 0};
		for(int i = 1; i <= l; i++){
			int op = get(s[i]);
			for(int j = p[op]; j < i; j++) g[j][op] = i;
			p[op] = i; 
		}
		scanf("%d %d", &n, &m);
		for(int i = 1; i <= n; i++){
			scanf("%s", a[i] + 1);
			for(int j = 1; j <= m; j++)
				if(a[i][j] == 'R') sx = i, sy = j;
		}
		printf("%s\n", dfs(sx, sy) ? "NO" : "YES");
		clear();
	}
}
/*
2
DWAWAADWASDSWSS
8 8
.***...*
**....*.
**...**.
**.*.***
......**
.*..*..*
*...**R*
...*..**
DWAWAADWASDSWSS
8 8
.***...*
**....*.
**...**.
**.*.***
......**
.*..*..*
*...**R*
..**..**
*/

总结

本来预计有 \(630\) 分的,最后却一共挂了 \(3\) 题,全都挂在细节上,要是能对拍或至少静态查错或许还能查出 1-2 道。

D2T3 LMY 仅花了 1h 便敲完了,所以我还是码力太弱了 /kk 是不是该练点大模拟了 /kel