2023.10.31 模拟赛总结

2023.10.31 模拟赛总结

  • 总结
    • 时间安排
    • 考试总结
  • 题解
  • T1
  • T2
  • T3

总结

时间安排


\qquad 没啥可说的,罚坐全场,中间有一段狂交 rand 刷分,开启 IOI 赛制

考试总结


\qquad 虽然本场题非常恶心,区分度不高,但是还是有些分没拿到。像 T3 \text{T3} T3 O ( q n ) O(qn) O(qn) dp \text{dp} dp 或贪心, T1 \text{T1} T1 的爆搜(没写是因为感觉细节太多,不是很会处理)。考场上看到难题要想尽办法磕部分分啊……

题解

T1

2023.10.31 模拟赛总结_第1张图片
2023.10.31 模拟赛总结_第2张图片


\qquad 看到数据范围,果断状压 dp \text{dp} dp。容易想到的一个 dp \text{dp} dp 转移方法是:设 dp m a s k \text{dp}_{mask} dpmask 表示由 mask \text{mask} mask 状态到最终状态所需的最短时间,然后每次进行一个新操作去转移。但是,不难发现,这么转移有两个弊端:其一,它有后效性,因为你不知道当前状态 mask \text{mask} mask 在新加一个操作后的新状态 mask’ \text{mask'} mask’ mask \text{mask} mask 比谁大,这意味着转移时不能固定一个枚举顺序。但是这个问题很好解决,每当状态有后效性的时候,考虑加维度即可。其二,我们往后加操作时,前面所有操作都需要往后延伸一秒,非常难处理。而这个问题的解决方案很妙。

\qquad 我们考虑两个操作序列:1,从第 1 1 1 秒开始,以后三秒内分别操作第 1 , 2 , 3 1,2,3 1,2,3 号节点;2,第 1 1 1 秒不进行任何操作,从第 2 2 2 秒开始,以后三秒内分别操作第 1 , 2 , 3 1,2,3 1,2,3 号节点。不难发现,这两种操作序列的最终状态是相同的。这意味着,我们可以把向后添加操作转变为向前添加操作,这样我们就只需关心新添加操作在若干秒后的状态了。

\qquad 综上,我们最终的 dp \text{dp} dp 为:设 dp i , m a s k \text{dp}_{i,mask} dpi,mask 表示第 i i i 秒后能否到达状态 mask \text{mask} mask,同时,我们预处理一个 w i , j \text{w}_{i,j} wi,j 表示对点 i \text{i} i 进行操作, j \text{j} j 秒后树的状态,那么 dp \text{dp} dp 转移式就很好写了,具体见 Code \text{Code} Code

\qquad 核心 Code \text{Code} Code

for(int i = 1; i <= n; i ++) {//预处理 w 数组
	int k = i;
	for(int j = 1; j <= n; j ++) {
		if(k) w[i][j] = w[i][j - 1] + (1 << (k - 1));
		else w[i][j] = w[i][j - 1];
		k = f[k];
	}
}
dp[0][0] = 1;
for(int i = 0; i <= n; i ++) {
	if(dp[i][ed]) return printf("%d\n", i), 0;//若第i秒能达到目标状态就直接输出
	for(int j = 0; j < (1 << n); j ++) {
		if(!dp[i][j]) continue;
		dp[i + 1][j] = 1;//意味着不往前添加操作
		for(int k = 1; k <= n; k ++) {//枚举往前添加操作哪一个点
			dp[i + 1][j ^ w[k][i + 1]] = 1;
		}
	}
}

T2

2023.10.31 模拟赛总结_第3张图片
2023.10.31 模拟赛总结_第4张图片


\qquad 这题真的很难想……

\qquad 看到本题,第一直觉应该就是隐式建图。但是怎么建呢?考虑到本题给了每个人一个最终状态,学会、没学会或无所谓,这相当于是一个限制,所以我们就从限制入手。我们设 maxT i \text{maxT}_i maxTi 表示第 i \text{i} i 个人最早得在什么时候学会算法。形式化地说,如果设 t i \text{t}_i ti 表示第 i i i 个人学会毒瘤算法的时间,那么 maxT i ≤ t i \text{maxT}_i \leq \text{t}_i maxTiti。显然,最终没学会的人(设其状态为 w i = − 1 \text{w}_i = -1 wi=1)的 maxT i \text{maxT}_i maxTi + ∞ +\infty +。我们想:如果有一个 w i ≠ − 1 \text{w}_i\ne -1 wi=1 w j = − 1 \text{w}_j=-1 wj=1 的人必须要在 [ L k , R k ] [\text{L}_k,\text{R}_k] [Lk,Rk] 内吃饭,那么人 j \text{j} j 可以给人 i \text{i} i 一个什么限制呢?如下图:

2023.10.31 模拟赛总结_第5张图片
\qquad 我们想:若 maxT i \text{maxT}_i maxTi 处于位置①,那么就意味着在他们吃饭前人 i \text{i} i 就学会了算法,这就意味着无论他们在哪一天吃饭,人 j j j 都将学会毒瘤算法,这显然是不合法的。但是,如果 maxT i \text{maxT}_i maxTi 处于位置②,那么我们只要将他们吃饭的时间安排在 [ L k , maxT i − 1 ] [\text{L}_k,\text{maxT}_i-1] [Lk,maxTi1],就可以同时保证两人维持合法状态。所以此时人 j \text{j} j 会限制人 i \text{i} i maxT ≥ L + 1 \text{maxT}\geq \text{L}+1 maxTL+1。那么,我们继续想:若一个人 i \text{i} i 要在 [ L k 1 , R k ] [\text{L}_{k1},\text{R}_k] [Lk1,Rk] [ L k 2 , R k ] [\text{L}_{k2},\text{R}_k] [Lk2,Rk] 这两个时间段分别与两个 w j = − 1 \text{w}_j=-1 wj=1 的人吃饭,那么此时 maxT i \text{maxT}_i maxTi 会受到什么限制呢?显然是 maxT ≥ max ⁡ ( L k 1 + 1 , L k 2 + 1 ) \text{maxT}\geq \max(\text{L}_{k1}+1,\text{L}_{k2}+1) maxTmax(Lk1+1,Lk2+1),需要取 max ⁡ \max max

\qquad 通过上面的分析,我们知道 w = − 1 \text{w}=-1 w=1 的人可以限制 w ≠ − 1 \text{w}\ne -1 w=1 的人,那么 w ≠ − 1 \text{w}\ne -1 w=1 的人之间能不能互相限制呢?答案是可以的。我们想,什么情况下这种限制才是有意义的呢?
2023.10.31 模拟赛总结_第6张图片
\qquad 我们想:假设当前人 i,j \text{i,j} i,j w ≠ − 1 \text{w}\ne -1 w=1 maxT i \text{maxT}_i maxTi 在③或④位置, maxT j \text{maxT}_j maxTj 在①或②位置。我们分类讨论一下:1、若 maxT i \text{maxT}_i maxTi 在③位置,那么如果我们把两人的吃饭时间安排在 [ maxT i , R k ] [\text{maxT}_i,\text{R}_k] [maxTi,Rk] 之间,便可以让人 i,j \text{i,j} i,j 同时满足条件。因为此时人 i \text{i} i 已经学会了,完全可以教给人 j \text{j} j,并不会给 maxT j \text{maxT}_j maxTj 带来限制。但是如果 maxT i \text{maxT}_i maxTi 在位置④,就意味着人 i \text{i} i 在此次饭局中不能学会,那么我们就可以把他们安排在 L \text{L} L 吃饭,然后限制 maxT j = L + 1 \text{maxT}_j=\text{L}+1 maxTj=L+1。所以这表明,只有当 maxT i > R + 1 \text{maxT}_i > \text{R}+1 maxTi>R+1 的时候才会给 maxT j \text{maxT}_j maxTj 带来限制。2、若 maxT j \text{maxT}_j maxTj 在位置②,就意味着之前已经有一个 i’ \text{i'} i’ 限制了 maxT j \text{maxT}_j maxTj,而且限制的要比 i \text{i} i 更晚,这就意味着 i \text{i} i 并不会给 j \text{j} j 带来限制。综上,只有当 maxT j ≤ L ≤ R < maxT i \text{maxT}_j\leq \text{L}\leq \text{R}<\text{maxT}_i maxTjLR<maxTi 的时候,转移才有意义。

\qquad 清楚了转移的原理,我们来想一下转移的实现。通过上面的分析,我们不难发现: maxT \text{maxT} maxT 的值总是由大的一个转到小的一个,而且对于单个 maxT i \text{maxT}_i maxTi 只会越转越大,最终在所有可能的转移中取 max ⁡ \max max。这是不是类似于 Dijkstra \text{Dijkstra} Dijkstra 呢?我们每次从堆顶取出 maxT \text{maxT} maxT 最大的一个(保证已被限制到了最终状态),然后去更新其他的。这样,我们的转移就成功实现了。

\qquad 在这一转移结束后,我们想:如果 maxT 1 > 0 \text{maxT}_1>0 maxT1>0,是不是意味着一号节点被限制在 0 0 0 时刻之后才能学会?这与 1 1 1 节点开始就会的规定不符,所以我们可以直接判 Impossible \text{Impossible} Impossible

\qquad 接下来,我们考虑,上面这个转移结束后,整道题就结束了吗?显然没有。上面这一转移中我们只说明了存在一种方案使得所有 w = − 1 w=-1 w=1 的人满足条件,但是并没有顾及到 w = 1 w=1 w=1 的人。怎么办呢?考虑到如果一个 w = 1 w=1 w=1 的人要学会,那肯定是越早越好。我们效仿上面的过程,设 d i \text{d}_i di 表示人 i i i 最早什么时候能学会。显然,初始时 d 1 = 0 \text{d}_1=0 d1=0,其余的为正无穷。我们思考,人 i \text{i} i 给人 j \text{j} j 转移时,有哪些限制条件呢?
2023.10.31 模拟赛总结_第7张图片
\qquad 在上图中,我们可以清晰地发现, d j \text{d}_j dj 需要同时满足:大于等于 d i \text{d}_i di,大于等于 L k \text{L}_k Lk,大于等于 maxT j \text{maxT}_j maxTj。同时, d j \text{d}_j dj 还需要小于等于 R k \text{R}_k Rk。形式化地说,我们设 maxx = max ⁡ ( d i , max ⁡ ( L k , maxT j ) ) \text{maxx}=\max(\text{d}_i,\max(\text{L}_k,\text{maxT}_j)) maxx=max(di,max(Lk,maxTj)),当 maxx ≤ R \text{maxx}\leq \text{R} maxxR maxx < d j \text{maxx}<\text{d}_j maxx<dj 时, d j = maxx \text{d}_j=\text{maxx} dj=maxx。最后,如果有一个 w = 1 \text{w}=1 w=1 的人 i \text{i} i,他的 d i = + ∞ \text{d}_i=+\infty di=+,那么我们就可以直接判 Impossible \text{Impossible} Impossible

\qquad 这个转移结束后,所有 w = 1 , − 1 \text{w}=1,-1 w=1,1 的人全都考虑到了,一定存在一个合法状态使得他们都满足条件。但是我们好像忽略了一种人: w = 0 \text{w}=0 w=0 的人!那么 w = 0 \text{w}=0 w=0 的人怎么处理呢?显然的是, w = 0 \text{w}=0 w=0 的人是不能用来判 Impossible \text{Impossible} Impossible 的,因为他最终状态是啥无所谓。但是他有一个传递限制的作用。例如一个 w = 1 \text{w}=1 w=1 和另一个 w = − 1 \text{w}=-1 w=1 的人要同时和一个 w = 0 \text{w}=0 w=0 的人吃饭,那么这个 w = 0 \text{w}=0 w=0 的人就会把 − 1 -1 1 带来的限制传给 w = 1 \text{w}=1 w=1 的人。这意味着我们并不需要对 w = 0 \text{w}=0 w=0 的人做任何特殊操作,正常转即可。

\qquad 核心 Code \text{Code} Code

for(int i = 1; i <= n; i ++) {
	scanf("%d", &a[i]);
	if(a[i] == -1) maxT[i] = INF, q1.push((node1){i, maxT[i]});
}
while(!q1.empty()) {//让maxT大的优先出堆
	node1 Top = q1.top(); q1.pop();
	int l = Top.loc, r = Top.road;
	if(vis[l]) continue;
	vis[l] = 1;
	for(auto p : to[l]) {
		int v = p.to, L = p.L, R = p.R;
		if(a[v] != -1) {
			if(maxT[v] < L && R < maxT[l])//若maxT[l]<=R,那么完全可以让x提前学会后在maxT[l]~R中的某一天安排x,y见面,这样的话转移毫无意义,也不会对x提出限制
				maxT[v] = L + 1, q1.push((node1){v, maxT[v]});
		}
	}
}
if(maxT[1] > 0) return puts("Impossible"), 0;
memset(d, 0x7f, sizeof d), memset(vis, 0, sizeof vis);
d[1] = 0, q2.push((node2){1, 0});
while(!q2.empty()) {
	node2 Top = q2.top(); q2.pop();
	int l = Top.loc, r = Top.road;
	if(vis[l]) continue;
	vis[l] = 1;
	for(auto p : to[l]) {
		int v = p.to, L = p.L, R = p.R;
		int maxx = max(d[l], max(L, maxT[v]));
		if(maxx <= R && maxx < d[v]) d[v] = maxx, q2.push((node2){v, d[v]});//满足限制
	}
}
for(int i = 1; i <= n; i ++) {
	if(a[i] == 1 && d[i] == INF) return puts("Impossible"), 0;
}
puts("JJXSM");

T3

T3题面
2023.10.31 模拟赛总结_第8张图片

\qquad 首先,我们先考虑本题的 Θ ( Qn ) \Theta(\text{Qn}) Θ(Qn) 做法怎么写?既然要在 Θ ( len ) \Theta(\text{len}) Θ(len) 的时间复杂度内完成区间查询,那就一定是一维 dp \text{dp} dp 或贪心去搞。但是考虑到贪心拓展性小,我们考虑设计 dp \text{dp} dp 状态。设 dp i , 0 / 1 \text{dp}_{i,0/1} dpi,0/1 表示:考虑到第 i i i 个位置, 0 0 0 表示全是 0 0 0 1 1 1 表示末尾有一段连续一的最小操作步数。状态设计好之后,转移其实并不难,简单分讨一下即可。

\qquad 核心 Code \text{Code} Code

if(s[x] == '0') f[x][0] = 0, f[x][1] = 1;//0->0:0  0->1:1
else f[x][1] = 0, f[x][0] = 1;//1->0:1  1->1:0
for(int j = x + 1; j <= y; j ++) {
	if(s[j] == '0') {
		f[j][0] = min(f[j - 1][1] + 2, f[j - 1][0]);//要么两步让前面连续1变为0,要么直接与前面0拼上
		f[j][1] = min(f[j - 1][1] + 1, f[j - 1][0] + 1);//一定要花费1让当前0变为1
	}
	else {//类似上面
		f[j][0] = min(f[j - 1][0] + 1, f[j - 1][1] + 2);
		f[j][1] = min(f[j - 1][0], f[j - 1][1]);
	}
}
printf("%d\n", min(f[y][0], f[y][1] + 2));

\qquad 现在我们考虑优化。注意到每次的转移只与 i − 1 \text{i}-1 i1 有关,而且当 s i \text{s}_i si 定了之后,每次转移在 0 / 1 0/1 0/1 之间加的系数是相同的,所以我们考虑用线段树维护一个类似矩阵的转移。建树和修改就变得非常简单,但是查询的时候有一点细节。我们不能直接查 [ l , r ] [\text{l},\text{r}] [l,r] 的矩阵,而是应该判断 l \text{l} l 的值,然后与 [ l + 1 , r ] [\text{l}+1,\text{r}] [l+1,r] 的矩阵结合后再输出。

\qquad Code \text{Code} Code

#include 
using namespace std;

const int maxn = 3e5 + 10;
int n, Q;
char s[maxn];
struct Matrix {
	int mt[2][2];
	Matrix() {memset(mt, 0x3f, sizeof mt);}//定义时自动初始化
	
	friend Matrix operator * (Matrix a, Matrix b) {//重载乘号
		Matrix c;
		for(int i = 0; i < 2; i ++)
			for(int j = 0; j < 2; j ++)
				for(int k = 0; k < 2; k ++)
					c.mt[i][j] = min(c.mt[i][j], a.mt[i][k] + b.mt[k][j]);
		return c;
	}
};
struct Segment {
	int l, r;
	Matrix dat;
	#define l(x) tree[x].l
	#define r(x) tree[x].r
	#define dat(x, a, b) tree[x].dat.mt[a][b]
	#define d(x) tree[x].dat
}tree[maxn << 2];

inline void update(int p) {
	d(p) = d(p << 1) * d(p << 1 | 1);
}

void build(int p, int l, int r) {
	l(p) = l, r(p) = r;
	if(l == r) {
		if(s[l] == '0') dat(p, 0, 0) = 0, dat(p, 1, 0) = 2, dat(p, 0, 1) = dat(p, 1, 1) = 1;
		else dat(p, 0, 0) = 1, dat(p, 1, 0) = 2, dat(p, 0, 1) = dat(p, 1, 1) = 0;
		return ;
	}
	int mid = l + r >> 1;
	build(p << 1, l, mid), build(p << 1 | 1, mid + 1, r);
	update(p);
}

void modify(int p, int x, int val) {
	if(l(p) == r(p)) {
		if(val == 0) dat(p, 0, 0) = 0, dat(p, 1, 0) = 2, dat(p, 0, 1) = dat(p, 1, 1) = 1;
		else dat(p, 0, 0) = 1, dat(p, 1, 0) = 2, dat(p, 0, 1) = dat(p, 1, 1) = 0;
		return ;
	}
	int mid = l(p) + r(p) >> 1;
	if(x <= mid) modify(p << 1, x, val);
	else modify(p << 1 | 1, x, val);
	update(p);
}

Matrix query(int p, int l, int r) {
	if(l <= l(p) && r(p) <= r) return d(p);
	int mid = l(p) + r(p) >> 1;
	if(r <= mid) return query(p << 1, l, r);
	if(l > mid) return query(p << 1 | 1, l, r);
	return query(p << 1, l, r) * query(p << 1 | 1, l, r);
}

int solve(int l, int r) {
	Matrix res;
	if(s[l] == '0') res.mt[0][0] = 0, res.mt[0][1] = 1;//l与[l+1,r]的矩阵相乘
	else res.mt[0][0] = 1, res.mt[0][1] = 0;
	if(l == r) return res.mt[0][0];
	res = res * query(1, l + 1, r);
	return res.mt[0][0];
}

int main() {
	scanf("%d%s%d", &n, s + 1, &Q);
	build(1, 1, n);
	for(int i = 1, opt, x, y; i <= Q; i ++) {
		scanf("%d%d%d", &opt, &x, &y);
		if(opt == 1) printf("%d\n", solve(x, y));
		else modify(1, x, y), s[x] = y + '0';
	}
	return 0;
}

你可能感兴趣的:(个人总结,模拟考总结,算法)