USACO2026 JAN1 Gold 简要题解
预估难度 *2100, *2400, *2300,对应洛谷蓝蓝蓝。个人最喜欢这场的 B 题,分析性质的过程比较有趣。
卡线 AK 的,感觉半夜打比赛脑子还是太不清晰了,比赛里至少有 2.5h 都在调题和写拍子。
A
Sol.1 吉司机线段树
一开始没动脑想的诡异搞笑做法。
对基环树缩点,然后根据修改的点分类讨论:
- 如果是非根节点,用吉司机线段树对 DFN 区间进行区间深度取 \(\max\),维护区间深度最小值的个数,即可得知修改后的变化量。
- 如果是根节点,则沿着环按照之前的做法往前继续修改即可。只要让他们的 DFS 序是相邻的即可做到一起将他们修改。
Sol.2 并查集
因为这种操作相当于把原来的一大块打成了多个小块,所以很自然地想到时光倒流的套路。
把最后有派对的点打上标记,将所有没有派对的点 \(u\) 向其父节点 \(f_u\) 处合并。计算出每种颜色对应的人数后,考虑撤销本次的操作。具体而言,对点的状态进行分类讨论:
- 若本次操作之前 \(u\) 处就有派对,那么直接将这个连通块的贡献加到另一个颜色上就好了。
- 否则:
- 若本次操作后会回到无颜色的状态,即 \(f_u, u\) 已经在同一个连通块中,直接把自己的贡献删掉。
- 否则把 \(u\) 合并到 \(f_u\) 去,并同步更改其贡献。
找到撤销操作后的前一次染色可以使用邻接表 + 指针的方式实现,时间复杂度 \(O(n)\)。这个题细节很多,可以打个拍子。
#include <bits/stdc++.h>
#define fi first
#define se second
#define eb(x) emplace_back(x)
#define pb(x) push_back(x)
#define lc(x) (tr[x].ls)
#define rc(x) (tr[x].rs)
using namespace std;
typedef long long ll;
typedef unsigned long long ull;
typedef long double ldb;
typedef __int128 i128;
using pi = pair<int, int>;
const int N = 200005;
int n, m, a[N], final_c[N], typ[N], Qc[N], Qv[N], ans[5], p[N], orians[N][5];
vector<int> tot[N];
vector<int> g[N];
struct DSU{
int fa[N], sz[N];
void init()
{
for(int i = 1; i <= n; i++) fa[i] = i, sz[i] = 1;
}
int findf(int x)
{
if(fa[x] != x) fa[x] = findf(fa[x]);
return fa[x];
}
void combine(int x, int y)
{
int fx = findf(x), fy = findf(y);
if(fx == fy) return;
fa[fx] = fy;
sz[fy] += sz[fx];
}
} dsu;
int main()
{
// freopen("Gold_A.in", "r", stdin);
// freopen("Gold_A.out", "w", stdout);
ios::sync_with_stdio(0);
cin.tie(0);
cout.tie(0);
cin >> n;
dsu.init();
for(int i = 1; i <= n; i++)
cin >> a[i];
cin >> m;
for(int i = 1; i <= m; i++)
{
char c;
cin >> Qc[i] >> c;
if(c == 'C') Qv[i] = 1;
else if(c == 'O') Qv[i] = 2;
else Qv[i] = 3;
final_c[Qc[i]] = Qv[i];
typ[Qc[i]] = Qv[i];
tot[Qc[i]].push_back(Qv[i]);
p[Qc[i]] = tot[Qc[i]].size() - 1;
}
for(int i = 1; i <= n; i++)
{
if(final_c[i]) continue;
dsu.combine(i, a[i]);
}
for(int i = 1; i <= n; i++)
if(final_c[i])
ans[final_c[i]] += dsu.sz[i];
for(int i = m; i >= 1; i--)
{
for(int j = 1; j <= 3; j++) orians[i][j] = ans[j];
int u = Qc[i], c = Qv[i];
ans[c] -= dsu.sz[u];
if(p[u] == 0)
{
int anc = dsu.findf(a[u]);
if(anc == dsu.findf(u))
{
typ[anc] = 0;
continue;
}
ans[typ[anc]] -= dsu.sz[anc];
dsu.combine(u, a[u]);
ans[typ[anc]] += dsu.sz[anc];
continue;
}
p[u]--;
ans[tot[u][p[u]]] += dsu.sz[u];
typ[u] = tot[u][p[u]];
}
for(int i = 1; i <= m; i++)
{
for(int j = 1; j <= 3; j++)
{
cout << orians[i][j];
if(j < 3) cout << " ";
}
if(i < m) cout << "\n";
}
return 0;
}
B
分析性质的清新思维题。
先考虑最优化的策略应当是怎样的。发现让两个数变为 \(\dfrac{x+y}{2}\) 相当于把原序列的和减去了 \(\dfrac{x+y}{2}\),而我们要最大化最后一个元素就相当于最大化最后留下来的序列的元素和。
依据上面的分析不难发现,我们每次选择最小的两个数合并在一起就是最优策略。
那么对交换后数组的形态进行分析,则我们合并的路线一定是蛇形往返的。因此可以得到第二个性质:最终形成的数组一定是单谷形态的。
问题被转化为了最少要操作多少次才能让序列变为单谷。
我们转换思路,先不从最小值的角度入手,而是观察最大值的位置。发现最大值此时一定是在序列的左边或者右边,而它无论到了哪一边都对我们后续的操作无影响,即不存在后效性。
因此可以从大到小遍历数字,判断它是往左还是往右,选择一个移动次数更小的进行操作即可。可以使用树状数组维护区间内未选完的数字个数。
时间复杂度 \(O(n\log n)\)。
#include <bits/stdc++.h>
#define fi first
#define se second
#define eb(x) emplace_back(x)
#define pb(x) push_back(x)
#define lc(x) (tr[x].ls)
#define rc(x) (tr[x].rs)
using namespace std;
typedef long long ll;
typedef unsigned long long ull;
typedef long double ldb;
typedef __int128 i128;
using pi = pair<int, int>;
const int N = 500005, V = 1000000000;
int n, a[N], flag[N];
pi b[N];
ll ans = 0;
int lowbit(int x)
{
return (x & (-x));
}
struct BIT{
int tr[N];
void init()
{
for(int i = 0; i <= n; i++) tr[i] = 0;
}
void update(int p, int v)
{
while(p <= n)
{
tr[p] += v;
p += lowbit(p);
}
}
int query(int p)
{
if(p == 0) return 0;
int res = 0;
while(p)
{
res += tr[p];
p -= lowbit(p);
}
return res;
}
int query_range(int l, int r)
{
return (query(r) - query(l - 1));
}
} tr1;
void solve()
{
cin >> n;
tr1.init();
for(int i = 1; i <= n; i++)
{
tr1.update(i, 1);
flag[i] = 1;
cin >> a[i];
b[i] = {V - a[i], i};
}
sort(b + 1, b + n + 1);
ans = 0;
for(int i = 1; i <= n; i++)
{
int pos = b[i].se;
if(flag[i])
{
for(int j = i; j <= n && b[j].fi == b[i].fi; j++)
{
tr1.update(b[j].se, -1);
flag[j] = 0;
}
}
int lx = 0, rx = 0;
if(pos != 1) lx = tr1.query_range(1, pos - 1);
if(pos != n) rx = tr1.query_range(pos + 1, n);
ans += min(lx, rx);
}
cout << ans << "\n";
}
int main()
{
//freopen("sample.in", "r", stdin);
//freopen("sample.out", "w", stdout);
ios::sync_with_stdio(0);
cin.tie(0);
cout.tie(0);
int t;
cin >> t;
while(t--) solve();
return 0;
}
C
容易发现在教练的集合确定了之后,可以直接求出营员的方案数,即 \(2^k\),其中 \(k\) 为覆盖的营员数量。
又因为每个教练是向右覆盖的,所以我们可以设计 DP:\(dp_i\) 表示教练 \(i\) 为从右往左的最后一个教练的方案数。然后从右往左遍历每个人,用区间加乘的线段树维护 DP 值的转移:
- 若这个人为教练,则 \(dp_i\gets (sum + 1)\times 2^k\)。其中 \(sum\) 表示线段树上 \(i + 1\sim n\) 的区间和,\(k\) 表示 \([p_i, p_i + k]\) 里的营员个数。这个可以通过维护一个队列实现。
- 若这个人为营员,则让 \([p_i - d, p_i]\) 之间的教练在线段树上的转移系数乘上 \(2^{-1}\),因为需要把重叠部分的贡献抵消。
- 在教练的队列弹出一个营员的时候,要同时把他的贡献撤销。
就此转移,时间复杂度 \(O(n\log n)\)。
#include <bits/stdc++.h>
#define fi first
#define se second
#define eb(x) emplace_back(x)
#define pb(x) push_back(x)
#define lc (p << 1)
#define rc ((p << 1) | 1)
using namespace std;
typedef long long ll;
typedef unsigned long long ull;
typedef long double ldb;
using pi = pair<int, int>;
const int N = 1000005;
const ll mod = 1e9 + 7, inv2 = 500000004;
ll n, d, a[N], typ[N], dp[N], pw2[N], ans;
struct Tag{
ll k, b;
Tag operator + (const Tag & t) const{
Tag res = {
k * t.k % mod,
(t.k * b % mod + t.b) % mod
};
return res;
}
};
struct Info{
ll sm, len;
Info operator + (const Info & t) const{
Info res = {
(sm + t.sm) % mod,
len + t.len
};
return res;
}
Info operator * (const Tag & t) const{
Info res = {
(t.k * sm % mod + t.b * len % mod) % mod,
len
};
return res;
}
};
struct Node{
int l, r;
bool havetag;
Info info;
Tag tag;
};
struct Segtree{
Node tr[4 * N];
void pushup(int p)
{
tr[p].info = tr[lc].info + tr[rc].info;
}
void modify(int p, Tag tag)
{
tr[p].info = tr[p].info * tag;
if(tr[p].havetag) tr[p].tag = tr[p].tag + tag;
else
{
tr[p].havetag = 1;
tr[p].tag = tag;
}
}
void pushdown(int p)
{
if(tr[p].havetag)
{
modify(lc, tr[p].tag);
modify(rc, tr[p].tag);
}
tr[p].havetag = 0;
}
void build(int p, int ln, int rn)
{
tr[p] = {ln, rn, 0, {0, rn - ln + 1}, {1, 0}};
if(ln == rn) return;
int mid = (ln + rn) >> 1;
build(lc, ln, mid);
build(rc, mid + 1, rn);
pushup(p);
}
void update(int p, int ln, int rn, Tag tag)
{
if(ln <= tr[p].l && tr[p].r <= rn)
{
modify(p, tag);
return;
}
int mid = (tr[p].l + tr[p].r) >> 1;
pushdown(p);
if(ln <= mid) update(lc, ln, rn, tag);
if(rn >= mid + 1) update(rc, ln, rn, tag);
pushup(p);
}
Info query(int p, int ln, int rn)
{
if(ln <= tr[p].l && tr[p].r <= rn) return tr[p].info;
int mid = (tr[p].l + tr[p].r) >> 1;
pushdown(p);
if(rn <= mid) return query(lc, ln, rn);
if(ln >= mid + 1) return query(rc, ln, rn);
return (query(lc, ln, rn) + query(rc, ln, rn));
}
} tr1;
void init()
{
pw2[0] = 1;
for(int i = 1; i < N; i++) pw2[i] = (pw2[i - 1] << 1) % mod;
}
int main()
{
ios::sync_with_stdio(0);
cin.tie(0);
cout.tie(0);
init();
cin >> n >> d;
tr1.build(1, 1, n);
for(int i = 1; i <= n; i++)
{
cin >> a[i] >> typ[i];
if(typ[i]) tr1.update(1, i, i, {1, 1});
}
queue<int> q;
for(int i = n; i >= 1; i--)
{
if(typ[i] == 0)
{
int lx = lower_bound(a + 1, a + n + 1, a[i] - d) - a - 1;
lx = max(lx, 1);
if(lx <= i) tr1.update(1, lx, i, {inv2, 0});
q.push(i);
}
else
{
while(!q.empty() && a[q.front()] > a[i] + d)
{
int lx = lower_bound(a + 1, a + n + 1, a[q.front()] - d) - a - 1;
lx = max(lx, 1);
if(lx <= q.front()) tr1.update(1, lx, q.front(), {2, 0});
q.pop();
}
dp[i] = ((i + 1 <= n ? tr1.query(1, i + 1, n).sm : 0) + 1) * pw2[q.size()] % mod;
ans = (ans + dp[i]) % mod;
tr1.update(1, i, i, {dp[i], 0});
}
}
cout << ans;
return 0;
}

浙公网安备 33010602011771号