ACM@HIT 2025 寒假集训
0109 简单数据结构
回放 | 题单
New Energy Vehicle(贪心,堆)
有一辆含 \(n\) 个电瓶的车,第 \(i\) 个电瓶的容量 \(a_i\),耗 \(1\) 单位任意电瓶的电力前进 \(1\)(只能向前),有 \(m\) 个充电站,第 \(j\) 个充电站位于 \(x_j\),可以给电瓶 \(t_j\) 充满电。求初始电瓶满的情况下最远可以行驶多远。
数据组数 \(1\le T\le10^4\)。\(1\le n,m\le10^5\),\(\sum n,\sum m\le2\cdot10^5\)。\(1\le a_i\le10^9\),\(1\le x_j,t_j\le10^9\),保证 \(x_i\) 两两不等,且按从小到大顺序给出。
有的电瓶可以充电,而有的电瓶不可以。为了最大化被利用的电力,我们应该优先使用可充电的电瓶,并且优先使用距离其充电站最近的电瓶。用堆(优先队列 std::priority_queue
)维护这一贪心过程。
将充电站的编号作为元素加入堆,按位置从近到远排序。每种电瓶只需加入最近的充电站。
遍历所有充电站,假设当前要从充电站 \(i-1\) 到达充电站 \(i\),则当前要走的距离为 \(d=x_i-x_{i-1}\)(令 \(x_0=0\))。不断取堆顶的充电站对应的电瓶中的电力,直到到达 \(i\),即 \(d\) 减到 \(0\),或者直到堆中的电力已被用完,这时开始用不可充电的电瓶中的电力。如果过程中所有电力都被耗尽,则输出答案,否则 \(d\) 减到 \(0\),充电站 \(i\) 将电瓶 \(t_i\) 充满电,于是将 \(t_i\) 的下一个充电站加入堆,继续前往下一个充电站 \(i+1\)。
最后,到达最后一个充电站 \(m\),则把所有电瓶中的剩余电量耗尽,结束行驶,输出答案。
#include <bits/stdc++.h>
#define int long long
#define f(x, y, z) for (int x = (y); x <= (z); ++x)
using namespace std;
int const N = 1e5 + 10;
int tt, n, m, a[N], c[N], nxt[N], sum;
struct node {int x, t;
} b[N];
vector<int> v[N];struct cmp {bool operator()(int const &p, int const &q) {return b[p].x > b[q].x;}
};
priority_queue<int, vector<int>, cmp> q;void solve() {sum = 0;f(i, 1, n) v[i].clear();f(i, 1, m) nxt[i] = 0;while (!q.empty()) q.pop();cin >> n >> m;f(i, 1, n) cin >> a[i], c[i] = a[i], sum += a[i]; // sum: 所有电池的总电量, 动态变化f(i, 1, m) cin >> b[i].x >> b[i].t, v[b[i].t].push_back(i);f(i, 1, n) {f(j, 0, (int)v[i].size() - 2) {nxt[v[i][j]] = v[i][j + 1]; // nxt[i]: 下一个与充电站 i 所充电瓶相同的充电站}if (v[i].size()) q.push(v[i][0]); // 将充电站编号加入堆}f(i, 1, m) {int dis = b[i].x - b[i - 1].x;int tp = 0;while (dis && !q.empty()) {tp = q.top(); q.pop();int tmp = min(c[b[tp].t], dis);c[b[tp].t] -= tmp, dis -= tmp, sum -= tmp;}if (dis == 0) { // 到达 isum += a[b[i].t] - c[b[i].t], c[b[i].t] = a[b[i].t];if (nxt[i]) q.push(nxt[i]); // 如果当前电池充完电后, 后面还有充电站能给它充电if (b[tp].t != b[i].t && c[b[tp].t] != 0) q.push(tp); // 如果最后用的电瓶中还有电, 将其放回堆中} else { // 只用能充电的电瓶, 没到达 iint tmp = min(sum, dis);sum -= tmp, dis -= tmp;if (dis == 0) { // 用不能充电的电瓶到达 isum += a[b[i].t] - c[b[i].t], c[b[i].t] = a[b[i].t];if (nxt[i]) q.push(nxt[i]);} else { // 电量耗尽, 结束行驶cout << b[i].x - dis << '\n';return;}}}cout << b[m].x + sum << '\n';return;
}signed main() {cin.tie(0)->sync_with_stdio(false);cin >> tt;while (tt--) solve();return 0;
}
Make Max(笛卡尔树)
给定长度为 \(n\) 的序列 \(a_1,\cdots,a_n\),对其进行操作如下:
- 选定一个连续子序列 \(a_l,\cdots,a_r\)(\(1\le l<r\le n\)),其中的数不都相同(即存在 \(i,j\in[l,r]\) 满足 \(i\ne j\) 且 \(a_i\ne a_j\)),将其中所有数变成 \(\max_{l\le i\le r}\{a_i\}\)。
问最多可以进行上述操作多少次。
数据组数 \(1\le t\le1000\)。\(1\le n\le2\cdot10^5\),\(\sum n\le4\cdot10^5\)。\(1\le a_i\le10^9\)。
Valiant's New Map(二分答案,二维前缀和)
给定一个 \(n\times m\) 的矩阵 \(A=(a_{ij})\),找到最大的 \(l\),使得存在一个 \(l\times l\) 的子块,其中的元素都不小于 \(l\)。
数据组数 \(1\le t\le10^3\)。\(1\le n\le m\),\(1\le\sum(n\cdot m)\le10^6\)。\(1\le a_{ij}\le10^6\)。
二分答案,假设二分的答案为 \(x\),验证是否存在一个边长等于 \(x\) 的方子块,使得其中的元素都大于等于 \(x\)。
为了快速判断以 \((i,j)\) 为左上角的边长为 \(x\) 的方子块,是否满足其中元素均大于等于 \(x\),建立辅助矩阵 \(B=(b_{ij})_{n\times m}\),其中 b[i][j] = a[i][j] >= x ? 0 : 1
。那么如果 \(\sum\limits_{p=i}^{i+x}\sum\limits_{q=j}^{j+x}b_{pq}=0\),则说明以 \((i,j)\) 为左上角的边长为 \(x\) 的方子块满足要求,\(O(nm)\) 枚举即可。总复杂度 \(O(nm\log n)\)。
inline int sum(const vector<vector<int> > & a, int x1, int y1, int x2, int y2) {return a[x2][y2] - a[x1 - 1][y2] - a[x2][y1 - 1] + a[x1 - 1][y1 - 1];
}bool check(int x, const vector<vector<int> > & a) {vector<vector<int> > b(n + 1, vector<int>(m + 1, 0));f(i, 1, n) f(j, 1, m) b[i][j] = (a[i][j] < x);f(i, 1, n) f(j, 1, m) b[i][j] = b[i][j] + b[i - 1][j] + b[i][j - 1] - b[i - 1][j - 1];f(i, 1, n - x + 1) f(j, 1, m - x + 1)if (sum(b, i, j, i + x - 1, j + x - 1) == 0) return true;return false;
}void solve() {cin >> n >> m;vector<vector<int> > a(n + 1, vector<int>(m + 1, 0));f(i, 1, n) f(j, 1, m) cin >> a[i][j];int l = 0, r = n + 1, mid;while (l + 1 < r) {mid = l + r >> 1;if (check(mid, a)) l = mid;else r = mid;}cout << l << '\n';return;
}
Substract Operation(结论题,std::set
)
给定两个数 \(n,k\) 和 \(n\) 个数 \(a_1,a_2,\cdots,a_n\)。一次操作是指,从数列中选出一个数 \(x\),将其删去,剩余的数分别减去 \(x\)。问能否通过 \(n-1\) 次操作,使最后剩下的一个数恰好为 \(k\)。如果可以,输出
YES
(大小写不限)。数据组数 \(1\le t\le10^4\)。\(2\le n\le2\cdot10^5\),\(2\le\sum n\le2\cdot10^5\)。\(-10^9\le a_i\le10^9\),\(1\le k\le10^9\)。
顺序不重要。假设 \(n=4\),按顺序删去 \(a_1,a_2,a_3\),那么有:
也就是说,最后剩下的一定为两个数的差。那么只需要检查是否存在两个数的差等于 \(k\) 即可。利用 std::set
,\(\forall i\) 检查数列中是否存在 \(a_i+k\) 即可。时间 \(O(n\log n)\)。由于 std::unordered_set
一次操作的最坏时间复杂度为 \(O(n)\),对于此题数据会超时。。。
void solve() {cin >> n >> k;set<int> s;vector<int> a;f(i, 1, n) {int x; cin >> x;a.push_back(x);s.insert(x);}for (int x: a) if (s.find(x + k) != s.end())return void(cout << "YES\n");cout << "NO\n";return;
}
Closest Pair(结论题)
给定 \(n\) 个二元组 \((x_i,w_i)\),其中 \(x_i\) 严格单调递增。给出 \(q\) 次询问,每次询问给出 \(l,r\)(\(1\le l<r\le n\)),求
\[\min_{l\le i<j\le r}\{|x_i-x_j|\cdot(w_i+w_j)\}. \]\(2\le n\le3\cdot10^5\),\(1\le q\le3\cdot10^5\)。\(|x_i|\le10^9\),\(1\le w_i\le10^9\)。
0110 简单 STL
回放 | 题单
关押罪犯(扩展域并查集)
有 \(N\) 名罪犯要被关押进两座监狱,其中有 \(M\) 对罪犯之间有仇,假设第 \(i\) 对有仇的罪犯为 \(\{a_i,b_i\}\),他们之间的怨气值为 \(c_i\),那么如果他们被关押在同一座监狱,则会爆发影响力为 \(c_i\) 的冲突事件。问如何分配罪犯到这两座监狱,使得冲突事件的影响力的最大值最小。
\(N\le2\cdot10^4\),\(M\le10^5\)。\(0<c_i\le10^9\),\(1\le a_i<b_i\le N\)。
根据贪心的思想,尽量将冲突影响力最大的罪犯放到不同的监狱。首先将冲突事件按影响力从大到小排序,然后将事件的双方试着分别放到不同的监狱。如果在此之前的事件已经将二者放到同一监狱,那么就只能以当前事件为影响力最大的事件了。
「放到监狱」的操作用扩展域并查集来实现。由于一个罪犯一定属于两个监狱中的一个,这符合「敌人的敌人是朋友」的思想。
#include <bits/stdc++.h>
#define f(x, y, z) for (int x = (y); x <= (z); ++x)
using namespace std;
int const N = 2e4 + 10, M = 1e5 + 10;
int n, m;int fa[N << 1];
int getfa(int x) { return x == fa[x] ? x : (fa[x] = getfa(fa[x])); }
void Merge(int x, int y) {int f1 = getfa(x), f2 = getfa(y);if (f1 ^ f2) fa[f1] = f2;return;
}struct Node {int a, b, c;inline bool operator<(Node const & o) const {return c > o.c;}
} s[M];signed main() {cin.tie(0)->sync_with_stdio(false);cin >> n >> m;f(i, 1, n) fa[i] = i, fa[i + n] = i + n;f(i, 1, m) cin >> s[i].a >> s[i].b >> s[i].c;sort(s + 1, s + m + 1);f(i, 1, m) {int f1 = getfa(s[i].a), f2 = getfa(s[i].b);if (f1 == f2) return cout << s[i].c << '\n', 0;else Merge(s[i].a, s[i].b + n), Merge(s[i].b, s[i].a + n);}cout << "0\n"; //没有冲突事件发生return 0;
}
黑匣子(对顶堆)
给定长度为 \(n\) 的元素序列 \(a_1,\cdots,a_n\) 和长度为 \(m\) 的下标序列 \(u_1,\cdots,u_m\)。另外有一个初始为空的集合,对于每个 \(1\le i\le m\),将 \(a_{u_{i-1}},\cdots,a_{u_i}\) 加入集合后(规定 \(u_0=1\)),输出集合中第 \(i\) 大的数(即集合中的元素从小到大排序后的第 \(i\) 个元素)。
\(1\le n,m\le2\cdot10^5\)。\(|a_i|\le2\cdot10^9\),\(1\le u_1\le\dots\le u_m\le n\)。
动态维护集合中第 \(i\) 大的数,采用对顶堆的方法实现。维护一个小根堆和一个大根堆,其中:
- 小根堆中的元素 >= 小根堆的根 >= 大根堆的根 >= 大根堆中的元素。
- 大根堆中有 \(i-1\) 个元素,两堆中一共有 \(u_i\) 个元素。这时,小根堆的根即为第 \(i\) 大的元素。
对于每个 \(1\le i\le m\),不断向两堆中加入元素,直到元素个数达到 \(u_i\)。先将元素加入大根堆,当大根堆中元素达到 \(i\) 时,将大根堆的堆顶放到小根堆中。
设 \(n,m\) 同阶,时间复杂度 \(O(n\log n)\)。
#include <bits/stdc++.h>
using namespace std;
int const N = 2e5 + 10;
int n, m, a[N];priority_queue<int> qmx; // 大根堆
priority_queue<int, vector<int>, greater<int> > qmn; // 小根堆signed main() {cin.tie(0)->sync_with_stdio(false);cin >> n >> m;for (int i = 1; i <= n; ++i) cin >> a[i];for (int i = 1, t = 0, u; i <= m; ++i) {cin >> u;while (t < u) {qmx.push(a[++t]);if (qmx.size() == i) qmn.push(qmx.top()), qmx.pop(); // 一旦大根堆中达到 i 个元素, 就移到小根堆中一个, 并且保证小根堆的根 (min) >= 大根堆的根 (max)}cout << qmn.top() << '\n'; // 此时大根堆中有 i-1 个元素, 小根堆的堆顶是第 i 大的元素, 两个堆中共有 u 个元素qmx.push(qmn.top()), qmn.pop(); // 保证下一轮大根堆中有 (++i)-1 个元素}return 0;
}
0113 最小生成树
回放 | 题单
0114 最短路
回放 | 题单
0116 Tarjan
回放 | 题单
0117 倍增与 ST 表
回放 | 题单
0120 树状数组与线段树
回放 | 题单
0121 分块与莫队
回放 P1(密码:nn7n) | 回放 P2(密码:nn7n) | 题单
0122 最终测试
比赛链接