2025 寒假集训补题
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) { // 到达 i
sum += 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 { // 只用能充电的电瓶, 没到达 i
int tmp = min(sum, dis);
sum -= tmp, dis -= tmp;
if (dis == 0) { // 用不能充电的电瓶到达 i
sum += 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 表
最大数(反向 ST 表,末尾动态插入)
对一个初始为空的数列,进行 \(M\) 次操作,操作包括以下两种:
- 给定正整数 \(L\),查询当前数列中末尾 \(L\) 个数中的最大的数,保证 \(L\) 不超过当前数列的长度。
- 给定整数 \(N\),在末尾插入 \((N+T)\bmod D\),其中 \(T\) 为上一次查询操作的答案(如果还未执行过查询操作,则 \(T=0\)),\(D\) 是一个给定的常数。保证 \(N\) 在
int范围内。\(1\le M\le2\times10^5\),\(1\le D\le2\times10^9\)。
为了在插入后不影响前面建好的 ST 表,我们将建立 ST 表的过程反过来,即 st[j][i] 表示在 \((i-2^j,i]\) 范围内的最大值。这样,以新加入的数结尾的 ST 表区间永远只与其前面的数有关,并且仍然可以 \(O(\log n)\) 插入,\(O(1)\) 查询区间最大值。
#include <bits/stdc++.h>
#define f(x, y, z) for (int x = (y); x <= (z); ++x
using namespace std;
int const N = 2e5 + 10;
int m, D, a[18][N], n, lgn;
signed main() {
cin.tie(0)->sync_with_stdio(false);
cin >> m >> D;
char ch;
int x, t = 0;
while (m--) {
cin >> ch >> x;
if (ch == 'Q') {
int lg = __builtin_log2(x);
cout << (t = max(a[lg][n], a[lg][n - x + (1 << lg)])) << '\n';
} else {
a[0][++n] = (1ll * x + t) % D;
for (int j = 1; n - (1 << j) + 1 > 0; ++j)
a[j][n] = max(a[j - 1][n], a[j - 1][n - (1 << j - 1)]);
}
}
return 0;
}
*区间与除法(状态压缩,ST 表维护区间或)
给定一个长度为 \(n\) 的序列 \(a_1,a_2,\cdots,a_n\) 和 \(m\) 个「原数」\(b_1,b_2,\cdots,b_m\)。再给定一个除数 \(d\),如果 \(a_i\) 经过若干次(或者零次)除以 \(d\) 向下取整可以变成其中一个「原数」,则称 \(a_i\) 可以被这个「原数」消灭。
进行 \(q\) 次询问,每次询问给定 \(l,r\),请求出:在消灭最多个数的前提下,最少需要多少个「原数」?
\(n\le5\times10^5\),\(m\le60\),\(2\le d\le10\),\(q\le10^6\),\(0\le a_i,b_i<2^{63}\)。
首先我们先预处理出每个数能否被消灭,这个是肯定要做的,复杂度 \(O(n\log_dV)\),其中 \(V\) 为序列 \(a\) 的值域大小。
但是如何保存?最直观的想法是用一个数组记录每个数被哪个原数消灭了,而查询能否匹配上某一个原数的工作就交给 std::map。然而,一个数可以被很多个原数消灭,这总不能用 map< int, vector<int> > 来做吧。
注意到,题中的询问是:最少需要多少个原数。再注意到,如果 \(a_i\) 被一个较大的原数消灭了,并且这个原数可以被一个较小的原数消灭,那么 \(a_i\) 也可以被这个较小的原数消灭。也就是说,我们只需要保存能消灭 \(a_i\) 的最小的原数是谁,这样可以最大化地利用这个较小的原数。或者说,这样可以让一个原数去消灭尽可能多的 \(a_i\)。具体实现见代码。
注意到,\(m\le60\),所以我们可以直接用一个二进制数 \(msk_i\) 表示能消灭 \(a_i\) 的最小原数的位置。那么,每次询问 \(l,r\) 的答案就是 \(msk_l,\cdots,msk_r\) 进行按位或的结果。
最后,对于多组询问,用 ST 表维护静态区间按位或。
总复杂度 \(O(n\log_dV+n\log_2n+q)\)。
实现细节:C++ 内置函数 double __bulitin_log2(double) 和 int __builtin_popcountll(long long)(返回二进制表示中 1 的个数)。
十年
OIXCPC 一场空,不开 long long 见祖宗!!!!
#include <bits/stdc++.h>
#define f(x, y, z) for (int x = (y); x <= (z); ++x)
#define se second
using namespace std;
typedef long long ll;
int const N = 5e5 + 10;
int n, m, d, q;
ll a[N], st[19][N];
map<ll, int> mp;
signed main() {
cin.tie(0)->sync_with_stdio(false);
cin >> n >> m >> d >> q;
f(i, 1, n) cin >> a[i];
ll x;
f(i, 1, m) {
cin >> x;
if (mp.find(x) == mp.end()) mp[x] = i; // 重复的原数只需要存一个
}
f(i, 1, n) {
ll tmp = a[i];
while (tmp) {
auto it = mp.find(tmp);
if (it != mp.end()) st[0][i] = 1ll << (*it).se - 1; // 尽量用 mp.find() 而不是 mp[]
tmp /= d;
}
auto it = mp.find(tmp); // 原数可能为 0 (不过题目貌似没卡)
if (it != mp.end()) st[0][i] = 1ll << (*it).se - 1;
}
int lgn = __builtin_log2(n);
f(j, 1, lgn) for (int i = 1; i + (1 << j) - 1 <= n; ++i)
st[j][i] = st[j - 1][i] | st[j - 1][i + (1 << j - 1)];
int l, r;
while (q--) {
cin >> l >> r;
int lgd = __builtin_log2(r - l + 1);
cout << __builtin_popcountll(st[lgd][l] | st[lgd][r - (1 << lgd) + 1]) << '\n';
}
return 0;
}
国旗计划(倍增 DP,区间覆盖)
有 \(M\) 个点围成一圈,编号为 \(1\) 到 \(M\)。给定 \(N\) 个区间 \([l_i,r_i]\),表示点 \(l_i\) 沿顺时针方向至点 \(r_i\),保证任意两个区间不会互相包含。请求出:对于每一个区间,在选择该区间的情况下,一共至少需要选择几个区间,使得全部 \(M\) 个点被覆盖。
\(N\leqslant 2×10^5\),\(M<10^9\),\(1\leqslant l_i,r_i\leqslant M\)。
对于环形问题,一般的处理方法是破环成链,并在后面复制一遍,变成 \(2N\) 个区间,值域为 \([1,2M]\)。
之后首先考虑,对于一个区间,如何求出答案。既然题目保证了任意两个区间不会互相包含,那么可以直接按照左端点从小到大排序,这样右端点也是从小到大排序(否则出现 \(l_i<l_{i+1}\) 而 \(r_i>r_{i+1}\),也就是区间 \(i\) 包含区间 \(i+1\))。同时显然地,对于任意 \(i\ne j\),必有 \(l_i\ne l_j\)。那么,根据贪心的思想,可选择的下一个区间中,只有最右侧的区间是最优的,见下图(红色的为选择的区间)。
现在的问题是,如何快速求出从每个区间开始的答案。注意到,对于每一个区间 \(i\),要选择的下一个区间 \(nxt_i\) 总是固定的。那么我们处理出所有 \(nxt_i\) 之后,再想求出跳任意步后的区间,只需要用倍增法预处理一下即可。设 \(f(j,i)\) 表示从区间 \(i\) 跳 \(2^j\) 步后到达的区间,则
初值 \(f(0,i)=nxt_i\),预处理复杂度 \(O(n\log n)\)。最后,从每个区间开始往后跳,每一步跳的区间尽量多,也就是从 \(2\) 的最高次方逐个往下试,如果右端点没超出需要的终点,那么就贪心地往后跳。易知这样跳总是可行的:设最终要跳的步数的二进制表示为
其中 \(b\) 为位数,那么这样相当于是从高到低试出了所有 \(c_i=1\) 的 \(i\),最终合成了步数 \(s\)。
注意,排序后,区间顺序改变,所以需要存一下区间原来的编号,从而按原来的区间顺序输出答案。
总复杂度 \(O(n\log n)\)。
#include <bits/stdc++.h>
#define f(x, y, z) for (int x = (y); x <= (z); ++x)
#define g(x, y, z) for (int x = (y); x >= (z); --x)
using namespace std;
int const N = 2e5 + 10;
int n, m, f[19][N << 1], ans[N];
struct Interval {
int l, r, idx;
inline bool operator<(Interval const &o) const {
return l < o.l;
}
} a[N << 1];
signed main() {
cin.tie(0)->sync_with_stdio(false);
cin >> n >> m;
f(i, 1, n) {
cin >> a[i].l >> a[i].r;
a[i].idx = i;
if (a[i].r < a[i].l) a[i].r += m;
}
sort(a + 1, a + n + 1);
f(i, 1, n) {
a[i + n].l = a[i].l + m;
a[i + n].r = a[i].r + m;
a[i + n].idx = 0;
}
int t = 1;
f(i, 1, n * 2) {
while (t <= n * 2 && a[t].l <= a[i].r) ++t;
f[0][i] = --t;
}
int lgn = __builtin_log2(n * 2);
f(j, 1, lgn) f(i, 1, n * 2) f[j][i] = f[j - 1][f[j - 1][i]];
f(i, 1, n) {
int pos = i;
g(j, lgn, 0) if (f[j][pos] && a[f[j][pos]].r - a[i].l < m) {
pos = f[j][pos];
ans[a[i].idx] |= (1 << j);
}
ans[a[i].idx] += 2; // 加上初始区间和结束区间 (由于 while 循环的写法)
}
f(i, 1, n) cout << ans[i] << ' ';
return 0;
}
**星际穿越(倍增 DP,区间覆盖)
在数轴上有 \(n\) 个点,编号为 \(1\) 到 \(n\)。给定数列 \(L_i\),表示第 \(i\) 个点与编号在 \([L_i,i-1]\) 范围内的所有点之间可以花费 \(1\) 单位时间互相传送。设 \(dist(s,t)\) 表示经若干次传送从 \(s\) 到 \(t\) 花费的最少时间(即传送次数),进行 \(q\) 次询问,每次询问给定 \(l,r,x\),求出
\[\frac1{r-l+1}\sum_{y=l}^rdist(x,y), \]以既约分数
p/q的形式输出。\(n,q\le3\times10^5\)。保证每次询问中 \(l<r<x\)。
参考题解(作者 Luogu@user100566)。
防守战线(线性规划?)
战线可以看作一个长度为 \(n\) 的序列,现在需要在这个序列上建塔,有 \(m\) 个区间 \([L_1, R_1], [L_2, R_2], \cdots, [L_m, R_m]\),在第 \(i\) 个区间内至少要建 \(D_i\) 座塔。在位置 \(i\) 上建一座塔有 \(C_i\) 的花费,且一个位置可以建任意多的塔,费用累加计算。求最少花费。
对于 \(100\%\) 的数据,\(n\le 1000\),\(m\le 10000\),\(1\le L_i\le R_i\le n\),其余数据均 \(\le 10000\)。
大量的工作沟通(LCA)
一棵树,根节点为 \(0\),其他节点编号为 \(1\) 到 \(N-1\),节点 \(i\) 的父亲为 \(f_i\)(\(1\le i\le N-1\))。\(m\) 次询问,每次询问给定一些点,求这些点的公共祖先中编号最大的那个。
\(3\le N\le10^5\),\(Q\le100\),\(m\le10^4\)。
首先求出 LCA,然后在根节点到 LCA 的路径上找出编号最大的节点,可以用树上前缀最大值来求。
对于 LCA,可以用 DFS 序求 LCA 这种方法。设节点 \(i\) 的 DFS 序编号为 \(dfn_i\),且 \(dfn_x<dfn_y\),则 \(lca(x,y)\) 为 DFS 序上介于 \(x+1\) 与 \(y\) 之间的所有节点中深度最小的节点的父亲,即
具体实现上,我们可以维护一个 ST 表,里面存的值是节点的父亲,而比较规则是父亲 DFS 序的大小。
LCA 的时间复杂度:\(O(N\log N)\) 预处理,\(O(1)\) 询问。
本题时间复杂度:\(O(N\log N+Qm)\)。
本题将节点的下标范围从 \(0\) 到 \(N-1\) 平移到了 \(1\) 到 \(N\),不然会出错。
#include <bits/stdc++.h>
#define f(x, y, z) for (int x = (y); x <= (z); ++x)
using namespace std;
int const N = 1e5 + 10, LGN = 19;
int n, lgn, q, mx[N];
vector<int> edge[N];
int dfn[N], mn[LGN][N], tot;
void dfs(int u, int fa) {
mn[0][dfn[u] = ++tot] = fa;
mx[u] = max(u, mx[fa]);
for (int v: edge[u]) if (v ^ fa) dfs(v, u);
return;
}
inline int get(int x, int y) { return dfn[x] < dfn[y] ? x : y; }
int lca(int u, int v) {
if (u == v) return u;
u = dfn[u], v = dfn[v];
if (u > v) swap(u, v);
int lgd = __builtin_log2(v - u);
return get(mn[lgd][u + 1], mn[lgd][v - (1 << lgd) + 1]);
}
signed main() {
cin.tie(0)->sync_with_stdio(false);
cin >> n;
lgn = __builtin_log2(n);
f(i, 2, n) {
int fa; cin >> fa;
++fa;
edge[fa].push_back(i);
edge[i].push_back(fa);
}
dfs(1, 0);
f(j, 1, lgn) for (int i = 1; i + (1 << j) - 1 <= n; ++i)
mn[j][i] = get(mn[j - 1][i], mn[j - 1][i + (1 << j - 1)]);
cin >> q;
int m, l, x;
while (q--) {
cin >> m >> l;
++l;
f(i, 2, m) {
cin >> x;
++x;
l = lca(l, x);
}
cout << mx[l] - 1 << '\n';
}
return 0;
}
Minimum spanning tree for each edge(最小生成树,倍增求 LCA)
给定一个 \(n\) 个点 \(m\) 条边的带权无向图,第 \(i\) 条边为将节点 \(u_i\) 和 \(v_i\) 相连,权值为 \(w_i\)。对于所有 \(1\le i\le m\),求出包括第 \(i\) 条边的生成树的最小边权和。
\(1\le n\le2\cdot10^5\),\(n-1\le m\le2\cdot10^5\)。\(1\le u_i,v_i\le n\),\(u_i\ne v_i\),\(1\le w_i\le10^9\)。
首先考虑,已知图的最小生成树,如何修改,使之包含原本不在其中的边 \((u_i,v_i)\)。显然,加入边 \((u_i,v_i)\) 之后会形成一个环,那么将环中的任意一条边删去,就又变回了一棵树。为了让新的树的边权和最小,我们自然要删去最大的边。设最小生成树的边权和为 \(S\),最小生成树上 \(u_i\) 到 \(v_i\) 的路径上的最大边为 \(mx(u_i,v_i)\),则答案为
如果边原本就在最小生成树上,那么答案就是 \(S\)。
如何在建出来的最小生成树上求 \(mx(x,y)\) 呢?用倍增求 LCA,预处理从节点 \(i\) 向上跳 \(2^j\) 步到达的祖先 \(f(i,j)\),同时处理出向上跳 \(2^j\) 步所经历的边中的最大值 \(g(i,j)\),这样在求 LCA 的同时也可以求出路径上的最大边。
时间 \(O(m\log m+n\log n+(m-n)\log n)=O(m\log m)\)。
#include <bits/stdc++.h>
#define f(x, y, z) for (int x = (y); x <= (z); ++x)
#define g(x, y, z) for (int x = (y); x >= (z); --x)
#define fi first
#define se second
using namespace std;
typedef long long ll;
typedef pair<int, int> pii;
int const N = 2e5 + 10, LGN = 20;
int n, m, lgn;
ll ans[N];
vector<pii> e[N];
struct Edge {
int u, v, w, id;
inline void add() {
e[u].push_back(make_pair(v, w));
e[v].push_back(make_pair(u, w));
return;
}
} edge[N];
struct DSU {
int fa[N];
void init() { f(i, 1, n) fa[i] = i; }
int getfa(int x) { return x == fa[x] ? x : (fa[x] = getfa(fa[x])); }
bool merge(int x, int y) {
int fx = getfa(x), fy = getfa(y);
if (fx == fy) return 0;
fa[fx] = fy;
return 1;
}
} dsu;
bool in[N];
ll Kruskal() {
int cnt = 0;
ll sum = 0;
sort(edge + 1, edge + m + 1, [](Edge const &p, Edge const &q) { return p.w < q.w; });
dsu.init();
f(i, 1, m) {
if (dsu.merge(edge[i].u, edge[i].v)) {
sum += edge[i].w;
edge[i].add();
in[i] = true;
if (++cnt == n - 1) break;
}
}
return sum;
}
int dep[N], f[LGN][N], g[LGN][N];
void dfs(int u, int fa, int from) {
dep[u] = dep[fa] + 1;
f[0][u] = fa;
g[0][u] = from;
f(j, 1, lgn) {
f[j][u] = f[j - 1][f[j - 1][u]];
g[j][u] = max(g[j - 1][u], g[j - 1][f[j - 1][u]]);
}
for (auto v: e[u]) if (v.fi ^ fa)
dfs(v.fi, u, v.se);
return;
}
int path_max(int x, int y) {
if (x == y) return 0;
if (dep[x] < dep[y]) swap(x, y);
int mx = 0;
g(j, lgn, 0) if (dep[f[j][x]] >= dep[y]) {
mx = max(mx, g[j][x]);
x = f[j][x];
}
if (x == y) return mx;
g(j, lgn, 0) if (f[j][x] ^ f[j][y]) {
mx = max({mx, g[j][x], g[j][y]});
x = f[j][x], y = f[j][y];
}
return max({mx, g[0][x], g[0][y]});
}
signed main() {
cin.tie(0)->sync_with_stdio(false);
cin >> n >> m;
lgn = __lg(n);
int u, v, w;
f(i, 1, m) {
cin >> u >> v >> w;
edge[i] = Edge{u, v, w, i};
}
ll S = Kruskal();
dfs(1, 0, 0);
f(i, 1, m) {
if (in[i]) ans[edge[i].id] = S;
else ans[edge[i].id] = S - path_max(edge[i].u, edge[i].v) + edge[i].w;
}
f(i, 1, m) cout << ans[i] << '\n';
return 0;
}
0120 树状数组与线段树
0121 分块与莫队
回放 P1(密码:nn7n) | 回放 P2(密码:nn7n) | 题单

浙公网安备 33010602011771号