USACO2022 OPEN【杂题】

A. [USACO22OPEN] 262144 Revisited P

对于一个长为 \(m\) 的序列 \(b\),如下定义其权值:对其进行 \(m-1\) 次操作,每次选择相邻的两个数合并,合并后将其替换为一个大于两数最大值的数。\(m-1\) 次操作后将只剩下一个数,\(b\) 的权值即为最终数字的最小值。

给定一个长为 \(n\) 的序列 \(a\),求 \(a\) 的所有子序列的权值之和。

\(n \leq 262144\)\(a_i \leq 10^6\)


首先有一个显然的区间 DP:设 \(f_{i,j}\) 为区间 \([i,j]\) 的权值,则 \(f_{i,j} = \min \limits_{i \leq k < j} \{ \max(f_{i,k}, f_{k+1,j}) + 1\}\)

进一步地,由于 \(f_{i,k}\) 关于 \(k\) 单调不降,\(f_{k+1,j}\) 关于 \(k\) 单调不升,因此我们只需要二分出最大的 \(k'\) 使得 \(f_{i,k'} \leq f_{k' + 1,j}\),然后在 \(k \in \{k',k'+1\}\) 中做决策即可,也可以利用决策单调性做到 \(\mathcal{O}(n^2)\)

但到此为止,直接 DP 似乎已经看不到前途了。我们可能需要找到一种更优的计算方式。


考虑一种无脑的合并方式:每次将区间划分成长度尽量相同的两部分,然后归并。这样我们可以得到答案的一个上界 \(\max a_i + \lceil \log n \rceil\)。既然答案的上界只有 \(\mathcal{O}(A)\),我们不妨转变一下思路,考虑依次枚举 \(v\),然后计算权值 \(\geq v\) 的区间个数。

由于 \(f_{i,k}\) 关于 \(k\) 单调不降,我们考虑对于每个左端点 \(i\) 维护最大的右端点 \(r_i\) 使得 \([i,r_i)\) 的权值不大于 \(v\)。维护的方式如下:初始时 \(r_i = i\),当 \(v-1 \to v\) 时,我们依次枚举 \(i\),令 \(r_i \gets r_{r_i}\),然后对于所有 \(a_i = v\) 的位置 \(i\),令 \(r_i \gets i+1\)

我们接下来说明为什么上面的做法是对的。当 \(i = r_i\) 时正确性显然。否则考虑 \([i,r_i)\)\([r_i,r_{r_i})\) 这两个区间,不妨设它们分别为区间 \(\mathcal{A},\mathcal{B}\)。当 \(\mathcal{A}\)\(\mathcal{B}\) 的权值都是 \(v-1\) 正确性显然。否则若 \(\mathcal{A}\) 的权值小于 \(v - 1\),那么由 \(\mathcal{A}\) 的极大性,有 \(a_{r_i} \geq v-1\)。若 \(a_{r_i} \geq v\),则此时 \(r_{r_i} = r_i\),否则 \(a_{r_i} = v-1\),两者的正确性都是显然的。当 \(\mathcal{B}\) 的权值小于 \(v-1\) 时同理。

于是我们得到了一个 \(\mathcal{O}(nA)\) 的做法。目前而言,算法的效率上似乎没有什么实质性的进展,我们还需要寻找加速的方法。


不妨再关注一下 \(f\) 的单调性。如果一个区间的 \(f\)\(\leq v\),那么其所有子区间的 \(f\) 值都 \(\leq v\)

这句话听上去是一句废话,但我们可以做一个容斥,每次改为计算权值 \(< v\) 的区间个数。这时考虑一类特殊的区间:我们称一个区间是 \([l,r]\) 是 “极大的”,当且仅当其向左或向右扩展都会使 \(f\) 值增大。

对于所有权值为 \(v-1\) 的极大区间,显然它们不会互相包含。于是我们要求的就变成了这样的一个问题:给定 \(L\) 个左右端点单调增的区间,求它们覆盖的区间数。这可以通过简单容斥做到 \(\mathcal{O}(L)\)

也就是说,如果我们直接在枚举 \(v\) 的过程中维护极大区间的集合,就能够避免每次 \(\mathcal{O}(n)\) 的枚举。但从直觉上看,极大区间的数量似乎还是可能很多,复杂度究竟能优化多少还有待商榷。


为此,我们需要证明如下引理:

引理:设 \(f(n)\) 表示长度为 \(n\) 的序列的极大区间的数量,则 \(f(n)=O(n \log n)\)

证明:我们可以证明,\(f(n) \leq f(\log n!) = f(\log 1 + \cdots + \log n) \leq f(n \log n)\)

考虑原序列的笛卡尔树,设其中一个最大元素的位置为 \(p\),则有:

\[f(n) \leq f(p-1) + f(n - p) + \text{val}(p) \]

其中 \(\text{val}(p)\) 为原序列中包含位置 \(p\) 的极大区间数。不妨设 \(2p \leq n\),我们可以证明,\(\text{val}(p) \leq \mathcal{O}(p \log \frac{n}{p})\)

具体来说,我们设 \(\text{val}_{k}(p)\) 表示包含 \(p\),且权值为 \(a_p + k\) 的极大区间数,则有 \(\text{val}_k(p) \leq \min(p,2^k)\),其中 \(0 \leq k \leq \lceil \log_2 n \rceil\)\(p\) 的限制是因为权值相同的极大区间左端点位置一定不同,而 \(2^k\) 的限制是因为权值从 \(a_p + k - 1\)\(a_p + k\) 必然会经过一次扩展,每次扩展我们可以选择向左或是向右,而我们需要从 \(a_p\) 开始扩展 \(k\) 次。

考虑对所有 \(\text{val}_i(p)\) 求和。注意到,当 \(0 \leq k < \lceil \log_2 p \rceil\) 时,\(\min(p,2^k) = 2^k\),这部分 \(\text{val}_i(p)\) 的和为 \(\mathcal{O}(p)\)。当 \(\lceil \log_2p \rceil \leq k \leq \lceil \log_2 n \rceil\) 时,\(\min(p,2^k) = p\),这部分 \(\text{val}_i(p)\) 的和为 \(\mathcal{O}(p \log \frac{n}{p})\)。于是命题得证。

对于引理的证明,我们考虑归纳。由 \(\mathcal{O}(p \log \frac{n}{p}) \leq \mathcal{O}(\log\frac{n}{p} + \log\frac{n-1}{p-1} + \cdots + \log \frac{n-p+1}{1}) \leq \mathcal{O}(\log \binom{n}{p}) \leq \mathcal{O}(\log \frac{n!}{(p-1)!(n-p)!})\),则有

\[\begin{aligned} f(n) &\leq f(p-1) + f(n-p) + \text{val}(p) \\ &\leq \mathcal{O}(\log(p-1)!) + \mathcal{O}(\log(n-p)!) + \mathcal{O}(\log n! - \log(p-1)! - \log (n-p)!) \\ &\leq \mathcal{O}(n!) \end{aligned} \]

则原命题得证。


接下来只需要考虑如何维护极大区间。对当前的权值 \(v\),我们称一个区间 \([l,r)\) 是一个段,当且仅当其中所有元素都不大于 \(v\),且 \(\min(a_{l-1},a_r) > v\)。对于当前的所有极大区间,其要么权值为 \(v\),要么是一个段。

我们将所有极大区间储存在其所属的段 \([l,r)\) 的左端点的集合 \(\text{seg}_l\) 内。当 \(v-1 \to v\) 时,我们依次执行以下步骤:对所有需要更新的段,更新极大区间并重新计算它们的贡献。合并过后,段内所有极大区间的权值变为 \(v\)。接着在所有 \(a_i = v\) 的位置添加一个极大区间 \([i,i+1)\),然后将相邻的段合并。

使用双向链表维护段的合并,总时间复杂度为 \(\mathcal{O}(A + n \log n)\)。具体细节见代码。

code
#include <bits/stdc++.h>
using namespace std;
#define fi first
#define se second
typedef vector <int> vi;
typedef pair <int, int> pi;
typedef long long LL;
const int N = 3e5 + 5, M = 1e6 + 35;
int n, a[N], pre[N], nex[N]; LL cur;
vi p[M];
list <pi> seg[N];
LL sum(int l, int r) {
    return 1LL * (l + r) * (r - l + 1) / 2;
}
LL calc(list <pi> L) {
    LL ret = 0;
    for (auto it = L.begin(); it != L.end(); it++) {
        auto [x, y] = *it;
        if (next(it) == L.end()) {
            ret += sum(1, y - x);
            break;
        } else {
            int nx = next(it) -> fi;
            ret += 1LL * (nx - x) * y - sum(x, nx - 1);
        }
    }
    return ret;
}
void upd(list <pi> &L) {
    if (L.size() <= 1) 
        return;
    cur += calc(L);
    int mx = -1;
    list <pi> nL;
    auto it = L.begin();
    for (auto [x, y] : L) {
        while (next(it) != L.end() && next(it) -> fi <= y)
            it++;
        if (it -> se > mx) {
            nL.push_back({x, mx = it -> se});
        }
    }
    swap(L, nL);
    cur -= calc(L);
}
int main() {
    ios :: sync_with_stdio(false);
    cin.tie(nullptr);
    cin >> n;
    for (int i = 1; i <= n; i++) {
        cin >> a[i];
        p[a[i]].push_back(i);
    }
    cur = 1LL * n * (n + 1) / 2;
    for (int i = 1; i <= n; i++) 
        pre[i] = nex[i] = i;
    LL ans = 0;
    vi q;
    for (int v = 1; cur > 0; v++) {
        ans += cur;
        vi nq;
        for (auto l : q) {
            upd(seg[l]);
            if (seg[l].size() > 1) nq.push_back(l);
        }
        for (auto i : p[v]) {
            int l = pre[i], r = nex[i + 1];
            bool add = seg[l].size() <= 1;
            nex[l] = r, pre[r] = l;
            seg[l].push_back({i, i + 1});
            cur--;
            seg[l].splice(seg[l].end(), seg[i + 1]);
            add &= seg[l].size() > 1;
            if (add) nq.push_back(l); 
        }
        swap(q, nq);
    }
    cout << ans << "\n";
    return 0;   
}

B. [USACO22OPEN] Hoof and Brain P

给定 \(n\) 个点,\(m\) 条边的无重边、自环的有向图。甲乙两人玩游戏,初始时 \(x,y\) 处各有一个棋子,甲先选择一个棋子,乙必须将这个棋子沿着一边移动,并且需要保证两个棋子不重合。

轮流进行这一过程,若乙不能移动则乙输,若游戏能无限进行则甲输。 \(q\) 组询问 \((x,y)\),问谁必胜。

\(n,q \leq 10^5\)\(m \leq 2 \times 10^5\)


首先可以注意到,如果某个棋子所在的点没有出边,那么甲只需要选择这个棋子就能够获胜。

于是我们可以将这些点标记,然后将它们从原图中删除。但在完成删除操作后,可能又会有新的节点出边数量变成了 \(0\),我们需要重复这样的删除操作只到图中任意节点都有至少 \(1\) 条出边。

现在我们考虑一个只有 \(1\) 条出边的节点 \(x\),假设这条边指向的节点是 \(y\),那么对于任意 \(x\) 上的棋子,移动后都一定会到达 \(y\)。如果甲能够通过让两个棋子都移动到 \(x\) 获胜,那么甲显然也能够通过让两个棋子都移动到 \(y\) 获胜。于是我们可以将 \(x\)\(y\) 合并,具体来说,我们对于每条边 \(z \to x\),将其删除,然后若 \(z \to y\) 不存在则加入边 \(z \to y\)。和之前相同,此时可能又会有新的节点出边数量变为 \(1\),我们需要将它们继续合并。

合并后,新图中的所有点都有至少 \(2\) 条出边,或有一个自环。先考虑所有点都至少有 \(2\) 条出边的情况,显然乙的每次操作都能够避免使两个棋子进入同一个节点。而有自环的情况同样平凡。

于是,对于询问 \((x,y)\),我们可以按照如下方式判定胜负:

\(x\)\(y\) 被删除,那么甲必胜。否则,如果 \(x\)\(y\) 被合并成了一个节点,那么甲必胜,否则乙必胜。

使用启发式合并 set 维护每个节点的出边集合 \(f\) 和入边集合 \(g\),并查集维护合并后每个点所在的集合,时间复杂度 \(\mathcal{O}(m \log n + q)\)

code
#include <bits/stdc++.h>
#define ins insert
#define era erase
using namespace std;
const int N = 2e5 + 5;
int n, m, k, fa[N];
set <int> f[N], g[N]; // f : out, g : in
queue <int> q;
int gf(int x) {
    return x == fa[x] ? x : fa[x] = gf(fa[x]);
}
int main() {
    ios :: sync_with_stdio(false);
    cin.tie(nullptr);
    cin >> n >> m;
    for (int i = 1; i <= n; i++) 
        fa[i] = i;
    for (int i = 1, x, y; i <= m; i++) {
        cin >> x >> y;
        f[x].ins(y);
        g[y].ins(x);
    }
    for (int i = 1; i <= n; i++)
        if (f[i].size() == 0) q.push(i);
    while (!q.empty()) {
        int v = q.front();
        q.pop();
        fa[v] = 0;
        for (auto u : g[v]) {
            f[u].era(v);
            if (f[u].size() == 0) q.push(u);
        }
    }
    for (int i = 1; i <= n; i++)
        if (f[i].size() == 1) q.push(i);
    while (!q.empty()) {
        int x = q.front();
        q.pop();
        int y = 0;
        for (auto v : f[x]) y = v;
        x = gf(x);
        y = gf(y);
        if (x == y) continue;
        if (g[x].size() > g[y].size())
            swap(x, y);
        for (auto z : g[x]) {
            f[z].era(x);
            f[z].ins(y);
            g[y].ins(z);
            if (f[z].size() == 1) q.push(z);
        }
        fa[x] = y;
    }
    cin >> k;
    while (k--) {
        int x, y; cin >> x >> y;
        x = gf(x);
        y = gf(y);
        if (x == 0 || y == 0 || x == y) {
            cout << "B";
        } else {
            cout << "H";
        }
    }
    return 0;   
}

C. [USACO22OPEN] Up Down Subsequence P

给定一个长度为 \(n\) 的排列 \(p\) 和一个长为 \(n-1\) 的字符串 \(s\),其中每一位都是 UD

求最大的 \(k \leq n - 1\),使得存在 \(p\) 的一个子序列 \(a_0,a_1,\cdots,a_k\),满足对于所有 \(1 \leq j \leq k\),当 \(s_j\)U\(a_{j - 1} < a_j\),当 \(s_j\)D\(a_{j-1} > a_j\)

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


以下我们将 U 的极长连续段称为上升段, D 的极长连续段称为下降段。

很容易想到一个直观的贪心:即维护当前的子序列 \(a\),每次找到最近的满足 \(s\) 限制的位置。但很遗憾,这个做法甚至在 \(s\) 全为 U 时都是错的。

但这也给了我们启示,虽然在 LIS 和 LDS 问题中我们不能使用贪心,但我们可以试着在上升段和下降段交换时使用这种贪心的方式。具体来说,假设 \(s_j\)D,且接下来的 \(x\) 个位置都是 U。考虑能和 \(s_1 \sim s_j\) 对应的所有 \(p\) 的子序列 \(a\),设其结尾位置 \(a_j\) 的最小值为 \(k\),我们找到在 \(p_k \sim p_n\) 内长度为 \(x+1\) 的,结尾位置最靠前的上升子序列 \(b\),设 \(b\) 的结尾位置为 \(k'\)

显然,对于 \(p\) 的能和 \(s_1 \sim s_{j + x}\) 对应的子序列,其结尾位置一定 \(\geq k'\)。我们断言,根据序列 \(a\)\(b\) 一定能够构造出一个子序列使得其和 \(s_1 \sim s_{j + x}\) 对应,且结尾位置为 \(k'\)

分类讨论:若 \(a_j = b_0\),那就只需要把它们拼在一起就行了。否则,注意到此时一定有 \(a_j > b_0\),否则我们可以把 \(a_j\) 加在序列 \(b\) 的开头,然后把 \(b\) 的末尾元素删掉,这样能够得到一个结尾位置更靠前的长为 \(x+1\) 的上升子序列。于是我们可以把 \(a_j\) 删掉,然后将 \(b\) 接在后面即可。

U 变为 D 时同理。也就是说,这种贪心在不同段落交换时都是成立的。

于是我们的算法可以按照如下流程进行:将 \(s\) 划分成若干上升段和下降段,维护当前子序列的结尾位置 \(t\),对于一个长为 \(x\) 的段,我们试着找到在 \(p_t \sim p_n\) 内长度为 \(x+1\) 的,结尾位置最靠前的上升/下降子序列,若存在这样的子序列,设其结尾位置为 \(t'\),我们将答案加上 \(x\),然后令 \(t \gets t'\),否则将答案加上后缀的 LIS 或 LDS 的长度,并结束计算。

容易通过树状数组优化 DP 来维护上述过程,时间复杂度 \(\mathcal{O}(n \log n)\)

code
#include <bits/stdc++.h>
using namespace std;
typedef long long LL;
const int N = 3e5 + 5;
int n, ans, a[N], q[N], c, f[N];
struct BIT {
    int c[N];
    void clr(int x) {
        for (int i = x; i <= n; i += i & -i) c[i] = 0;
    }
    void mdf(int x, int v) {
        for (int i = x; i <= n; i += i & -i) c[i] = max(c[i], v);
    }
    int qry(int x) {
        int ret = 0;
        for (int i = x; i >= 1; i -= i & -i) ret = max(ret, c[i]);
        return ret;
    }
} t;
int main() {
    ios :: sync_with_stdio(false);
    cin.tie(nullptr);
    cin >> n;
    for (int i = 1; i <= n; i++) cin >> a[i];
    string s;
    cin >> s;
    q[++c] = 1;
    for (int i = 1; i < n - 1; i++) {
        if (s[i] == s[i - 1]) {
            q[c]++;
        } else {
            q[++c] = 1;
        }
    }
    bool _ = s[0] == 'U';
    int k = 1, j = 1;
    for (int i = 1; i <= c; i++, _ ^= 1) {
        if (j == n) 
            break;
        k = j;
        int x = q[i] + 1, val = 0;
        if (_) {
            f[j] = 1, t.mdf(a[j], 1);

            while (j < n && f[j] < x) {
                j++;
                f[j] = t.qry(a[j]) + 1, t.mdf(a[j], f[j]);
            }
            while (k <= j) {
                t.clr(a[k]), val = max(val, f[k]), k++;
            } 
        } else {
            f[j] = 1, t.mdf(n - a[j] + 1, 1);

            while (j < n && f[j] < x) {
                j++;
                f[j] = t.qry(n - a[j] + 1) + 1, t.mdf(n - a[j] + 1, f[j]);
            }
            while (k <= j) {
                t.clr(n - a[k] + 1), val = max(val, f[k]), k++;
            } 
        }
        if (j < n) {
            ans += q[i];
        } else {
            ans += val - 1;
        }
    }
    cout << ans << "\n";
    return 0;   
}
posted @ 2023-01-25 17:26  came11ia  阅读(120)  评论(0编辑  收藏  举报