JOISC2019

A. Examination

可以直接套三维偏序板子,也可以放在平面上数。如果 \(x + y > z\),本身就是二维偏序。否则只需要容斥一下,变成 \(\#(s > z) - \#(s > z \land a < x) - \#(s > z \land b < y)\),而这三个都是不超过二维的偏序,离线二维数点即可。

代码(三维偏序)
#include <iostream>
#include <algorithm>
#define int long long
#define lowbit(x) ((x) & (-(x)))
using namespace std;
int n, q;
struct BIT {
    int bit[500005];
    void add(int x, int y = 1) { for (; x; x -= lowbit(x)) bit[x] += y; }
    int query(int x) {
        int ret = 0;
        for (; x <= 500000; x += lowbit(x)) ret += bit[x];
        return ret;
    }
} bit;
int d[500005], dcnt;
struct X {
    int x, y, z, id;
} qs[500005];
int qcnt;
int ans[500005];
void Solve(int l, int r) {
    if (l == r) return;
    int mid = (l + r) >> 1;
    Solve(l, mid), Solve(mid + 1, r);
    sort(qs + l, qs + mid + 1, [](X x, X y) { return x.x > y.x; });
    sort(qs + mid + 1, qs + r + 1, [](X x, X y) { return x.x > y.x; });
    int j = l;
    for (int i = mid + 1; i <= r; i++) {
        if (qs[i].id < 0) continue;
        while (j <= mid && qs[j].x >= qs[i].x) (qs[j].id != -1 ? void() : bit.add(qs[j].y)), ++j;
        ans[qs[i].id] += bit.query(qs[i].y);
    }
    for (int i = l; i < j; i++) qs[i].id == -1 ? bit.add(qs[i].y, -1) : void();
}
signed main() {
    cin >> n >> q;
    for (int i = 1; i <= n; i++) cin >> qs[i].x >> qs[i].y, qs[i].z = qs[i].x + qs[i].y, d[++dcnt] = qs[i].y, qs[i].id = -1;
    for (int i = 1; i <= q; i++) cin >> qs[n + i].x >> qs[n + i].y >> qs[n + i].z, d[++dcnt] = qs[n + i].y, qs[n + i].id = i; qcnt = n + q;
    sort(d + 1, d + dcnt + 1);
    dcnt = unique(d + 1, d + dcnt + 1) - d - 1;
    for (int i = 1; i <= qcnt; i++) qs[i].y = lower_bound(d + 1, d + dcnt + 1, qs[i].y) - d;
    sort(qs + 1, qs + qcnt + 1, [](X x, X y) { return x.z == y.z ? (x.x == y.x ? (x.y == y.y ? x.id < y.id : x.y > y.y) : (x.x > y.x)) : (x.z > y.z); });
    Solve(1, qcnt);
    for (int i = 1; i <= q; i++) cout << ans[i] << "\n";
    return 0;
}

B. Meetings

考虑先钦定一个根 \(r\),然后随机选一个点 \(x\),把所有 \(y \neq r, \neq x\) 都问一遍,如果问出来 \(y\),那么说明 \(y\)\(x\)\(r\) 的路径上。否则可以知道它在 \(x\) 到根路径上哪个点的子树内。将所有点挂在它所在的子树内,对每个子树分别递归做这件事即可。确定到根路径上点的顺序是容易的,因为询问根和两个点即可知道这两个点哪个到根距离更小。因此直接排序即可。

然后可以证明若每次选的点是随机的,那么这个做法就是对的。反正我不会证。有一篇题解说这个相当于随机选根做点分治,我不知道这有没有道理。

代码
#include <iostream>
#include <algorithm>
#include <random>
#include <vector>
#include "meetings.h"
using namespace std;
random_device rd;
mt19937 mtrand(rd());
vector<pair<int, int> > E;
vector<int> V[2005];
void Solve(vector<int> vec, int X) {
    if (!vec.size()) return;
    V[X].clear();
    int m = vec.size();
    int id = mtrand() % m, x = vec[id];
    vector<int> tmp;
    for (int i = 0; i < m; i++) {
        if (vec[i] != x) {
            int t = Query(vec[i], x, X);
            if (t == vec[i]) tmp.emplace_back(t);
            else V[t].emplace_back(vec[i]);
        }
    }
    sort(tmp.begin(), tmp.end(), [X](int p, int q) { return Query(X, p, q) == p; });
    if (tmp.size()) {
        E.emplace_back(X, tmp[0]), E.emplace_back(tmp.back(), x);
        for (int i = 0; i < (int)tmp.size() - 1; i++) E.emplace_back(tmp[i], tmp[i + 1]);
    } else E.emplace_back(x, X);
    Solve(V[x], x); Solve(V[X], X);
    for (int i = 0; i < (int)tmp.size(); i++) Solve(V[tmp[i]], tmp[i]);
}
void Solve(int N) {
    vector<int> vec;
    for (int i = 1; i < N; i++) vec.emplace_back(i);
    Solve(vec, 0);
    for (auto v : E) Bridge(min(v.first, v.second), max(v.first, v.second));
}

C. Naan

从第一段开始考虑,我们考虑把每一段分给哪个人。如果我们要把第一段分给 \(x\),那么 \(x\) 会要求第一段划分的长度不能小于某个值。设为 \(p_{x, 1}\)。那么根据贪心的原则,我们肯定是找那个 \(p_{x, 1}\) 最小的 \(x\),把第一段划这么长然后分给他。这个时候再考虑第二段,我们也是贪心地做这个事,找到 \(p_{y, 2}\) 最小的 \(y\),然后把第二段划这么长分给他。那么这个时候,如果考察第二段划分的合法性,会发现由于 \(p_{y, 1} > p_{x, 1}\),因此我们分给 \(y\) 的这一段一定是多于他要求的的。于是这一段合法。而对于接下来的每一段我们都做这个事,那么根据刚才的证明,每一段都会是合法的。于是我们就找到了一组合法的划分和分配方案。输出即可。

代码
#include <iostream>
#define int long long
using namespace std;
int n, m;
struct Frac {
    int a, b;
    Frac(int x = 0, int y = 1) { a = x, b = y; } 
} p[2005][2005], A1[2005];
int a[2005][2005], pre[2005][2005];
bool h[2005];
bool operator<(Frac x, Frac y) { return (__int128)x.a * y.b < (__int128)x.b * y.a; }
int A2[2005];
signed main() {
    cin >> n >> m;
    for (int i = 1; i <= n; i++) {
        int s = 0;
        for (int j = 1; j <= m; j++) cin >> a[i][j], s += a[i][j], pre[i][j] = s;
        for (int j = 1, k = 0; j <= n; j++) {
            while (k < m && pre[i][k + 1] * n <= s * j) ++k;
            p[i][j] = Frac(j * s - n * pre[i][k] + k * n * a[i][k + 1], n * a[i][k + 1]);
        }
    }
    for (int i = 1; i <= n; i++) {
        int mxp = 0;
        for (int j = 1; j <= n; j++) {
            if (!h[j] && (mxp == 0 || p[j][i] < p[mxp][i])) 
                mxp = j;
        }
        A1[i] = p[mxp][i]; A2[i] = mxp;
        h[mxp] = 1;
    }
    for (int i = 1; i < n; i++) cout << A1[i].a << " " << A1[i].b << "\n";
    for (int i = 1; i <= n; i++) cout << A2[i] << " ";
    cout << "\n";
    return 0;
}

D. Two Antennas

扫描线枚举右端点,维护每个左端点答案。接下来只考虑 \(i < j\) 的天线对的贡献。对于每个左端点 \(i\),将其挂在 \(i + l_i\) 加入,在 \(i + r_i + 1\) 删除。于是对于每个右端点可以知道它有哪些左端点是合法的。那么接下来要拿当前右端点 \(j\)\([j - r_j, j - l_j]\) 这一段的左端点贡献。注意只能给当前还存活的左端点贡献。只需要打一个 tag 上去即可。而线段树上的维护,考虑每个点维护 \(mx, mn, mxtg, mntg, ans\) 分别表示区间存活的左端点的最大和最小,以及打上去覆盖着这一段的右端点的最大和最小,以及答案。要注意的是打上去的右端点必须以懒标记的形式存下来,之后访问到这个区间时必须 pushdown 并清空标记,而且只能给区间内有左端点的区间打标记。pushup 的时候只需要把左右儿子的答案拿上来,不能把 \(mxtg, mntg\) 拿上来。而对于加入删除左端点,只需要加入时把 \(mx, mn\) 变成 \(h_i\),删除时再变回 \(0, +\infty\) 即可。\(ans\) 不能动,因为之前的右端点给出的贡献依旧存在。然后查询只需要区间求 \(\max\) 即可。

代码
#include <iostream>
#include <algorithm>
#include <vector>
#define int long long
using namespace std;
const int inf = 0x3f3f3f3f3f3f3f3f;
int n, q;
int a[200005], L[200005], R[200005];
vector<pair<int, int> > vec[400005];
int ans[200005];
struct Segment_Tree {
    int mx[800005], mn[800005], ans[800005];
    int mxtg[800005], mntg[800005];
    void tag(int o, int l, int r) {
        mntg[o] = min(mntg[o], l), mxtg[o] = max(mxtg[o], r);
        ans[o] = max({ ans[o], mxtg[o] - mn[o], mx[o] - mntg[o] });
    }
    void pushdown(int o) {
        if (!mxtg[o]) return;
        if (mx[o << 1]) tag(o << 1, mntg[o], mxtg[o]);
        if (mx[o << 1 | 1]) tag(o << 1 | 1, mntg[o], mxtg[o]);
        mxtg[o] = 0, mntg[o] = inf;
    }
    void pushup(int o) {
        mx[o] = max(mx[o << 1], mx[o << 1 | 1]), mn[o] = min(mn[o << 1], mn[o << 1 | 1]);
        ans[o] = max(ans[o << 1], ans[o << 1 | 1]);
    }
    void Build(int o, int l, int r) {
        mntg[o] = mn[o] = inf;
        ans[o] = -1;
        if (l == r) return;
        int mid = (l + r) >> 1;
        Build(o << 1, l, mid);
        Build(o << 1 | 1, mid + 1, r);
    }
    void Opt(int o, int l, int r, int x) {
        if (l == r) {
            if (mx[o]) mx[o] = 0, mn[o] = inf;
            else mx[o] = mn[o] = a[x];
            return;
        }
        pushdown(o);
        int mid = (l + r) >> 1;
        if (x <= mid) Opt(o << 1, l, mid, x);
        else Opt(o << 1 | 1, mid + 1, r, x);
        pushup(o);
    }
    void Add(int o, int l, int r, int L, int R, int v) {
        if (L <= l && r <= R) return mx[o] ? tag(o, v, v) : void();
        pushdown(o);
        int mid = (l + r) >> 1;
        if (L <= mid) Add(o << 1, l, mid, L, R, v);
        if (R > mid) Add(o << 1 | 1, mid + 1, r, L, R, v);
        pushup(o);
    }
    int Query(int o, int l, int r, int L, int R) {
        if (L <= l && r <= R) return ans[o];
        pushdown(o);
        int mid = (l + r) >> 1;
        if (R <= mid) return Query(o << 1, l, mid, L, R);
        if (L > mid) return Query(o << 1 | 1, mid + 1, r, L, R);
        return max(Query(o << 1, l, mid, L, R), Query(o << 1 | 1, mid + 1, r, L, R));
    }
} seg;
signed main() {
    cin >> n;
    for (int i = 1; i <= n; i++) {
        cin >> a[i] >> L[i] >> R[i];
        vec[i + L[i]].emplace_back(i, 0);
        vec[i + R[i] + 1].emplace_back(i, 0);
    }
    cin >> q;
    for (int i = 1; i <= q; i++) {
        int l, r;
        cin >> l >> r;
        vec[r].emplace_back(l, i);
    }
    seg.Build(1, 1,  n);
    for (int i = 1; i <= n; i++) {
        for (auto v : vec[i]) if (v.second == 0) seg.Opt(1, 1, n, v.first);
        if (i - L[i] >= 1) seg.Add(1, 1, n, max(1ll, i - R[i]), i - L[i], a[i]);
        for (auto v : vec[i]) if (v.second != 0) ans[v.second] = seg.Query(1, 1, n, v.first, i);
    }
    for (int i = 1;i <= q; i++) cout << ans[i] << "\n";
    return 0;
}

E. Two Dishes

怎么二见还是不会,流泪了 /ll

在网格图上,我们有两种贡献:一种是走到一行时列号不能超过某个值,这体现为每一行的一个前缀是一条收益线段;一种是走到一列时行号不能超过某个值,这体现为每一列的一个前缀是一条收益线段。但是对着这两种贡献不好同时搞,我们考虑统一两种限制。如果在图上画一画就发现我们可以容易地把其中一种贡献变成另一种,通过把收益取负并在一开始把收益加入答案。于是接下来就很好 dp 了,按行做,只维护 dp 的差分上非 \(0\) 的位置,每经过一个收益相当于把一个位置单点修改,然后一直往后合并直到合并的这些东西的和变成正数。map 维护,总复杂度 \(\mathcal{O}(n \log n)\)

代码
#include <iostream>
#include <algorithm>
#include <vector>
#include <set>
#define int long long
using namespace std;
const int inf = 0x3f3f3f3f3f3f3f3f;
int n, m, ans;
vector<pair<int, int> > vec[1000005];
set<pair<int, int> > st;
int a[1000005], b[1000005], s[1000005], t[1000005], p[1000005], q[1000005];
signed main() {
    cin >> n >> m;
    for (int i = 1; i <= n; i++) cin >> a[i] >> s[i] >> p[i], a[i] += a[i - 1];
    for (int i = 1; i <= m; i++) cin >> b[i] >> t[i] >> q[i], b[i] += b[i - 1];
    for (int i = 1; i <= n; i++) {
        if (s[i] >= a[i]) {
            ans += p[i];
            int x = upper_bound(b + 1, b + m + 1, s[i] - a[i]) - b;
            if (x <= m) 
                vec[i].emplace_back(make_pair(-p[i], x));
        }
    }
    for (int i = 1; i <= m; i++) {
        if (t[i] >= b[i]) {
            int x = upper_bound(a + 1, a + n + 1, t[i] - b[i]) - a;
            if (x <= n) 
                vec[x].emplace_back(make_pair(q[i], i));
            else 
                ans += q[i];
        }
    }
    st.insert(make_pair(m + 1, inf));
    for (int x = 1; x <= n; x++) {
        sort(vec[x].begin(), vec[x].end(), greater<pair<int, int> >());
        for (auto v : vec[x]) {
            if (v.first >= 0) {
                pair<int, int> p = *st.lower_bound(make_pair(v.second, -inf));
                if (p.first == v.second) {
                    st.erase(p);
                    p.second += v.first;
                    st.insert(p);
                } else 
                    st.insert(make_pair(v.second, v.first));
            } else {
                while (v.first < 0) {
                    pair<int, int> p = *st.lower_bound(make_pair(v.second, -inf));
                    v.first += p.second;
                    v.second = p.first;
                    st.erase(p);
                }
                st.insert(make_pair(v.second, v.first));
            }
        }
    }
    for (auto v : st) {
        if (v.first <= m) 
            ans += v.second;
    }
    cout << ans << "\n";
    return 0;
}

F. Two Transportations

以及我怎么不会写 \(n^2\) dijkstra?

考虑让两个人一起跑 dijkstra,我们现在需要找到和当前点集 \(S\) 距离最近的点 \(x\)。会发现 \(x\) 要么是 A 的图里和 \(S\) 最近的点 \(p\),要么是 B 的图里和 \(S\) 最近的点 \(q\)。让两个人分别求出这两个点,然后把距离发给对面。距离很大,我们只发和上一次的差分。而差分显然 \(\le 500\)。然后两个人分别收到之后和自己的距离比一下,然后距离小的再把自己找到的点的编号发给对面,这样就完成了一轮扩展。总共需要通信 \(9 + 9 + 11 = 29\) 次,\(n = 2000\) 轮刚好卡到 \(58000\) 次。

代码
#include <iostream>
#include <string.h>
// #include "transportations.h"
#include "Azer.h"
#include "Baijan.h"
using namespace std;
const int inf = 0x3f3f3f3f;
namespace Azer {
    int g[2005][2005];
    int n;
    bool vis[2005];
    int dist[2005];
    int p, d, lst;
    void find() {
        p = -1, d = inf;
        for (int i = 0; i < n; i++) !vis[i] && (p == -1 || dist[i] < d) ? (p = i, d = dist[i]) : 0;
        if (d != inf) for (int i = 8; ~i; i--) SendA(((d - lst) >> i) & 1);
        else for (int i = 0; i < 9; i++) SendA(1);
    }
    void upd(int x, int t) { dist[x] = t; vis[x] = 1; for (int i = 0; i < n; i++) dist[i] = min(dist[i], dist[x] + g[x][i]); }
    void InitA(int N, int A, vector<int> U, vector<int> V, vector<int> C) {
        n = N;
        memset(g, 63, sizeof g);
        memset(dist, 63, sizeof dist);
        for (int i = 0; i < A; i++) g[U[i]][V[i]] = g[V[i]][U[i]] = C[i];
        dist[0] = 0;
        upd(0, 0);
        find();
    }
    int cnt, cur, tp;
    void ReceiveA(bool x) {
        cur = cur << 1 | x; ++cnt;
        if (tp == 0 && cnt == 9) {
            if (d < lst + cur) {
                for (int i = 10; ~i; i--) SendA((p >> i) & 1);
                upd(p, lst = d);
                find();
                cur = cnt = 0;
            } else tp = 1, lst = d = lst + cur, cur = cnt = 0;
        }
        if (tp == 1 && cnt == 11) {
            upd(cur, d); tp = cur = cnt = 0;
            find();
        }
    }
    vector<int> Answer() {
        vector<int> ret(n);
        for (int i = 0; i < n; i++) ret[i] = dist[i];
        return ret;
    }
}

void InitA(int N, int A, vector<int> U, vector<int> V, vector<int> C) { Azer::InitA(N, A, U, V, C); }
void ReceiveA(bool x) { Azer::ReceiveA(x); }
vector<int> Answer() { return Azer::Answer(); }

namespace Baijan {
    int g[2005][2005];
    int n;
    bool vis[2005];
    int dist[2005];
    int p, d, lst;
    void find() {
        p = -1, d = inf;
        for (int i = 0; i < n; i++) !vis[i] && (p == -1 || dist[i] < d) ? (p = i, d = dist[i]) : 0;
        if (d != inf) for (int i = 8; ~i; i--) SendB(((d - lst) >> i) & 1);
        else for (int i = 0; i < 9; i++) SendB(1);
    }
    void upd(int x, int t) { dist[x] = t; vis[x] = 1; for (int i = 0; i < n; i++) dist[i] = min(dist[i], dist[x] + g[x][i]); }
    void InitB(int N, int B, vector<int> S, vector<int> T, vector<int> D) {
        n = N;
        memset(g, 63, sizeof g);
        memset(dist, 63, sizeof dist);
        for (int i = 0; i < B; i++) g[S[i]][T[i]] = g[T[i]][S[i]] = D[i];
        dist[0] = 0;
        upd(0, 0);
        find();
    }
    int cnt, cur, tp;
    void ReceiveB(bool x) {
        cur = cur << 1 | x; ++cnt;
        if (tp == 0 && cnt == 9) {
            if (d <= lst + cur) {
                for (int i = 10; ~i; i--) SendB((p >> i) & 1);
                upd(p, lst = d);
                find();
                cur = cnt = 0;
            } else tp = 1, lst = d = lst + cur, cur = cnt = 0;
        }
        if (tp == 1 && cnt == 11) {
            upd(cur, d); tp = cur = cnt = 0;
            find();
        }
    }
}

void InitB(int N, int B, vector<int> S, vector<int> T, vector<int> D) { Baijan::InitB(N, B, S, T, D); }
void ReceiveB(bool x) { Baijan::ReceiveB(x); }

G. Designated Cities

从部分分开始考虑\(k = 1\) 是显然的换根 dp,\(k = 2\) 时,设 \(w(x)\) 为以 \(x\) 为根的内向树边权和,\(dist(x, y)\) 为以 \(c_i + d_i\) 为边权的距离,则选择 \(x, y\) 两个点的收益是 \(\frac{w(x) + w(y) + dist(x, y)}2\)。这个可以套两遍 dfs 求直径的方法求出。把直径缩成一个点,那么接下来再选一个点的收益就是根到它的有向边权和。

根据经典结论,把新树长链剖分,然后按权值从大到小依次选每条长链即可。

代码
#include <iostream>
#include <algorithm>
#include <vector>
#define int long long
using namespace std;
const int inf = 0x3f3f3f3f3f3f3f3f;
struct E { int v, x, y; };
vector<E> G[200005];
int n, s;
int w[200005];
void dfs1(int x, int fa) {
    for (auto e : G[x]) {
        int v = e.v;
        if (v != fa) w[1] += e.y, dfs1(v, x);
    }
}
int ans[200005];
void dfs2(int x, int fa) {
    ans[1] = max(ans[1], w[x]);
    for (auto e : G[x]) {
        int v = e.v;
        if (v != fa) w[v] = w[x] - e.y + e.x, dfs2(v, x);
    }
}
int f[200005], dist[200005];
void dfs3(int x, int fa, int d, int &p) {
    f[x] = fa; dist[x] = d;
    if (!p || (d + w[x] > dist[p] + w[p])) p = x;
    for (auto e : G[x]) {
        int v = e.v;
        if (v != fa) dfs3(v, x, d + e.x + e.y, p);
    }
}
bool ind[200005];
int len[200005];
vector<int> vec;
void dfs4(int x, int fa) {
    int son = 0;
    for (auto e : G[x]) {
        int v = e.v;
        if (v != fa && !ind[v]) {
            dfs4(v, x);
            if (len[v] + e.x > len[x]) len[x] = len[v] + e.x, son = v;
        }
    }
    for (auto e : G[x]) {
        int v = e.v;
        if (v != fa && !ind[v] && v != son) vec.emplace_back(len[v] + e.x);
    }
}
signed main() {
    ios::sync_with_stdio(false);
    cin.tie(0);
    cout.tie(0);
    cin >> n;
    for (int i = 1; i < n; i++) {
        int u, v, x, y;
        cin >> u >> v >> x >> y; s += x + y;
        G[u].push_back({ v, x, y });
        G[v].push_back({ u, y, x });
    }
    dfs1(1, 0);
    dfs2(1, 0);
    int p = 0, q = 0;
    dfs3(1, 0, 0, p);
    dfs3(p, 0, 0, q);
    ans[2] = (dist[q] + w[p] + w[q]) / 2;
    while (p != q) ind[q] = 1, q = f[q]; ind[p] = 1;
    for (int i = 1; i <= n; i++) if (ind[i]) dfs4(i, 0), vec.emplace_back(len[i]);
    sort(vec.begin(), vec.end(), greater<int>());
    for (int i = 0; i < (int)vec.size(); i++) ans[i + 3] = ans[i + 2] + vec[i];
    cin >> q;
    while (q--) {
        int x;
        cin >> x;
        cout << (ans[x] ? s - ans[x] : ans[x]) << "\n";
    }
    return 0;
}

H. Lamps

显然推平取反区间之内各自互不相交,且取反一定在推平之后做。那么就可以直接 dp,直接记录当前点上叠了哪些操作,转移到下一个点的时候考虑不变或新开或者结束原本的操作,即可。

代码
#include <iostream>
#include <algorithm>
#include <string.h>
using namespace std;
inline void Cmin(int &x, int y) { x = min(x, y); }
int n;
string S, T;
int f[1000005][6];
char F(char x, int o) {
    if (o >> 1) (o & 2) ? (x = '0') : (x = '1');
    if (o & 1) x ^= 1;
    return x;
}
int cost(int x, int y) {
    int ret = (((x >> 1) & (y >> 1)) != (y >> 1));
    return ret + ((x & y & 1) != (y & 1));
}
int main() {
    cin >> n >> S >> T; S = ' ' + S, T = ' ' + T;
    memset(f, 63, sizeof f);
    f[0][0] = 0;
    for (int i = 1; i <= n; i++) {
        for (int j = 0; j < 6; j++) {
            for (int k = 0; k < 6; k++) {
                if (F(S[i], k) == T[i]) Cmin(f[i][k], f[i - 1][j] + cost(j, k));
            }
        }
    }
    cout << *min_element(f[n], f[n] + 6) << "\n";
    return 0;
}

I. Bitaro, who Leaps through Time

考虑从左往右,反之同理。先把每个位置上的值减掉下标,接下来就是从左往右能走就走,否则等或者回溯时间。考察每一段区间 \([l, r]\) 的影响,发现实际上是把进来的 \(>r\) 的数花费代价变成 \(r\)\(<l\) 的数直接变成 \(l\),其他数不变。那么考虑一段区间和后面的区间叠起来会发生什么。发现如果这些区间都有交,那么把它们全走一遍的效果等效于直接走它们的交区间。而如果没有交了,情况就可以变得混乱邪恶并十分癫狂。如果区间没有交的话,那么所有东西进去之后出来都是同一个点,顶多是带着不同的代价。而这个代价的不同其实是在这些区间的交第一次消失的时候就决定的,可以用一个值 \(x\) 表示所有不同的代价本质上都相当于先把进来的东西归到 \(x\) 花费的代价。而之后所有东西代价同增同减,没有区别。那其实就相当于这段区间的影响是先归到 \(x\),然后花费代价变成 \(y\) 出去。显然所有形式的区间的影响复合之后就只有这两种形式,一种是形如有交时的 \([l, r]\),另一种形如无交时的先归到 \(x\),再花费代价 \(c\) 变成 \(y\) 出去。我们考虑把这两种不同的东西分开表示,一种用二元组 \((l, r)\),另一种用三元组 \((x, y, c)\)。显然这两个东西是能加的,并且结合。反正就是然后你就可以直接上个线段树维护了。然后就做完了。

代码
#include <iostream>
#define int long long
using namespace std;
struct Info { int x, y, z; };
Info operator+(Info a, Info b) {
    if (a.z == -1 && b.z == -1) {
        if (max(a.x, b.x) <= min(a.y, b.y)) return { max(a.x, b.x), min(a.y, b.y), -1 };
        else if (a.x > b.y) return { a.x, b.y, a.x - b.y };
        else return { a.y, b.x, 0 };
    } else if (a.z != -1 && b.z != -1) return { a.x, b.y, a.z + b.z + max(0ll, a.y - b.x) };
    else if (a.z == -1) {
        if (b.x > a.y) return { a.y, b.y, b.z };
        else if (b.x < a.x) return { a.x, b.y, b.z + a.x - b.x };
        else return b;
    } else {
        if (a.y > b.y) return { a.x, b.y, a.z + a.y - b.y };
        else if (a.y < b.x) return { a.x, b.x, a.z };
        else return a;
    }
}
struct node { Info x, y; } T[1200005];
node operator+(node a, node b) { return { a.x + b.x, b.y + a.y }; }
struct Segment_Tree {
    void Change(int o, int l, int r, int x, node y) {
        if (l == r) return T[o] = y, void();
        int mid = (l + r) >> 1;
        if (x <= mid) Change(o << 1, l, mid, x, y);
        else Change(o << 1 | 1, mid + 1, r, x, y);
        T[o] = T[o << 1] + T[o << 1 | 1];
    }
    node Query(int o, int l, int r, int L, int R) {
        if (L <= l && r <= R) return T[o];
        int mid = (l + r) >> 1;
        if (R <= mid) return Query(o << 1, l, mid, L, R);
        if (L > mid) return Query(o << 1 | 1, mid + 1, r, L, R);
        return Query(o << 1, l, mid, L, R) + Query(o << 1 | 1, mid + 1, r, L, R);
    }
} seg;
int n, q;
signed main() {
    ios::sync_with_stdio(false);
    cin.tie(0);
    cout.tie(0);
    cin >> n >> q;
    for (int i = 1; i < n; i++) {
        int l, r;
        cin >> l >> r; --r;
        seg.Change(1, 1, n - 1, i, { { l - i, r - i, -1 }, { l - (n - i - 1), r - (n - i - 1), -1 } });
    }
    while (q--) {
        int op, a, b, c, d;
        cin >> op;
        if (op == 1) cin >> a >> b >> c, --c, seg.Change(1, 1, n - 1, a, { { b - a, c - a, -1 }, { b - (n - a - 1), c - (n - a - 1), -1 } });
        else {
            cin >> a >> b >> c >> d;
            if (a == c) cout << max(0ll, b - d) << "\n";
            else if (a < c) cout << max(0ll, ((Info) { b - a, b - a, -1 } + seg.Query(1, 1, n - 1, a, c - 1).x + (Info) { d - c, d - c, -1 }).z) << "\n";
            else cout << max(0ll, ((Info) { b - (n - a), b - (n - a), -1 } + seg.Query(1, 1, n - 1, c, a - 1).y + (Info) { d - (n - c), d - (n - c), -1 }).z) << "\n";
        }
    }
    return 0;
}

J. Cake 3

???这决策单调性谁想得到???

显然把 \(C\) 排序选,后面减掉的那项就是 \(2(C_{\max} - C_{\min})\)。我们枚举 \(r\)然后发现最优的 \(l\) 具有决策单调性。于是套个分治就做完了。区间权值前 \(k\) 大和直接主席树维护即可。

决策单调性证明:设 \(y < x\),且在 \(i\)\(f(x, i) > f(y, i)\),我们只需要证明 \(f(x, i + 1) > f(y, i + 1)\)。显然会变的只有区间的前 \(k\) 大和,而由于 \(y\) 更靠左,其前 \(k\) 大的东西总的来说不会比 \(x\) 小,因此新加一个 \(i\) 进来 \(y\) 的前 \(k\) 大扔掉的东西一定不小于 \(x\) 扔掉的,于是 \(x\) 的增量也更大,再加上它原本就更大,因此 \(f(x, i + 1) > f(y, i + 1)\)。证毕。

誒,不知道能不能通过证明里的粗体字正面想出决策单调性呢。不过要是能直接想到决策单调性就更好了。

代码
#include <iostream>
#include <algorithm>
#define int long long
using namespace std;
const int inf = 0x3f3f3f3f3f3f3f3f;
int n, m;
pair<int, int> a[200005];
int d[200005], dcnt;
struct Segment_Tree {
    struct node {
        int l, r, cnt, s;
    } T[5000005];
    int ncnt;
    void Insert(int &p, int q, int l, int r, int x) {
        T[p = ++ncnt] = T[q]; ++T[p].cnt; T[p].s += d[x];
        if (l == r) return;
        int mid = (l + r) >> 1;
        if (x <= mid) Insert(T[p].l, T[q].l, l, mid, x);
        if (x > mid) Insert(T[p].r, T[q].r, mid + 1, r, x);
    }
    int Query(int p, int q, int l, int r, int k) {
        if (l == r) return T[q].s / T[q].cnt * k;
        int mid = (l + r) >> 1;
        if (T[T[q].r].cnt - T[T[p].r].cnt >= k) return Query(T[p].r, T[q].r, mid + 1, r, k);
        else return Query(T[p].l, T[q].l, l, mid, k - (T[T[q].r].cnt - T[T[p].r].cnt)) + T[T[q].r].s - T[T[p].r].s; 
    }
} seg;
int ans = -inf;
int rt[200005];
void Solve(int l, int r, int pl, int pr) {
    if (l > r) return;
    int mid = (l + r) >> 1;
    int p = -1, t = -inf;
    for (int i = pl; i <= min(pr, mid - m + 1); i++) {
        int v = seg.Query(rt[i], rt[mid - 1], 1, dcnt, m - 2) + 2 * a[i].first - 2 * a[mid].first + a[i].second + a[mid].second;
        if (v > t) t = v, p = i;
    }
    if (p == -1) return;
    ans = max(ans, t);
    Solve(l, mid - 1, pl, p);
    Solve(mid + 1, r, p, pr);
}
signed main() {
    cin >> n >> m;
    for (int i = 1; i <= n; i++) cin >> a[i].second >> a[i].first, d[i] = a[i].second;
    sort(d + 1, d + n + 1);
    dcnt = unique(d + 1, d + n + 1) - d - 1;
    sort(a + 1, a + n + 1);
    for (int i = 1; i <= n; i++) seg.Insert(rt[i], rt[i - 1], 1, dcnt, lower_bound(d + 1, d + dcnt + 1, a[i].second) - d);
    Solve(m, n, 1, n);
    cout << ans << "\n";
    return 0;
}

K. Mergers

处理出所有不在任一点集中的边,剩下的边直接合并。对于新树,我们希望用尽量少的可交链覆盖所有边。显然这个答案就是叶子个数除以二上取整。

代码
#include <iostream>
#include <vector>
using namespace std;
int n, m;
vector<int> G[500005];
vector<int> vec[500005];
int dep[500005], son[500005], top[500005], sz[500005], f[500005];
void dfs1(int x, int fa, int d) {
    f[x] = fa;
    sz[x] = 1;
    dep[x] = d;
    for (int v : G[x]) {
        if (v != fa) {
            dfs1(v, x, d + 1);
            sz[x] += sz[v];
            if (sz[v] > sz[son[x]]) son[x] = v;
        }
    }
}
void dfs2(int x, int t) {
    top[x] = t;
    if (son[x]) dfs2(son[x], t);
    for (int v : G[x]) if (v != f[x] && v != son[x]) dfs2(v, v);
}
int LCA(int x, int y) {
    while (top[x] ^ top[y]) (dep[top[x]] < dep[top[y]]) ? (y = f[top[y]]) : (x = f[top[x]]);
    return (dep[x] < dep[y] ? x : y);
}
int dsu[500005];
int deg[500005];
int getf(int x) { return (dsu[x] == x ? x : (dsu[x] = getf(dsu[x]))); }
int main() {
    ios::sync_with_stdio(false);
    cin.tie(0);
    cout.tie(0);
    cin >> n >> m;
    for (int i = 1; i < n; i++) {
        int u, v;
        cin >> u >> v;
        G[u].emplace_back(v);
        G[v].emplace_back(u);
    }
    dfs1(1, 0, 1);
    dfs2(1, 1);
    for (int i = 1, x; i <= n; i++) cin >> x, vec[x].emplace_back(i), dsu[i] = i;
    for (int i = 1; i <= m; i++) {
        if (vec[i].empty()) continue;
        int x = vec[i][0];
        for (int v : vec[i]) x = LCA(x, v);
        for (int v : vec[i]) while (dep[v] > dep[x]) dsu[v] = f[v], v = getf(v);
    }
    int ans = 0;
    for (int i = 1; i <= n; i++) (getf(i) == i && i != 1) ? (++deg[getf(f[i])], ++deg[i]) : 0;
    for (int i = 1; i <= n; i++) ans += (deg[i] == 1);
    cout << (ans + 1) / 2 << "\n";
    return 0;
}

L. Minerals

懂平衡的。

先按顺序加入所有点,求出每个点是它的匹配的左边还是右边。然后我们直接整体二分,拿右边找左边。直接写常数很大,过不去一点。首先的优化就是对于右边拿来询问的点,我们只需要知道加入它之后答案有没有变,因此不需要再加入之后再删掉,只需要改一下它是否处于集合中的状态即可。而且当分给左右儿子中一个的询问已经满了,剩下的询问就可以直接扔给另一边。而对于左边分治的东西,朴素的实现肯定是每次把左边都重新加进去,但实际上我们只要保证左区间和右区间是否全在集合中的状态不同即可。因此每次进一个区间的时候只需要知道进来的时候这个区间到底是怎么个状态,然后反转左边区间的状态即可。那么这个时候一次分治内部的东西已经简化成了暴力一次左区间,再暴力区间中的所有询问。这本身已经没有什么好优化的了,但是观察到这里的左边和右边并不平衡,于是我们调整区间中点的取值,将左区间占区间总长的占比从 \(\frac12\) 变成 \(p\),而通过 dp 或查看官解 / 题解可以得出 \(p\) 取到 \(\frac13\) 左右是最优的。加了这个东西之后直接就能过了。

代码
#include <iostream>
#include <algorithm>
#include <random>
#include <vector>
#include "minerals.h"
std::vector<int> v1;int cnt;
std::random_device rd;
std::mt19937 mtrand(rd());
void Solve(int l, int r, std::vector<int> vec, bool f) {
    using namespace std;
    if (l == r) return Answer(v1[l], vec[0]);
    int mid = l + (r - l + 1) * 0.38, x = 0, y;
    for (int i = l; i <= mid; i++) x = Query(v1[i]);
    vector<int> vl, vr;
    int i = l; f ^= 1;
    for (int j = 0; j < (int)vec.size(); j++) {
        int v = vec[j];
        if ((int)vl.size() == mid - l + 1) vr.emplace_back(v);
        else if ((int)vr.size() == r - mid) vl.emplace_back(v);
        else {
            y = Query(v); ++cnt;
            if (y != x) x = y, f ? vr.emplace_back(v) : vl.emplace_back(v);
            else f ? vl.emplace_back(v) : vr.emplace_back(v);
        }
    }
    Solve(l, mid, vl, f);
    Solve(mid + 1, r, vr, f ^ 1);
}
bool ir[100005];
void Solve(int n) {
    using namespace std;
    vector<int> tmp;
    v1.emplace_back(0);
    for (int i = 1, lst = 0; i <= n * 2; i++) Query(i) != lst ? (++lst, v1.emplace_back(i)) : (ir[i] = 1, tmp.emplace_back(i));
    Solve(1, n, tmp, 1);
}

C。按顺序考虑。

E,尝试将多种贡献 / 限制统一。

G 经典结论。

I。分开考虑。

在各种地方都要想决策单调性。

L,前面的优化部分。

posted @ 2025-08-29 00:13  forgotmyhandle  阅读(6)  评论(0)    收藏  举报