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\),其中每一位都是 U
或 D
。
求最大的 \(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;
}