2025-08-10 模拟赛总结 😥

预期:没有预期,因为是 IOI 赛制。
实际:\(100+100+65+100+50+0=415\)
排名:\(rk55/148\)

比赛链接:http://oj.daimayuan.top/contest/372

A - 树链上的连续段:

题意:

给定一个 \(n\) 个点的树,每个点被赋值为 \(0/1\),其中有 \(k\)\(1\),定义 \(f(x,y)\) 表示 \(x\)\(y\) 的简单路径上的所有点的点权顺序写下来的连续段数量。你需要给 \(n\) 个点赋权,最小化 \(f(x,y)\) 的最大值。

思路:

首先注意到答案肯定不超过 \(3\),因为你给一个连通块赋值为 \(1\),那么肯定有 \(\max f(x,y)\le 3\),所以我们只需要判断答案为 \(1,2\) 的情况即可。

答案为 \(1\) 的充要条件显然是 \(k=0,n\),构造方案就是全 \(0\) 或全 \(1\)

答案为 \(2\) 的充要条件需要手玩一下,我们发现如果一颗子树(根不固定)的大小刚好等于 \(k\),那么答案显然是 \(2\),这其实就是充要条件,然后直接枚举根算子树大小就可以了。

答案为 \(3\) 直接构造一个连通块即可。

代码:

#include <bits/stdc++.h>

using namespace std;

const int kMaxN = 2005;

int T, n, k, deg[kMaxN], siz[kMaxN], dep[kMaxN];
bool col[kMaxN], flag;
vector<int> g[kMaxN];

void Clear() {
	flag = 0;
	for (int i = 1; i <= n; i++) {
		g[i].clear(), col[i] = deg[i] = 0;
	}
}

void Dfs(int u, int fa) {
	siz[u] = 1, dep[u] = dep[fa] + 1;
	for (int v : g[u]) {
		if (v != fa) {
			Dfs(v, u);
			siz[u] += siz[v];
		}
	}
}

void Dfs1(int u) {
	col[u] = 1;
	for (int v : g[u]) {
		if (dep[v] > dep[u]) {
			Dfs1(v);
		}
	}
}

void Dfs2(int u, int fa) {
	if (flag) return;
	col[u] = 1, k--;
	if (k == 0) {
		flag = 1;
		return;
	}
	for (int v : g[u]) {
		if (flag) return;
		if (v != fa) Dfs2(v, u);
		if (flag) return;
	}
}

int main() {
	ios::sync_with_stdio(0), cin.tie(0);
	for (cin >> T; T; T--, Clear()) {
		cin >> n >> k;
		for (int i = 1, u, v; i < n; i++) {
			cin >> u >> v;
			g[u].push_back(v), g[v].push_back(u);
			deg[u]++, deg[v]++;
		}
		if (k == 0) {
			cout << "1\n";
			for (int i = 1; i <= n; i++) {
				cout << "0 ";
			}
			cout << '\n';
		} else if (k == n) {
			cout << "1\n";
			for (int i = 1; i <= n; i++) {
				cout << "1 ";
			}
			cout << '\n';
		} else {
			pair<int, int> p = {-1, -1};
			for (int rt = 1; rt <= n; rt++) {
				Dfs(rt, 0);
				for (int i = 1; i <= n; i++) {
					if (siz[i] == k) {
						p = {rt, i};
						break;
					}
				}
				if (~p.first) break;
			}
			if (~p.first) {
				cout << "2\n";
				Dfs1(p.second);
			} else {
				cout << "3\n";
				Dfs2(1, 0);
			}
			for (int i = 1; i <= n; i++) {
				cout << col[i] << ' ';
			}
			cout << '\n';
		}
	}
	return 0;
}

B - 等价前缀:

题意:

给定两个排列 \(p,q\),定义两个前缀等价当且仅当前缀的所有子段的 \(p,q\) 最小值位置相同,你需要求出最长的等价前缀。

思路:

显然可以二分,我们可以枚举哪个数是最小值,用单调栈维护左边和右边第一个比他小的位置,如果满足条件那么区间肯定相同,判断一下就行了。

但是你会发现这二分有什么用,我们可以枚举最长等价前缀,每次更新可以用到之前的信息,所以一点用都没有。(但是我写了二分)

代码:

#include <bits/stdc++.h>

using namespace std;

const int kMaxN = 5e5 + 5;

int T, n, p[kMaxN], q[kMaxN], lp[kMaxN], lq[kMaxN], rp[kMaxN], rq[kMaxN], stk[kMaxN], top, L, R, M;

void Work(int n, int *p, int *l, int *r) {
	top = 0;
	for (int i = 1; i <= n; i++) {
		l[i] = 0, r[i] = n + 1;
	}
	for (int i = 1; i <= n; i++) {
		for (; top && p[stk[top]] > p[i]; top--) {
			r[stk[top]] = i;
		}
		stk[++top] = i;
	}
	for (int i = n; i; i--) {
		for (; top && p[stk[top]] > p[i]; top--) {
			l[stk[top]] = i;
		}
		stk[++top] = i;
	}
}

bool C(int pos) {
	Work(pos, p, lp, rp);
	Work(pos, q, lq, rq);
	for (int i = 1; i <= pos; i++) {
		if (lp[i] != lq[i] || rp[i] != rq[i]) {
			return 0;
		}
	}
	return 1;
}

int main() {
	ios::sync_with_stdio(0), cin.tie(0);
	for (cin >> T; T; T--) {
		cin >> n;
		for (int i = 1; i <= n; i++) {
			cin >> p[i];
		}
		for (int i = 1; i <= n; i++) {
			cin >> q[i];
		}
		L = 1, R = n, M = L + R + 1 >> 1;
		for (; L < R; M = L + R + 1 >> 1) {
			C(M) ? L = M : R = M - 1;
		}
		cout << L << '\n';
	}
	return 0;
}

C - 天平:

题意:

思路:

暴力就是直接背包,每次看一个元素加入到哪一侧天平或不加入,这样就可以拿到 65pts。这样做有个小问题,他不好滚动数组,所以 dp 数组你得开 vector

这样做肯定不行,我们需要发掘更深的性质。

我们先考虑只在天平一侧放物品,另一侧放同样重的物品即可,但是这样两侧方案会有重复,但是我们又发现重复的部分可以减掉,所以只需要两种方案不同即可。所以我们可以换一种方式背包,背包求出选到第 \(i\) 个物品,重量为 \(j\) 的方案数,只要方案数大于 \(1\),那么这个 \(j\) 就合法。

这样做复杂度还是不对,但是这样做至少可以用滚动数组将空间优化了。

我们充分利用条件,这题我们并不需要求出具体的方案数,只需要求出方案数是否大于 \(1\) 就行了,我们发现其实两个和相等的条件很容易发生,最坏情况就是 \(1,2,4,8,\cdots\)(不考虑重复的元素),所以一个区间长度为 \(O(\log\sum a)\) 就会有和相等的两种方案,所以我们跑背包的时候值域只需开到 \(O(n\log\sum a)\) 就可以了。(这样可能会被卡,因为会有重复元素,所以 shuffle 一下应该就可以了,但是没有被卡)

其实还有一种方法,就是每次背包,找到一个 \(f_j>1\) 就将值域设为 \(j\),这样做正确性肯定可以保证,复杂度也可以证明。

代码:

我写的是正确性不怎么对的代码。

#include <bits/stdc++.h>
#define Add(x, y) (x = min(2, x + y))

using namespace std;

const int kMaxN = 3005, kMaxV = 5005;

int n, maxa, suma, a[kMaxN], f[kMaxN * kMaxV];

int main() {
	ios::sync_with_stdio(0), cin.tie(0);
	cin >> n;
	for (int i = 1; i <= n; i++) {
		cin >> a[i], suma += a[i];
		maxa = max(maxa, a[i]);
	}
	f[0] = 1;
	for (int i = 1; i <= n; i++) {
		for (int j = maxa * 25; j >= a[i]; j--) {
			Add(f[j], f[j - a[i]]);
		}
	}
	for (int i = 1; i <= maxa * 25; i++) {
		if (f[i] > 1) {
			if (i + i == suma) {
				cout << -1;
			} else {
				cout << suma - i - i;
			}
			return 0;
		}
	}
	cout << -1;
	return 0;
}

D - 摄影:

题意:

在一条直线上有 \(n\) 个摄影点,第 \(i\) 个摄影点的位置是 \(a_i\),最佳摄影时间为 \(t_i\) 秒。(保证 \(t_i\) 严格单调)

\(0\) 时刻你在 \(0\) 位置,你的最大速度是 \(v\) 每秒,对于每个摄影点,你可以选择在最佳摄影时间摄影,也可以直接跳过。你需要求出你最多可以进行几次拍摄。

思路:

首先这道题肯定是一个 dp,设 \(f_i\) 表示第 \(i\) 个摄影点进行拍摄的最多拍摄次数,考虑从 \(j\) 转移,那么 \(j\) 需要满足 \(|a_i-a_j|\le v(t_i-t_j)\),这样直接转移就是 \(O(n^2)\) 的。

其实这个式子有丰富的几何意义,先将点 \((t_i,a_i)\) 放在平面上,然后将横坐标乘 \(v\),再旋转坐标系就可以了。

几何意义不好展示,所以下面讲代数做法。

首先将绝对值拆开,变成 \(vt_j-vt_i\le a_i-a_j\le vt_i-vt_j\),拆成两个不等式并将 \(i,j\) 各自分组得到:

\[vt_j+a_j\le vt_i+a_i \]

\[vt_j-a_j\le vt_i-a_i \]

\[j<i \]

这显然就是一个三维偏序,可以用 cdq 分治优化 dp 来做,时间复杂度 \(O(n\log^2n)\)

但是这样太麻烦了,但是我们发现如果满足 \(0\le |a_i-a_j|\le vt_i-vt_j\),我们发现就一定满足 \(t_j\le t_i\)\(j<i\)

所以只需要满足条件的前两个就可以转移了,就是一个二维偏序问题,按照第一个条件排序,然后跑最长上升子序列就行了。

注意这道题必须强制选点 \((0,0)\),所以要把不能从 \((0,0)\) 到达的点全部删掉。

代码:

二维偏序代码:

#include <bits/stdc++.h>

using namespace std;

const int kMaxN = 2e5 + 5;

int n, v, a[kMaxN], tot, ans;
long long t[kMaxN], g[kMaxN];

struct P {
	long long x, y;
} p[kMaxN];

int main() {
	ios::sync_with_stdio(0), cin.tie(0);
	cin >> n >> v;
	for (int i = 1; i <= n; i++) {
		cin >> t[i];
	}
	p[++tot] = {0, 0};
	for (int i = 1; i <= n; i++) {
		cin >> a[i];
		if (v * t[i] - a[i] >= 0 && v * t[i] + a[i] >= 0) {
			p[++tot] = {v * t[i] - a[i], v * t[i] + a[i]};
		}
	}
	sort(p + 1, p + 1 + tot, [](P i, P j) {
		return i.x != j.x ? i.x < j.x : i.y < j.y;
	});
	for (int i = 1, now; i <= tot; i++) {
		now = upper_bound(g + 1, g + 1 + ans, p[i].y) - g;
		ans = max(ans, now), g[now] = p[i].y;
	}
	cout << ans - 1;
	return 0;
}

三维偏序代码:

赛后写的。

#include <bits/stdc++.h>
#define Chkmax(x, y) (x = max(x, y))

using namespace std;

const int kMaxN = 1e6 + 5;

int n, v, a[kMaxN], f[kMaxN], c[kMaxN], stk[kMaxN * 2], top;
long long t[kMaxN], x[kMaxN], y[kMaxN], b[kMaxN * 2], tot;
bool instk[kMaxN * 2];

struct P {
	int x, y, z;
} p[kMaxN];

bool cmp(P i, P j) {
	return i.y != j.y ? i.y < j.y : i.z < j.z;
}

void Modify(int x, int y) {
	for (; x <= tot; x += x & -x) {
		Chkmax(c[x], y);
		if (!instk[x]) instk[stk[++top] = x] = 1;
	}
}

int Query(int x, int r = -1e9) {
	for (; x; x -= x & -x) Chkmax(r, c[x]);
	return r;
}

void Solve(int l, int r) {
	if (l == r) return;
	int mid = l + r >> 1;
	Solve(l, mid);
	stable_sort(p + mid + 1, p + 1 + r, cmp);
	for (int i = l, j = mid + 1; j <= r; j++) {
		for (; i <= mid && p[i].y <= p[j].y; i++) {
			Modify(p[i].z, f[p[i].x]);
		}
		Chkmax(f[p[j].x], Query(p[j].z) + 1);
	}
	for (; top; c[stk[top]] = -1e9, instk[stk[top]] = 0, top--) {
	}
	Solve(mid + 1, r);
	inplace_merge(p + l, p + mid + 1, p + r + 1, cmp);
}

int main() {
	ios::sync_with_stdio(0), cin.tie(0);
	cin >> n >> v;
	for (int i = 1; i <= n; i++) {
		cin >> t[i], t[i] *= v;
	}
	for (int i = 1; i <= n; i++) {
		cin >> a[i];
		x[i] = t[i] - a[i];
		y[i] = t[i] + a[i];
		b[++tot] = x[i], b[++tot] = y[i];
	}
	b[++tot] = 0;
	sort(b + 1, b + 1 + tot), tot = unique(b + 1, b + 1 + tot) - b - 1;
	for (int i = 0; i <= n; i++) {
		x[i] = lower_bound(b + 1, b + 1 + tot, x[i]) - b;
		y[i] = lower_bound(b + 1, b + 1 + tot, y[i]) - b;
		p[i] = {i, (int)x[i], (int)y[i]};
	}
	memset(f, 0xc0, sizeof(f)), memset(c, 0xc0, sizeof(c));
	f[0] = 0;
	Solve(0, n);
	cout << *max_element(f, f + 1 + n);
	return 0;
}

E - 安全通道:

题意:

给定一颗树,你需要求出有多少对 \(1\le i<j\le n\),使得连接 \(i,j\) 后得到的基环树的最大独立集等于原来树的最大独立集。

思路:

首先考虑暴力,如何快速求出一个基环树的最大独立集,我们把环边 \((i,j)\) 取出,那么 \(i,j\) 肯定有一个不选,所以我们可以以 \(i\) 为根跑一边 dfs 求最大独立集,再以 \(j\) 为根求最大独立集,所以 \(\max(f_{i,0},f_{j,0})\) 就是答案。这样的时间复杂度是 \(O(n^3)\) 的。

但是我们发现对于每一条边都跑一边 \(i,j\) 的最大独立集没有用,其实我们可以对于每个点 \(O(n)\) 预处理出 \(i\) 不选的最大独立集,这样就不需要枚举 \((i,j)\) 再跑一边了。时间复杂度:\(O(n^2)\)

其实我们只需要求出每一个 \(i\)\(i\) 为根的 \(f_{i,0}\) 就可以解决这道题,因为找到 \(\max(f_{i,0},f_{j,0})=ans\) 的个数很好找,算出 \(f_{i,0}=ans\) 的个数就可以算出总个数。

这显然是一个换根 dp,设 \(g_{i,0/1}\) 表示 \(i\) 选或不选,整棵树除去 \(i\) 的子树的方案数,很好转移具体看代码。


下面是赛时的思路。

但是其实可以不换根 dp,我们继续充分利用条件,其实我们只需要判断 \(f_{i,0}\) 是否等于 \(ans\) 即可,而 \(ans=\max(f_{i,0},f_{i,1})\),所以其实我们只需要判断是否存在一种方案使得 \(i\) 不被选上就可以了。这就变成了一个记录路径的问题。

我们直接从 \(1\) 开始爆搜每个点 \(u\) 选什么,然后看下一个点 \(v\) 在最优决策下可以选什么,首先如果 \(u\) 选,那么 \(v\) 只能选 \(0\),否则看 \(f_{v,0},f_{v,1}\) 哪个大,就选哪个,如果一样大都 dfs 一遍。然后就可以得到每个点是否可以不选,但是这样的时间复杂度可能还比 \(O(n^2)\) 更劣。(((

但是我们可以加一个记忆化搜索,如果这个点已经已经考虑过它选 / 不选的情况,就可以不考虑了,因为之前考虑过了,这样的复杂度好像就是 \(O(n)\) 的了。

代码:

换根 dp:

#include <bits/stdc++.h>

using namespace std;

const int kMaxN = 2.5e5 + 5;

int n, f[kMaxN][2], g[kMaxN][2], cnt;
vector<int> e[kMaxN];

void Dfs(int u, int fa) {
	f[u][1] = 1;
	for (int v : e[u]) {
		if (v != fa) {
			Dfs(v, u);
			f[u][0] += max(f[v][0], f[v][1]);
			f[u][1] += f[v][0];
		}
	}
}

void Dfs1(int u, int fa) {
	int c0 = 0, c1 = 0, c01 = 0;
	for (int v : e[u]) {
		if (v != fa) {
			c0 += f[v][0];
			c1 += f[v][1];
			c01 += max(f[v][0], f[v][1]);
		}
	}
	for (int v : e[u]) {
		if (v != fa) {
			g[v][1] = g[u][0] + c01 - max(f[v][0], f[v][1]);
			g[v][0] = max(g[u][0] + c01 - max(f[v][0], f[v][1]), g[u][1] + 1 + c0 - f[v][0]);
			Dfs1(v, u);
		}
	}
}

int main() {
	ios::sync_with_stdio(0), cin.tie(0);
	cin >> n;
	for (int i = 1, u, v; i < n; i++) {
		cin >> u >> v;
		e[u].push_back(v), e[v].push_back(u);
	}
	Dfs(1, 0), Dfs1(1, 0);
	for (int i = 1; i <= n; i++) {
		cnt += f[i][0] + g[i][0] == max(f[1][0], f[1][1]);
	}
	cout << 1LL * cnt * (cnt - 1) / 2 + 1LL * cnt * (n - cnt);
	return 0;
}

记忆化搜索记录路径:

#include <bits/stdc++.h>

using namespace std;

const int kMaxN = 2.5e5 + 5;

int n, f[kMaxN][2], a[kMaxN], cnt;
vector<int> g[kMaxN];

void Dfs(int u, int fa) {
	f[u][1] = 1;
	for (int v : g[u]) {
		if (v != fa) {
			Dfs(v, u);
			f[u][1] += f[v][0];
			f[u][0] += max(f[v][0], f[v][1]);
		}
	}
}

void Solve(int u, int fa, int o) {
	if (a[u] == o) return;
	a[u] = min(a[u], o);
	for (int v : g[u]) {
		if (v != fa) {
			if (o == 0) {
				if (f[v][0] >= f[v][1]) {
					Solve(v, u, 0);
				}
				if (f[v][0] <= f[v][1]) {
					Solve(v, u, 1);
				}
			} else {
				Solve(v, u, 0);
			}
		}
	}
}

int main() {
	ios::sync_with_stdio(0), cin.tie(0);
	cin >> n;
	for (int i = 1, u, v; i < n; i++) {
		cin >> u >> v;
		g[u].push_back(v), g[v].push_back(u);
	}
	Dfs(1, 0);
	fill(a, a + 1 + n, 1e9);
	if (f[1][0] >= f[1][1]) {
		Solve(1, 0, 0);
	}
	if (f[1][0] <= f[1][1]) {
		Solve(1, 0, 1);
	}
	for (int i = 1; i <= n; i++) {
		cnt += a[i] == 0;
	}
	cout << 1LL * cnt * (cnt - 1) / 2 + 1LL * cnt * (n - cnt);
	return 0;
}

F - 异或游走:

题意:

思路:

代码:

posted @ 2025-08-12 19:48  liruixiong0101  阅读(15)  评论(0)    收藏  举报