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\),那么有:

\[(a_1,a_2,a_3,a_4)\to(a_2-a_1,a_3-a_1,a_4-a_1)\to(a_3-a_2,a_4-a_2)\to(a_4-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 的个数)。

十年 OI XCPC 一场空,不开 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\)。那么,根据贪心的思想,可选择的下一个区间中,只有最右侧的区间是最优的,见下图(红色的为选择的区间)。

image-20250328200504469

现在的问题是,如何快速求出从每个区间开始的答案。注意到,对于每一个区间 \(i\),要选择的下一个区间 \(nxt_i\) 总是固定的。那么我们处理出所有 \(nxt_i\) 之后,再想求出跳任意步后的区间,只需要用倍增法预处理一下即可。设 \(f(j,i)\) 表示从区间 \(i\)\(2^j\) 步后到达的区间,则

\[f(j,i)=f(j-1,f(j-1,i)), \]

初值 \(f(0,i)=nxt_i\),预处理复杂度 \(O(n\log n)\)。最后,从每个区间开始往后跳,每一步跳的区间尽量多,也就是从 \(2\) 的最高次方逐个往下试,如果右端点没超出需要的终点,那么就贪心地往后跳。易知这样跳总是可行的:设最终要跳的步数的二进制表示为

\[s=\sum_{i=0}^{b-1}2^ic_i,\quad c_i\in\{0,1\}, \]

其中 \(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\) 之间的所有节点中深度最小的节点的父亲,即

\[lca(x,y)=father\left(\mathop{\operatorname{argmin}}_{i=dfn_x+1}^{dfn_y}\{dfn_i\}\right). \]

具体实现上,我们可以维护一个 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(u_i,v_i)+w_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) | 题单

0122 最终测试

比赛链接

posted @ 2025-02-23 23:49  f2021ljh  阅读(28)  评论(0)    收藏  举报