JOISC2018

A. Construction of Highway

重剖,对每条重链开 set 维护连续段。每次新加一个点时暴力跳重链,拿个 BIT 维护已经跳过的权值,然后把那些交到自己到根路径的连续段拿出来查一下 BIT 并扔进去,再删掉这个连续段。显然这样的总复杂度是 \(\mathcal{O}(n \log^2 n)\) 的。

代码
#include <iostream>
#include <algorithm>
#include <cassert>
#include <set>
#define int long long
#define lowbit(x) ((x) & (-(x)))
using namespace std;
int n;
int head[100005], nxt[200005], to[200005], ecnt;
void add(int u, int v) { to[++ecnt] = v, nxt[ecnt] = head[u], head[u] = ecnt; }
int a[100005], d[100005], dcnt;
int A[100005], B[100005];
int top[100005], dep[100005], son[100005], sz[100005], f[100005];
int dfn[100005], ncnt;
void dfs1(int x, int fa, int d) {
    dep[x] = d;
    f[x] = fa;
    sz[x] = 1;
    for (int i = head[x]; i; i = nxt[i]) {
        int v = to[i];
        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; dfn[x] = ++ncnt;
    if (!son[x]) return;
    dfs2(son[x], t);
    for (int i = head[x]; i; i = nxt[i]) {
        int v = to[i];
        if (v != son[x] && v != f[x]) 
            dfs2(v, v);
    }
}
set<pair<int, int> > st[100005];
struct BIT {
    int bit[2000005], sz; pair<int, int> p[2000005];
    void clear() { for (; sz; --sz) add(p[sz].first, -p[sz].second); }
    void add(int x, int y) { for ((y > 0 ? p[++sz] = make_pair(x, y) : pair<int, int>()); x <= n; x += lowbit(x)) bit[x] += y; }
    int query(int x) {
        int ret = 0;
        for (; x; x -= lowbit(x)) ret += bit[x];
        return ret;
    }
} bit;
int cur[100005];
int work(int x, int val) {
    int ret = 0;
    while (x) {
        int t = top[x], y;
        assert(st[t].size());
        pair<int, int> tmp;
        set<pair<int, int> >::iterator it = st[t].upper_bound({ dfn[x], n + 1 }), it2;
        assert(it != st[t].begin());
        --it; tmp = *it; y = dfn[x] + 1;
        while (1) {
            ret += (y - (it -> first)) * bit.query(it -> second - 1);
            bit.add(it -> second, (y - it -> first));
            y = it -> first;
            if (it == st[t].begin()) { st[t].erase(it); break; }
            it2 = prev(it), st[t].erase(it);
            it = it2;
        }
        if (dfn[x] != cur[t] && (st[t].empty() || st[t].begin() -> first != dfn[x] + 1)) st[t].insert({ dfn[x] + 1, tmp.second });
        st[t].insert({ dfn[t], val });
        x = f[t];
    }
    bit.clear();
    return ret;
}
signed main() {
    cin >> n;
    for (int i = 1; i <= n; i++) cin >> a[i], d[i] = a[i];
    sort(d + 1, d + n + 1); dcnt = unique(d + 1, d + n + 1) - d - 1;
    for (int i = 1; i <= n; i++) a[i] = lower_bound(d + 1, d + dcnt + 1, a[i]) - d;
    for (int i = 1; i < n; i++) cin >> A[i] >> B[i], add(A[i], B[i]), add(B[i], A[i]);
    dfs1(1, 0, 1);
    dfs2(1, 1);
    st[1].insert({ cur[1] = 1, a[1] });
    for (int i = 1; i < n; i++) {
        cout << work(A[i], a[B[i]]) << "\n", cur[top[B[i]]] = dfn[B[i]];
        if (top[B[i]] == B[i]) st[B[i]].insert({ dfn[B[i]], a[B[i]] });
    }
    return 0;
}

B. Fences

预处理任意两条线段在最终方案里相邻时的代价,如果涉及到中心矩形要特殊考虑。接下来我们只需要找到一个代价最小的环即可。但是这个环不一定包含中心矩形,于是根据判断点在多边形内部的做法,从原点往右引射线,然后只需要射线和最终连出来的东西交奇数次即可。dijkstra 的时候判一下大概就好了吧。

(计算几何,代码就不写了)

C. Tents

挺有趣的题。考虑按行 dp,每次转移可以:不变;加单点;加一对行上的匹配;加一对列上的匹配。前三种的转移都是平凡的,对于最后一种,我们并不需要真的知道它匹配到了下面的哪一行,而只关心总共又少了一行,而一共有(剩下那么多)行可以用来少,于是只需要乘以剩下的行数然后转移到 \(f_{i + 2}\)(而不是 \(f_{i + 1}\))即可。

代码
#include <iostream>
#include <algorithm>
#include <numeric>
#define int long long
using namespace std;
const int P = 1000000007;
inline void Madd(int &x, int y) { (x += y) >= P ? (x -= P) : 0; }
int n, m;
int f[3005][3005];
signed main() {
    cin >> n >> m;
    f[0][m] = 1;
    for (int i = 0; i < n; i++) {
        for (int j = 0; j <= m; j++) {
            Madd(f[i + 1][j], f[i][j]);
            if (j) Madd(f[i + 1][j - 1], f[i][j] * j * 4 % P), 
                   Madd(f[i + 2][j - 1], f[i][j] * j * (n - i - 1) % P);
            if (j > 1) Madd(f[i + 1][j - 2], f[i][j] * (j * (j - 1) / 2) % P);
        }
    }
    cout << accumulate(f[n], f[n] + m, 0ll) % P << "\n";
    return 0;
}

D. Asceticism

先二项式反演,钦定至少 \(x\) 个下降,接下来那些没有被钦定的位置就会将序列划分为 \(n - x\) 段。而我们要做的就是把 \(n\) 个数,按顺序扔进这 \(n - x\) 段里,并保证每段非空。也就是把 \(n\) 个互相区分的球扔进 \(n - x\) 个互相区分的盒子,每个盒子非空的方案数。再对空盒子容斥,列出式子:

\(\begin{equation}\begin{split} ans &= \sum\limits_{x = k}^{n - 1}(-1)^{x - k}\binom{x}{k}\sum\limits_{y = 0}^{n - x - 1}(-1)^{y}\binom{n - x}{y}(n - x - y)^n \\ &= \sum\limits_{x = k}^{n}\sum\limits_{y = 0}^{n - x}(-1)^{x - k + y}\binom{x}{k}\binom{n - x}{y}(n - x - y)^n \\ &= \sum\limits_{s = k}^{n}\sum\limits_{x = k}^{s}(-1)^{s - k}(n - s)^n\binom{x}{k}\binom{n - x}{s - x} \\ &= \sum\limits_{s = k}^{n}(-1)^{s - k}(n - s)^n\sum\limits_{x = k}^{s}\binom{x}{k}\binom{n - x}{n - s} \\ &= \sum\limits_{s = k}^{n}(-1)^{s - k}(n - s)^n\binom{n + 1}{n + k - s + 1} \end{split}\end{equation}\)

其中有换元 \(s = x + y\),最后一步用到了范德蒙德卷积。有了这个式子就可以直接算了。当然也可以把 \(s\) 变成 \(s - k\),这样可能式子好看一点。但是反正都一样。

代码
#include <iostream>
#define int long long
using namespace std;
const int P = 1000000007;
int fac[1000005], ifac[1000005], inv[1000005];
void Cpre(int n) {
    fac[0] = fac[1] = ifac[0] = ifac[1] = inv[0] = inv[1] = 1;
    for (int i = 2; i <= n; i++) {
        fac[i] = fac[i - 1] * i % P;
        inv[i] = (P - P / i) * inv[P % i] % P;
        ifac[i] = ifac[i - 1] * inv[i] % P;
    }
}
inline int C(int n, int m) { return n < 0 || m < 0 || n < m ? 0 : fac[n] * ifac[m] % P * ifac[n - m] % P; }
int qpow(int x, int y = P - 2) {
    int ret = 1;
    while (y) {
        if (y & 1) 
            ret = ret * x % P;
        y >>= 1;
        x = x * x % P;
    }
    return ret;
}
int n, K;
int ans;
int p[100005];
signed main() {
    Cpre(100001);
    cin >> n >> K; --K;
    for (int i = p[0] = 1; i <= n - K; i++) p[i] = p[i - 1] + C(K + i, K);
    for (int s = 0; s <= n - K; s++) {
        if (s & 1) ans += P - C(n + 1, n - s + 1) * qpow(n - s - K, n) % P;
        else ans += C(n + 1, n - s + 1) * qpow(n - s - K, n) % P;
    }
    cout << ans % P << "\n";
    return 0;
}

E. Road Service

提答题。考虑把 \(k\) 个点连成一个菊花,拿中心当根。然后跑退火,看题解好像需要倒腾初始态和估价函数啥的,我不知道,也没写。什么时候补一下。

F. Worst Reporter 3

归纳容易得出每个人的行动形如每隔 \(t_i\) 秒前进 \(t_i\) 距离。同时又有 \(t_i - 1 | t_i\),因此相同的连续段至多 \(\log\) 段。每次询问暴力跑一遍即可。

代码
#include <iostream>
#define int long long
using namespace std;
int n, q;
int a[500005], t[500005];
struct node {
    int t, l, r;
} A[105];
int acnt;
signed main() {
    ios::sync_with_stdio(false);
    cin.tie(0);
    cout.tie(0);
    cin >> n >> q;
    for (int i = 1; i <= n; i++) cin >> a[i];
    t[0] = 1; A[acnt = 1] = (node) { 1, 0, 0 };
    for (int i = 1; i <= n; i++) {
        t[i] = ((a[i] + t[i - 1] - 1) / t[i - 1]) * t[i - 1];
        if (t[i] != t[i - 1]) A[++acnt] = (node) { t[i], i, i };
        else A[acnt].r = i;
    }
    while (q--) {
        int x, l, r, ans = 0;
        cin >> x >> l >> r;
        auto f = [&](int tl, int tr) { return max(0ll, min(r, tr) - max(l, tl) + 1); };
        for (int i = 1; i <= acnt; i++) {
            int tmp = x / A[i].t;
            ans += f(tmp * A[i].t - A[i].r, tmp * A[i].t - A[i].l);
        }
        cout << ans << "\n";
    }
    return 0;
}

G. Airline Route Map

我草,有点意思。

其实相当于需要用 12 个点还原所有点的编号。然后想到二进制拆分(???),开 \(10\) 个新点,每个点向原图点中二进制下这一位为 \(1\) 的点连一条边。接下来我们要让 B 找到这 10 个点,注意我们还剩两个点没用。显然在打乱后的图中只有度数信息不会改变,因此要确定一个点只能通过度数。我们拿剩下的两个点之一连向除了剩下的另一个点之外的所有点,这样这个点的度数必然最大,可以辨认。然后它没有连边的那个点就是剩下的另一个点。我们只需要拿这另一个点向那 10 个点各连一条边。确定了那 10 个点之后,还需要确定顺序。于是我们让 A 连一条从第 0 个点到第 9 个点的链,而由于 0 的度数一定比 9 大,这样我们就确定了这 10 个点的顺序。于是我们就相当于做完了。

代码
#include <iostream>
#include <algorithm>
#include <vector>
#include "Alicelib.h"
#include "Boblib.h"
using namespace std;
void Alice(int N, int M, int A[], int B[]) {
    int ecnt = M + N + 10 + 10 + 9;
    for (int i = 0; i < N; i++) ecnt += __builtin_popcount(i);
    InitG(N + 12, ecnt);
    for (int i = 0; i < M; i++) MakeG(--ecnt, A[i], B[i]);
    for (int i = 0; i < N; i++) {
        for (int j = 0; j < 10; j++) {
            if (i & (1 << j)) 
                MakeG(--ecnt, N + j, i);
        }
    }
    for (int i = 0; i < N + 10; i++) MakeG(--ecnt, i, N + 11);
    for (int i = 0; i < 10; i++) MakeG(--ecnt, N + i, N + 10);
    for (int i = 0; i < 9; i++) MakeG(--ecnt, N + i, N + i + 1);
}
int deg[10005], mark[10005];
int i10[10005], k10[10005], kcnt;
vector<int> vec[10005];
int id[10005], cur;
void dfs(int x, int fa) {
    id[x] = cur++;
    for (int v : vec[x]) if (v != fa) dfs(v, x);
}
void Bob(int V, int U, int C[], int D[]) {
    for (int i = 0; i < U; i++) ++deg[C[i]], ++deg[D[i]];
    int _11 = 0, _10 = 0;
    for (int i = 0; i < V; i++) deg[i] > deg[_11] ? (_11 = i) : 0;
    mark[_11] = 1;
    for (int i = 0; i < U; i++) if (C[i] == _11 || D[i] == _11) mark[C[i] ^ D[i] ^ _11] = 1;
    for (int i = 0; i < V; i++) !mark[i] ? (_10 = i) : 0;
    for (int i = 0; i < U; i++) if (C[i] == _10 || D[i] == _10) i10[C[i] ^ D[i] ^ _10] = 1;
    for (int i = 0; i < V; i++) i10[i] ? (k10[++kcnt] = i) : 0;
    for (int i = 0; i < U; i++) if (i10[C[i]] && i10[D[i]]) vec[C[i]].emplace_back(D[i]), vec[D[i]].emplace_back(C[i]);
    sort(k10 + 1, k10 + 10 + 1, [](int x, int y) { return vec[x].size() == vec[y].size() ? (deg[x] > deg[y]) : (vec[x].size() < vec[y].size()); });
    dfs(k10[1], -1);
    for (int i = 0; i < U; i++) {
        if (C[i] == _10 || C[i] == _11 || D[i] == _10 || D[i] == _11) continue;
        if (i10[C[i]] && !i10[D[i]]) id[D[i]] |= (1 << id[C[i]]);
        if (i10[D[i]] && !i10[C[i]]) id[C[i]] |= (1 << id[D[i]]);
    }
    vector<pair<int, int> > g;
    for (int i = 0; i < U; i++) {
        if (C[i] == _10 || C[i] == _11 || D[i] == _10 || D[i] == _11) continue;
        if (i10[C[i]] || i10[D[i]]) continue;
        g.emplace_back(id[C[i]], id[D[i]]);
    }
    InitMap(V - 12, g.size());
    for (auto v : g) MakeMap(v.first, v.second);
}

H. Bitaro's Party

对询问根号分治,大询问暴力,小询问预处理每个点到后面所有点的前 \(B\) 长路,合并时归并并去重即可。

代码
#include <iostream>
#include <algorithm>
#include <string.h>
#include <vector>
using namespace std;
const int B = 300;
int n, m, q;
int head[100005], nxt[200005], to[200005], ecnt;
void add(int u, int v) { to[++ecnt] = v, nxt[ecnt] = head[u], head[u] = ecnt; }
bool vis[100005], mark[100005];
vector<pair<int, int> > vec[100005], tmp, tmp2;
int ban[100005];
void dfs1(int x) {
    vis[x] = 1;
    vec[x].emplace_back(0, x);
    for (int i = head[x]; i; i = nxt[i]) {
        int v = to[i];
        if (!vis[v]) dfs1(v);
        tmp = vec[v];
        for (int j = 0; j < (int)vec[v].size(); j++) ++tmp[j].first;
        tmp2.resize(vec[x].size() + vec[v].size());
        merge(vec[x].begin(), vec[x].end(), tmp.begin(), tmp.end(), tmp2.begin(), greater<pair<int, int> > ());
        vec[x].clear();
        for (auto v : tmp2) {
            !mark[v.second] ? (vec[x].emplace_back(v), mark[v.second] = 1) : 0;
            if ((int)vec[x].size() > B) break;
        }
        for (auto v : vec[x]) mark[v.second] = 0;
    }
}
int f[100005];
int dfs2(int x) {
    if (vis[x]) return f[x];
    vis[x] = 1;
    int ret = (mark[x] ? -n - 1 : 0);
    for (int i = head[x]; i; i = nxt[i]) {
        int v = to[i];
        ret = max(ret, dfs2(v) + 1);
    }
    return f[x] = ret;
}
int main() {
    cin >> n >> m >> q;
    for (int i = 1; i <= m; i++) {
        int u, v;
        cin >> u >> v;
        add(v, u);
    }
    for (int i = 1; i <= n; i++) !vis[i] ? dfs1(i) : void();
    memset(vis, 0, sizeof vis);
    while (q--) {
        int k, x;
        cin >> x >> k;
        for (int i = 1; i <= k; i++) cin >> ban[i], mark[ban[i]] = 1;
        if (k < B) {
            int ans = -1;
            for (int i = 0; i < min(B, (int)vec[x].size()); i++) {
                if (!mark[vec[x][i].second]) {
                    ans = vec[x][i].first;
                    break;
                }
            }
            cout << ans << "\n";
        } else cout << max(-1, dfs2(x)) << "\n", memset(vis, 0, sizeof vis);
        for (int i = 1; i <= k; i++) mark[ban[i]] = 0;
    }
    return 0;
}

I. Security Gate

我草,咋这么难/ll

观察:一个括号串合法当且仅当它从前面看和从后面看,所有前缀和都非负。

于是我们考虑对所有反转(或不反转)区间后合法的括号串分类并分别计数。有以下三类:

  1. 原本就合法。直接 dp 即可。

  2. 两侧之一非法。
    不妨假设从左往右看非法。另一侧翻转序列再 dp 即可。此时假设我们反转的区间为 \([l, r]\),前缀和数组为 \(S\)。为了满足最后的前缀和为 \(0\) 的限制,我们可以用 \(S_{l - 1}\)\(S_n\) 表出 \(S_r\)。具体地,我们有 \(S_n - S_r + S_{l - 1} + S_{l - 1} - S_r = 0\),即 \(S_r = S_{l - 1} + \frac{S_n}2\)。显然 \(l - 1\) 必须在前缀第一个 \(-1\) 之前,那么我们直接选择第一个 \(-1\) 之前的最大值作为 \(S_{l - 1}\)。记为 \(A\)。然后再选择 \(A\) 右边的第一个 \(A + \frac{S_n}2\) 作为 \(r\),反转这一段区间即可让 \(S_n\) 合法。
    然后还要考虑前缀和非负,会发现如果 \(A + \frac{S_n}2 > 0\),那么我们在第一个 \(-1\) 之前就可以找到合法的 \(r\),于是根据 \(A\) 是最大值且序列从右边看合法,这样的反转一定不会让前缀和出现负数。否则我们还需要保证翻转的区间里不能出现比 \(2A\) 大的东西,不然这个东西翻下来就要变负数了。于是我们要 dp。
    考虑枚举第一个 \(-1\) 出现的位置。前缀 dp 只需要多记一维前缀 \(\max\) 就好了。然后考虑将后缀全部减掉 \(S_n\),那么 \(r\) 之内的数的上界就变成了 \(2(A - \frac{S_n}2)\)。而我们要找的右端点的值则变为 \(A - \frac{S_n}2\)。我们直接枚举 \(A - \frac{S_n}2\) 并 dp,\(f_{i, j, 0 / 1}\) 表示从后往前考虑到 \(i\),当前后缀和为 \(j\),是否已经选出 \(r\)。这个 dp 颇有一些细节。最后再枚举 \(A\) 合并前后的 dp 即可。

  3. 两侧都非法。
    还是设从左往右第一个 \(-1\) 之前的最大值为 \(A\)。那么我们还是要找 \(A\) 右边第一个 \(A + \frac{S_n}2\) 作为右端点来反转。由于此时从右往左也非法了,于是我们的右端点就必须要包含从右往左的第一个 \(-1\)。因此如果从右往左第一个 \(-1\)(那个位置)后面的最大值 \(B\) 不到我们想要的 \(A + \frac{S_n}2\),那就没法在这一侧考虑。但是在这种情况下我们可以证明从另一侧看的时候彼时的 \(A + \frac{S_n}2\) 一定小于 \(B\),于是我们把这种情况放在另一侧考虑就好了。最后再减掉两个东西恰好相等的情况(也可以证明一侧相等另一侧也相等)。那么这里我们只考虑 \(A + \frac{S_n}2 \le B\) 的情况。
    这里我们前缀的 dp 不变,后缀同样先减掉 \(S_n\)。那么这里,区间里的东西还是限制不能超过 \(2(A - \frac{S_n}2)\),但是可以有负数了,虽然只能在选定 \(r\) 之后。于是后缀的 dp 变成 \(f_{i, j, 0 / 1 / 2}\) 表示是否选了 \(r\),以及选了 \(r\) 之后有没有出现过负数。最后统计答案的时候由于这是第三种情况所以中间必须出现负数(否则是第二种)。同样是枚举 \(A - \frac{S_n}2\) dp,同样是枚举 \(A\) 合并前后缀。最后算完之后再 dp 一遍减掉 \(A + \frac{S_n}2 = B\) 的方案数即可。唯一的区别在于出负数之前不能有超过 \(A + \frac{S_n}2\) 的东西。

于是做完了,时间复杂度 \(\mathcal{O}(n^3)\)

代码
#include <iostream>
#include <algorithm>
#include <string.h>
#define int long long
using namespace std;
const int P = 1000000007;
inline void Madd(int &x, int y) { (x += y) >= P ? (x -= P) : 0; }
inline int Msum(int x, int y) { return Madd(x, y), x; }
int n, ans;
string str;
int work0() {
    static int f[2][305];
    f[0][0] = 1;
    for (int i = 0; i < n; i++) {
        memset(f[(i + 1) & 1], 0, sizeof f[0]);
        for (int j = 0; j <= i; j++) {
            if (str[i + 1] != ')') Madd(f[(i + 1) & 1][j + 1], f[i & 1][j]);
            if (str[i + 1] != '(' && j) Madd(f[(i + 1) & 1][j - 1], f[i & 1][j]);
        }
    }
    return f[n & 1][0];
}
int f[305][305];
void work1() {
    static int g[2][305][305];
    memset(f, 0, sizeof f);
    g[0][0][0] = 1;
    for (int i = 1, cur = 1, lst = 0; i <= n; i++, swap(cur, lst)) {
        memset(g[cur], 0, sizeof g[0]);
        for (int j = 0; j < i; j++) {
            for (int k = j; k < i; k++) {
                if (str[i] != ')') Madd(g[cur][j + 1][max(k, j + 1)], g[lst][j][k]);
                if (str[i] != '(') {
                    if (j) Madd(g[cur][j - 1][k], g[lst][j][k]);
                    else Madd(f[i][k], g[lst][j][k]);
                }
            }
        }
    }
}
int work2() {
    static int g[305][305][2];
    int ret = 0;
    for (int x = 1; x <= n; x++) { // x = A - Sn / 2
        memset(g, 0, sizeof g);
        g[n][0][0] = 1;
        for (int i = n; i; i--) {
            for (int j = 0; j <= n - i; j++) for (int k : { 0, 1 }) {
                if (k && j > x * 2) g[i][j][k] = 0;
                if (k && j == x) g[i][j][k] = g[i][j][0];
                if (str[i] != '(') Madd(g[i - 1][j + 1][k], g[i][j][k]);
                if (str[i] != ')' && j) Madd(g[i - 1][j - 1][k], g[i][j][k]);
            }
        }
        for (int i = 1; i <= n; i++) {
            for (int A = 0; A < i; A++) {
                int B = (A - x) * 2;
                if (B < 0 && -n <= B) {
                    Madd(ret, f[i][A] * g[i][-B - 1][A + B / 2 < 0] % P);
                }
            }
        }
    }
    return ret;
}
int work3(bool fl = 0) {
    static int g[305][605][3];
    int ret = 0;
    for (int x = 0; x <= n; x++) { // x = A - Sn / 2
        memset(g, 0, sizeof g);
        g[n][n][0] = 1;
        for (int i = n; i; i--) {
            for (int j = i - n; j <= n - i; j++) for (int k : { 0, 1, 2 }) {
                if (k != 2 && j < 0) continue;
                if (k && j > x * 2) g[i][j + n][k] = 0;
                if (k == 1 && j == x) g[i][j + n][k] = g[i][j + n][0];
                if (k != 2 && fl && j > x) g[i][j + n][k] = 0;
                if (g[i][j + n][k]) {
                    if (str[i] != '(') Madd(g[i - 1][j + n + 1][k], g[i][j + n][k]);
                    if (str[i] != ')') {
                        if (j > 0) Madd(g[i - 1][j + n - 1][k], g[i][j + n][k]);
                        else if (k) Madd(g[i - 1][j + n - 1][2], g[i][j + n][k]);
                    }
                }
            }
        }
        for (int i = 1; i <= n; i++) {
            for (int A = 0; A < i; A++) {
                int C = (A - x) * 2;
                if (A - C / 2 >= 0 && -n <= C && C <= n) Madd(ret, f[i][A] * g[i][n - C - 1][2] % P);
            }
        }
    }
    return ret;
}
signed main() {
    cin >> n;
    if (n & 1) return cout << "0\n", 0;
    cin >> str; str = ' ' + str;
    ans = work0(); work1();
    ans += work2() + work3() - work3(1);
    reverse(str.begin(), str.end());
    for (int i = 0; i < n; i++) (str[i] != 'x') ? (str[i] ^= ('(' ^ ')')) : 0;
    str = ' ' + str;
    work1();
    ans += work2() + work3();
    cout << ans % P << "\n";
    return 0;
}

J. Candies

反悔贪心。大根堆维护所有决策,每次选出当前最优决策,然后和它两边的东西合并变成 \(a_l + a_r - a_x\),作为反悔扔回堆里。注意序列最开头和最后要赋为 \(-\infty\),不然可能会选到他们和两边合并之后的决策(但这样的决策实际上不能使选的东西的总量增加)。

模拟费用流,建模就是将一个东西视为两边的间隔的匹配。于是源向奇数间隔连边,偶数间隔向汇连边,奇数间隔向相邻的偶数间隔连费用为它俩中间的东西的价值的边。于是变成最大费用最大流。观察增广路的形态,发现每次相当于选择一个 \(01\) 交替的连续段满足其中 \(0\)\(1\) 多一个,然后反转这个连续段里的 \(0 / 1\),并获得收益。当然反转之后这个连续段会和左右的东西合并。那么我们只需要拿个东西维护当前所有这样的连续段及其收益即可。大根堆就很适合。

其他做法。在费用流建模已经证明了答案随 \(k\)(选的个数)的凸性的情况下,我们如果只询问一个 \(k\),那么大力 wqs 二分 + dp 即可。但是现在询问所有 \(k\),我们考虑分治,每个区间中求出 \(f_{k, 0 / 1, 0 / 1}\),表示左右端点选没选,总共选了 \(k\) 个的最大收益,然后闵可夫斯基和合并左右即可。但是我没写。

对于凸性这一套做这个题,我评价是,道高一尺,魔高一丈。

代码
#include <iostream>
#include <queue>
#define int long long
using namespace std;
int n;
priority_queue<pair<int, int> > q;
int pre[200005], nxt[200005];
int a[200005];
bool d[200005];
void del(int x) { d[x] = 1; pre[nxt[x]] = pre[x], nxt[pre[x]] = nxt[x]; }
signed main() {
    cin >> n;
    for (int i = 1; i <= n; i++) cin >> a[i], q.push({ a[i], i });
    for (int i = 0; i <= n; i++) pre[nxt[i] = i + 1] = i;
    a[0] = a[n + 1] = -0x3f3f3f3f3f3f3f3f;
    for (int i = 1, cur = 0; i <= (n + 1) / 2;) {
        pair<int, int> p = q.top(); q.pop();
        if (d[p.second]) continue;
        int v = 0;
        a[p.second] = a[pre[p.second]] + a[nxt[p.second]] - a[p.second];
        if (pre[p.second] != 0) del(pre[p.second]);
        if (nxt[p.second] != n + 1) del(nxt[p.second]);
        cur += p.first; ++i;
        cout << cur << "\n";
        q.push({ a[p.second], p.second });
    }
    return 0;
}

K. Library

维护已知点集及其中连边,我们每次往里加一个点,希望求出这个点到点集的连边情况。直接分治,\(f(l, r, k)\) 表示已知 \(x\)\([l, r]\) 中点一共有 \(k\) 条连边,要找出这些连边分别是啥。从中点劈开,左右分别询问递归即可。那么找到一条边的询问次数就是 \(\log n\),总复杂度 \(\mathcal{O}(n\log n)\)

代码
#include <iostream>
#include <vector>
#include "library.h"
using namespace std;
vector<int> g[1005];
vector<pair<int, int> > e;
vector<int> qv;
int n;
int qe(int l, int r) {
    int ret = 0;
    for (auto v : e) ret += (l <= v.first && v.first <= r && l <= v.second && v.second <= r);
    return ret;
}
int Qe(int l, int r, int x) {
    for (int i = 0; i < n; i++) qv[i] = 0;
    for (int i = l - 1; i < r; i++) qv[i] = 1; qv[x - 1] = 1;
    return r - l + 2 - Query(qv);
}
void Solve(int l, int r, int x, int v) {
    if (l == r) return e.emplace_back(l, x), void();
    int mid = (l + r) >> 1, t = Qe(l, mid, x) - qe(l, mid);
    if (t) Solve(l, mid, x, t);
    if (t != v) Solve(mid + 1, r, x, v - t);
}
vector<int> res;
void dfs(int x, int fa) {
    res.emplace_back(x);
    for (int v : g[x]) if (v != fa) dfs(v, x);
}
void Solve(int N) {
    if (N == 1) {
        res.resize(1, 1);
        Answer(res);
        return;
    }
    qv.resize(n = N);
    for (int i = 2; i <= N; i++) {
        int t = Qe(1, i - 1, i) - qe(1, i - 1);
        t ? Solve(1, i - 1, i, t) : void();
    }
    for (auto v : e) g[v.first].emplace_back(v.second), g[v.second].emplace_back(v.first);
    for (int i = 1; i <= N; i++) {
        if ((int)g[i].size() == 1) {
            dfs(i, -1);
            break;
        }
    }
    Answer(res);
}

L. Wild Boar

还不会,会补的。


B。暴力建图的思想。

一个括号串合法当且仅当它从前面看和从后面看,所有前缀和都非负。

考虑拿限制和操作互相表示。

反悔贪心与费用流与凸性。

posted @ 2025-08-28 01:24  forgotmyhandle  阅读(10)  评论(0)    收藏  举报