2025 图灵杯做题记录
2025 图灵杯做题记录
老师让我们打高级组,罚坐 3h,获得了 \(27 + 10 + 0 + 0 = 37\) 分的高分,获得了 rk69 的好成绩!
最后 50min 的时候觉得罚坐也没什么意义,然后报名了中级组,花 40min 通过了中级组 T1,但没时间看 T2 了/kk
各组别使用的题:
- 初级组:ABCD
- 中级组:CDEF
- 高级组:EFGH
C. 登机
注意到问题的关键点在于乘客前进时会被先走的人挡住。一个乘客在前进的途中可能被多个人挡住,这是不好处理的。关键的观察在于:只有最后一个挡住该乘客的人是值得关注的。因为即使忽略前面所有挡住他的人,到最后他还是会被挡住。
那么关键在于求出最后一个挡住第 \(i\) 个乘客的人。先把乘客按 \(p_i\) 从小到大排序并重标号,记 \(t_{j}\) 表示从前往后第 \(j\) 个乘客就座的时间,\(q_j\) 表示就坐的位置。按时间顺序处理,对所有坐在第 \(i\) 个乘客前面的人 \(j\),判断 \(i\) 会不会被 \(j\) 挡住,如果会被挡住,记 \(x\) 表示最后一个挡住 \(i\) 的人就坐的时间。那么
也就是说,\(i\) 到达第 \(j\) 个人的位置的时间是 \(s_i + q_j\),如果被挡住,就用 \(t_j\) 更新 \(x\)。注意着其实也处理了没有被人挡住的情况。取 \(\max\) 的过程就求出了最后一个挡住 \(i\) 的人 \(j\)。那么 \(i\) 就坐的时间
为了分离 \(ans\) 中的 \(j\),修改 \(x\) 的定义为
则
\(x\) 可以通过在树状数组上查询前缀最小值得到。时间复杂度 \(O(n \log n)\)。
(注意树状数组可以维护前缀最小值——虽然我原则上知道这一点,但实际使用上老是忘记,赛时还是写了线段树。)
Code
#include<bits/stdc++.h>
using namespace std;
typedef long long i64;
struct SGT {
#define lson (id << 1)
#define rson (id << 1 | 1)
const int n;
vector<i64> c;
SGT(int _n): n(_n), c(n << 2) {}
void update(int id) {
c[id] = max(c[lson], c[rson]);
}
void change(int id, int l, int r, int pos, i64 x) {
if(l == r) {
c[id] = x;
return;
}
int mid = (l + r) >> 1;
if(pos <= mid) change(lson, l, mid, pos, x);
else change(rson, mid + 1, r, pos, x);
update(id);
}
void change(int pos, i64 x) { change(1, 1, n, pos, x); }
i64 query(int id, int l, int r, int L, int R) {
if(l == L && r == R) {
return c[id];
}
int mid = (l + r) >> 1;
if(R <= mid) return query(lson, l, mid, L, R);
else if(L > mid) return query(rson, mid + 1, r, L, R);
else return max(query(lson, l, mid, L, mid), query(rson, mid + 1, r, mid + 1, R));
}
i64 query(int L, int R) { return query(1, 1, n, L, R); }
};
int main() {
cin.tie(nullptr) -> sync_with_stdio(false);
int n;
cin >> n;
vector<int> s(n + 1), p(n + 1), a(n + 1);
for(int i = 1; i <= n; i++) {
cin >> s[i];
}
for(int i = 1; i <= n; i++) {
cin >> p[i];
}
for(int i = 1; i <= n; i++) {
cin >> a[i];
}
vector<int> q = p;
sort(q.begin() + 1, q.end());
vector<i64> ans(n + 1);
SGT tr(n);
for(int i = 1; i <= n; i++) {
auto it = lower_bound(q.begin() + 1, q.end(), p[i]);
int id = (int)(it - q.begin());
i64 x = max((i64)s[i], tr.query(1, id));
ans[i] = x + a[i] + p[i];
tr.change(id, ans[i] - q[id]);
}
for(int i = 1; i <= n; i++) {
cout << ans[i] << '\n';
}
return 0;
}
D. Bougainvillea
给定一个无向带权图,多次询问两点之间是否存在长度为 \(w\) 的倍数的路径。
好题。不需要任何复杂的算法,只需要对性质的深刻观察。
\(o = 1\)
等价于询问两点之间是否可以只通过边权为 \(0\) 的边到达。用 \(0\) 边 dfs/bfs 预处理一遍连通块即可。
\(o = 2\)
等价于询问两点之间是否存在长度为偶数的路径。套路性地想到二分图,因为二分图满足性质:同色点之间的路径长度必定为偶数,异色点的路径长度必定为奇数。尝试对一个连通块进行黑白染色,如果成功,说明该连通块是二分图,询问时判断两点的颜色是否相同即可。否则说明连通块中存在奇环。此时,对于 \(u, v\) 之间的任意一条路径,如果长度为偶数,则我们已经找到了一个解;否则,设 \(x\) 是路径上的任意一个点,\(y\) 是奇环上的任意一个点,在原路径上到达 \(x\) 时,从 \(x\) 出发到达 \(y\),走过整个奇环回到 \(y\),再从 \(y\) 原路返回 \(x\),然后沿着原来的路径走。\(x \mapsto y\) 和 \(y \mapsto x\) 两条路径完全相同,长度之和是偶数,不影响总长度的奇偶性;而奇环改变了奇偶性,所以路径长度由奇数改变成了偶数。这就说明:如果图中存在奇环,则任意两点之间必定存在长度为偶数的路径。
\(w \le 3\)
本题中最关键的一个部分分。先假设边权都为 \(1\)。\(w = 1\) 的情况相当于询问两点之间是否连通,这是容易的;\(w = 2\) 的情况已经在上一个部分讨论过;关键在于 \(w = 3\) 的情况。我们观察到:
如果 \(k\) 是奇数,\(u, v\) 之间存在长度为 \(k\) 的倍数的路径等价于两点连通。
证明 对于 \(u \mapsto v\) 的任意一条路径,设其长度为 \(s\),我们反复走这条路径 \(k\) 次:
由于 \(k\) 是奇数,所以最终一定会回到 \(v\),而路径总长度为 \(k \cdot s\),是 \(k\) 的倍数。\(\Box\)
这说明 \(w\) 是奇数的情况都是平凡的:只要判断两点是否连通即可。
这个部分分中边权可能不为 \(1\),这只影响 \(w = 2\) 的情况。处理是容易的:在二分图染色时加上边权的影响即可:如果两点之间的边权为偶数,则染成同色,否则染成不同颜色。这样染出来的二分图仍然满足性质:同色点之间的路径长度为偶数,异色点为奇数。
\(o = 3\)
我们已经知道奇数询问是平凡的。进一步,设 \(k\) 是 \(w\) 质因数分解中 \(2\) 的幂次(或者说 \(2^{k}\) 是 \(w\) 的 \(\operatorname{lowbit}\)),\(w = a \cdot 2^{k}\),那么 \(a\) 是奇数。扩展之前的结论,从 \(w\) 中除掉一个奇数不影响答案,那么最终只用考虑 \(w = 2^{k}\) (\(k \ge 1\))的情况。
设连通块中所有边权的 \(\operatorname{lowbit}\) 的最小值为 \(2^{b}\),则如果 \(2^{k} \mid 2^{b}\),则任意路径长度都是 \(2^{k}\) 的倍数,一定有解。否则把所有边权都除以 \(2^{b}\),那么连通块中一定存在奇数边权。此时询问变成了是否存在 \(u \mapsto v\) 的长度为 \(2^{k - b}\) 的倍数的路径。还是二分图染色,由于连通块是树,所以一定可以染色。如果 \(u, v\) 异色,则 \(u, v\) 之间的任意路径长度都是奇数,一定无解。否则,由于图中存在奇数边权,可以走到这条边上一直走,调整路径长度模 \(2^{k - b}\) 的余数。每在这条边上往返一次,都会让路径长度模 \(2^{k - b}\) 的余数增加 \(2\)(这是边权长度为奇数决定的),所以往返有限次以后一定可以把路径长度调整为 \(2^{k - b}\) 的倍数。这就说明我们可以根据两点的颜色来判断是否存在合法路径。
一个小细节:任意整数都是 \(0\) 的因数,所以规定 \(\operatorname{lowbit}(0) = +\infty\),这是自然的。
一般情况
一般情况与 \(o = 3\) 的唯一区别在于连通块可能不是树,因此二分图染色不一定成功。我们沿用 \(o = 2\) 的做法:如果连通块不是二分图,说明存在奇环,因此可以不断走奇环调整路径长度模 \(2^{k}\) 的余数。设要在奇环上走 \(x\) 次,由于奇环的长度 \(s\) 为奇数,因此 \(s \perp 2^{k}\)。根据裴蜀定理, 对任意整数 \(y\),\(s \cdot x \equiv y \pmod{2^{k}}\) 一定有解,所以总是存在合法路径。
时间复杂度:预处理 \(O(n + m)\),单次询问 \(O(1)\)。
Code
#include<bits/stdc++.h>
using namespace std;
typedef long long i64;
constexpr i64 max_bit = 1LL << 62;
int n, m, o;
struct Edge {
int v; i64 w;
};
vector<vector<Edge>> G;
vector<int> comp, comp0, col, conf, vis;
// component id, color, conflict
vector<i64> g(n + 1);
// lowbit of all edges in the component
int cur;
i64 lowbit(i64 x) {
return x & -x;
}
void dfs(int u) {
comp[u] = cur;
for(auto [v, w]: G[u]) {
if(w != 0) {
i64 x = lowbit(w);
g[cur] = min(g[cur], x);
}
if(!comp[v]) {
dfs(v);
}
}
}
void dfs1(int u) {
vis[u] = 1;
for(auto [v, w]: G[u]) {
if(!vis[v]) {
col[v] = col[u] ^ (int)((w / g[cur]) & 1);
dfs1(v);
} else if(col[v] != (col[u] ^ (int)((w / g[cur]) & 1))) {
conf[cur] = 1;
}
}
}
void dfs0(int u) {
comp0[u] = cur;
for(auto [v, w]: G[u]) {
if(w == 0 && !comp0[v]) dfs0(v);
}
}
void prework() {
comp0.resize(n + 1);
for(int i = 1; i <= n; i++) {
if(!comp0[i]) cur++, dfs0(i);
}
cur = 0, comp.resize(n + 1), vis = col = conf = comp;
g.resize(n + 1, max_bit);
for(int i = 1; i <= n; i++) {
if(!comp[i]) {
cur++, dfs(i);
if(g[cur] != max_bit) {
dfs1(i);
}
}
}
}
bool query(int u, int v, i64 w) {
if(w == 0) {
return comp0[u] == comp0[v];
}
if(comp[u] != comp[v]) {
return false;
}
if(w & 1) {
return true;
}
int id = comp[u];
i64 x = lowbit(w);
if(x <= g[id] || conf[id]) {
return true;
}
return col[u] == col[v];
}
int main() {
cin.tie(nullptr) -> sync_with_stdio(false);
int q;
cin >> n >> m >> q >> o;
G.resize(n + 1);
for(int i = 1, u, v; i <= m; i++) {
i64 w;
cin >> u >> v >> w;
G[u].push_back({v, w}), G[v].push_back({u, w});
}
prework();
for(int i = 1, u, v; i <= q; i++) {
i64 w;
cin >> u >> v >> w;
cout << (query(u, v, w) ? "bougain" : "villea") << '\n';
}
return 0;
}

浙公网安备 33010602011771号