USACO23JAN P【杂题】

A. [USACO23JAN] Tractor Paths P

\(n\) 个区间,第 \(i\) 个区间为 \([l_i,r_i]\)。保证 \(l_1<l_2<\cdots<l_n\)\(r_1<r_2<\cdots<r_n\)。其中一部分区间是特殊的。

如果第 \(i\) 个区间和第 \(j\) 个区间相交,那么 \(i,j\) 之间有一条边。保证 \(1,n\) 联通。

\(q\) 次询问,每次给定 \(a,b\),求 \(a\)\(b\) 至少要经过多少条边,以及有多少个特殊区间对应的点,使得这个点可能在 \(a\)\(b\) 的最短路径上。

\(n,q \le 2\times 10^5\)


显然,每个区间能够到达的也是一个区间 \([L_i,R_i]\),并且也满足 \(L_i,R_i\) 递增。

第一问倍增即可。第二问相当于要求有多少个点 \(d(a,x) + d(x,b) = d(a,b)\),我们可以求出 \(f_{i,k}\) 表示从 \(i\) 往右 \(k\) 步能到达的最远点,\(g_{i,k}\) 表示从 \(i\) 往左 \(k\) 步能到达的最远点,我们枚举 \(d(a,x)\),设 \(c(l,r)\) 表示在 \([l,r]\) 之间有多少个关键点,则答案即为 \(c(a,a) + c(b,b) + \sum \limits_{i = 1}^{d(a,b) - 1} c(g_{b,d(a,b) - i}, f_{a,i})\),显然这些区间不交。拆成前缀和用倍增维护即可,时间复杂度 \(\mathcal{O}(n \log n)\)

code
#include <bits/stdc++.h>
using namespace std;
const int N = 2e5 + 5;
int n, k; char s[N], t[N]; 
int a[N], le[N], ri[N], f[N][18], g[N][18], p[N][18], q[N][18];
int qry(int x, int y) {
    if (x == y) {
        return 0;
    }
    int d = 0;
    for (int i = 17; i >= 0; i--) {
        if (f[x][i] >= y || f[x][i] == -1)
            continue;
        x = f[x][i], d += 1 << i;
    }
    return d + 1;
}
int main() {
    // ios :: sync_with_stdio(false);
    // cin.tie(nullptr);
    cin >> n >> k;
    scanf("%s %s", s + 1, t + 1);
    int c = 0, u = 1;
    for (int i = 1; i <= 2 * n; i++) {
        if (s[i] == 'L') {
            c++;
        } else {
            ri[u] = c, u++;
        }
    }
    c = n + 1, u = n;
    for (int i = 2 * n; i >= 1; i--) {
        if (s[i] == 'R') {
            c--;
        } else {
            le[u] = c, u--;
        }
    }
    for (int i = 1; i <= n; i++) {
        a[i] = a[i - 1] + t[i] - '0';
    }
    memset(f, -1, sizeof(f));
    memset(g, -1, sizeof(g));
    for (int i = 1; i < n; i++) {
        f[i][0] = ri[i], p[i][0] = a[ri[i]];
    }
    for (int i = n; i > 1; i--) {
        g[i][0] = le[i], q[i][0] = a[le[i] - 1];
    }
    for (int j = 1; j <= 17; j++) {
        for (int i = 1; i < n; i++) {
            if (f[i][j - 1] == -1)
                continue;
            f[i][j] = f[f[i][j - 1]][j - 1];
            p[i][j] = p[i][j - 1] + p[f[i][j - 1]][j - 1];
        }
    }
    for (int j = 1; j <= 17; j++) {
        for (int i = n; i > 1; i--) {
            if (g[i][j - 1] == -1)
                continue;
            g[i][j] = g[g[i][j - 1]][j - 1];
            q[i][j] = q[i][j - 1] + q[g[i][j - 1]][j - 1];
        }
    }
    while (k--) {
        int x, y;
        cin >> x >> y;
        int c = qry(x, y), d = 0;
        d += t[x] - '0';
        d += t[y] - '0';
        c--;
        for (int i = 17; i >= 0; i--) {
            if (c & (1 << i)) {
                d += p[x][i], x = f[x][i];
            }
        }
        for (int i = 17; i >= 0; i--) {
            if (c & (1 << i)) {
                d -= q[y][i], y = g[y][i];
            }
        }
        c++;
        cout << c << " " << d << "\n";
    }
    return 0;
}

B. [USACO23JAN] Mana Collection P

\(N\) 个法力池,第 \(i\) 个法力池每秒可积累 \(m_i\) 法力。有 \(M\) 条有向边 \((a_i,b_i,t_i)\) 连接,你可以在 \(t_i\) 秒内从 \(a_i\) 移动到 \(b_i\) 。每当你出现在一个池子里,你就可以收集储存在那个地方的所有法力,把它清空。在 \(0\) 的时候,所有的法力池都是空的,你可以选择任何一个池子来开始收集。

\(q\) 次询问,每个次给定两个整数 \(s\)\(e\),求如果你在第 \(s\) 秒结束时必须在法力池 \(e\) 处,在 \(s\) 秒内能收集的最大法力值。

\(N \leq 18\)\(q \leq 2 \times 10^5\),时限 \(\text{5.0s}\)


先用 Floyd 预处理出 \(d(x,y)\) 表示 \(x \to y\) 的最短距离,这步时间复杂度 \(\mathcal{O}(n^3)\)

容易发现,对于一条路径,收集到的法力仅与路径上所有点的最晚遍历时刻有关,也就是说,我们仅需知道这些点的最晚遍历时刻即可算出收集到的法力大小。形式化地,假设路径经过了 \(c_1,c_2,\cdots,c_k = e\),且 \(c_i\) 最晚遍历的时间是 \(d_i(0 \leq d_1 < d_2 < \cdots < d_k = s)\),那么最终收集到的法力大小为 \(\sum \limits_{i=1}^k f_{c_i} \cdot d_i\)

一个显然的贪心是,如果确定了一条路径,那么我们的策略一定是,先等一段时间,直到不走就来不及走到 \(e\) 了再开始走,即取 \(d_i = s - \sum \limits_{j = i}^{k-1} d(c_i,c_{i+1})\) 是最优的。那么法力大小即为 \(\sum \limits_{i = 1}^k f_{c_i} \cdot \left( s - \sum \limits_{j = i}^{k-1} d(c_i,c_{i+1}) \right) = s \cdot \sum \limits_{i=1}^k f_{c_i} - \sum \limits_{i=1}^{k-1} d(c_i,c_{i+1}) \sum \limits_{j=1}^{i} f_{c_j}\)

\(\sum \limits_{i = 1}^k f_{c_i}\) 看做斜率,\(- \sum \limits_{i=1}^{k-1} d(c_i,c_{i+1}) \sum \limits_{j=1}^{i} f_{c_j}\) 看做截距,那么它对应了一条直线。进一步地,对于一个确定的点集,它所对应的直线的斜率是一个定值,故我们保留截距最大的那条即可,这可以通过状压 DP 在 \(\mathcal{O}(2^nn^2)\) 的时间复杂度内预处理。

对于每个询问,我们考虑满足 \(c_k = e\) 的点集对应的直线,并求出当前横坐标 \(s\) 对应的最大纵坐标即可,这可以通过单调栈维护凸包来预处理,询问时二分 \(s\) 对应的是哪一段即可。

现在唯一的问题是如何保证 \(d_1 \geq 0\)。但我们发现如果 \(d_1 < 0\) 一定不优,所以可以直接不管了。时间复杂度 \(\mathcal{O}(n^3 + 2^n n^2 + q \log n)\)

code
#include <bits/stdc++.h>
using namespace std;
typedef long long LL;
const int N = 18;
const LL inf = 1e18;
int n, m, q, a[N];
LL d[N][N], v[1 << N], f[1 << N][N];
struct dat {
    LL x, y;
};
vector <dat> e[N], t[N];
vector <double> b[N];
double calc(dat a, dat b) {
    if (a.x == b.x) {
        if (a.y < b.y) {
            return inf;
        } else {
            return -inf;
        }
    }
    return 1.0 * (b.y - a.y) / (a.x - b.x);
}
int main() {
    ios :: sync_with_stdio(false);
    cin.tie(nullptr);
    cin >> n >> m;
    for (int i = 1; i <= n; i++) {
        cin >> a[i];
    }
    for (int i = 1; i < (1 << n); i++) {
        for (int j = 1; j <= n; j++) {
            f[i][j] = inf;
        }
    }
    for (int i = 1; i <= n; i++) {
        for (int j = 1; j <= n; j++) {
            if (i == j) {
                continue;
            }
            d[i][j] = inf;
        }
    }
    for (int i = 1, x, y, z; i <= m; i++) {
        cin >> x >> y >> z;
        d[x][y] = z;
    }
    for (int k = 1; k <= n; k++) {
        for (int i = 1; i <= n; i++) {
            for (int j = 1; j <= n; j++) {
                d[i][j] = min(d[i][j], d[i][k] + d[k][j]);
            }
        }
    }
    for (int s = 1; s < (1 << n); s++) {
        for (int i = 1; i <= n; i++) {
            if ((s & (1 << i - 1)) == 0) {
                continue;
            }
            v[s] += a[i];
            int t = s ^ (1 << i - 1);
            if (t == 0) {
                f[s][i] = 0;
                break;
            }
            for (int j = 1; j <= n; j++) {
                if ((t & (1 << j - 1)) == 0 || d[j][i] == inf) {
                    continue;
                }
                f[s][i] = min(f[s][i], f[t][j] + v[t] * d[j][i]);
            }
        }   
    }
    for (int i = 1; i <= n; i++) {
        for (int s = 1; s < (1 << n); s++) {
            if ((s & (1 << i - 1)) == 0) {
                continue;
            }
            e[i].push_back((dat){v[s], -f[s][i]});
        }
        sort(e[i].begin(), e[i].end(), [&](dat &a, dat &b) {
            if (a.x == b.x) {
                return a.y > b.y;
            } else {
                return a.x < b.x;
            }
        });
        t[i].push_back((dat){0, 0}), b[i].push_back(-inf);
        for (auto x : e[i]) {
            while (t[i].size() > 1 && calc(x, t[i].back()) < b[i].back()) {
                t[i].pop_back();
                b[i].pop_back();
            }
            double r = calc(x, t[i].back());
            t[i].push_back(x);
            b[i].push_back(r);
        }
    }
    cin >> q;
    while (q--) {
        int c, d;
        cin >> c >> d;
        int i = upper_bound(b[d].begin(), b[d].end(), c) - b[d].begin() - 1;
        dat z = t[d][i];
        LL ans = c * z.x + z.y;
        cout << ans << "\n";
    }
    return 0;
}

C. [USACO23JAN] Subtree Activation P

有一棵根为 \(1\) 的树,每个顶点最初都是关闭的。在一次操作中,你可以将一个顶点的状态切换。输出操作序列的最小长度,满足对于每一个顶点的子树,都有一个时刻,开启状态顶点的集合恰好是该子树中的顶点,且在所有操作之后,每个顶点都是关闭的。

\(n \leq 2 \times 10^5\)


第一眼:哎这不是 DSU on Tree 吗(x)

\(S_u\)\(u\) 子树的节点组成的集合。如果最后要求所有顶点都开启就可以直接 DSU on Tree 了,但是现在要所有顶点都关闭,那么不妨先考虑一下我们会怎么关。不难发现,假设现在开启的集合是 \(S_u\),那么我们一定会每次选择一个儿子 \(v\),关掉 \(S_u/S_v\),这样就能得到 \(S_v\),重复这样的操作直到所有点关闭。开启的时候也同理。

我们断言,所有操作大概都长成这样:每次从一个叶子 \(u\) 开始点亮,依次覆盖 \(S_u,S_{f_u},\cdots,S_r\),然后从 \(S_r\) 开始关,依次覆盖 \(S_{v' \in \text{son}_r},\cdots,S_{v}\),其中 \(v\) 是另一个叶子。进一步地,每次可以选择两个叶子 \((u,v)\),然后选择一个 \(\text{LCA(u,v)}\) 到根的路径上的节点 \(r\),覆盖 \(u \to r\)\(r \to v\) 上的所有集合。这样一次操作的代价是 \(2 \times s_r\),其中 \(s_r\)\(r\) 的子树大小。

证明考虑如果有走出 \((u,v)\) 的部分,一定可以拆成两条路径使得答案更优。显然,对于每个 \(r\),我们至多选择一条这样的路径,否则也可以拆成两条路径且答案更优。

现在问题就变成了:选取若干这样的路径,覆盖树上所有节点,并使得代价最小。于是可以 DP,设 \(f_{u,0}\) 表示覆盖 \(u\) 子树所需的最小代价,\(f_{u,1}\) 表示剩下一条从 \(u\) 到其子树内某个叶结点的链,\(u\) 子树内其余结点均被覆盖所需的最小代价。转移比较简单,这里就不具体写了,时间复杂度 \(\mathcal{O}(n)\)

code
#include <bits/stdc++.h>
using namespace std;
typedef long long LL;
const int N = 2e5 + 5;
const LL inf = 1e18;
int n, sz[N]; LL f[N][2];
vector <int> e[N];
LL max(LL x, LL y) {
    return x > y ? x : y;
}
void dfs(int u, int ff) {
    sz[u] = 1;
    LL sum = 0, mx = 0, mi = inf, _mi = inf;
    for (auto v : e[u]) {
        if (v == ff) {
            continue;
        }
        dfs(v, u);
        sz[u] += sz[v], mx = max(mx, sz[v]), sum += f[v][0];
        LL d = f[v][1] - f[v][0];
        if (d <= mi) {
            _mi = mi, mi = d;
        } else {
            _mi = min(_mi, d);
        }
    }
    if (sz[u] == 1) {
        f[u][0] = 2;
        return;
    }
    f[u][0] = 2 * sz[u] + sum, f[u][1] = sum + mi;
    if (_mi < inf) {
        f[u][0] += mi + _mi;
    }
    f[u][0] = min(f[u][0], sum + 2 * (sz[u] - mx));
}
int main() {
    ios :: sync_with_stdio(false);
    cin.tie(nullptr);
    cin >> n;
    for (int i = 2, f; i <= n; i++) {
        cin >> f;
        e[i].push_back(f);
        e[f].push_back(i);
    }
    dfs(1, 0);
    cout << f[1][0] << "\n";
    return 0;
}
posted @ 2023-03-01 13:22  came11ia  阅读(55)  评论(0编辑  收藏  举报