Say 赛选记(10.12 - 10.18)
10.14
P5426 [USACO19OPEN] Balancing Inversions G
只考虑组内交换的情况,可以发现答案上限是前半部分的逆序对数量 \(A\) 与后半部分的逆序对数量 \(B\) 的差的绝对值 \(|A - B|\)。组间交换的时候,假设我们现在是把前面的 0 和后面的一个 1 互换。由于要求步数最小,肯定是找到前半部分最后面那个 0 和后半部分最前面那个 1 进行交换。然后可以讨论一下算出交换之后 \(A\) 和 \(B\),如果减量比步数小,那么这次交换就是比直接组内交换更值的,一直做直到不值就行。
Code
#include <bits/stdc++.h>
using namespace std;
const int N = 1e5 + 5;
priority_queue<int> a[2];
priority_queue<int, vector<int>, greater<int> > b[2];
int An, Bn, x[N], n, y[N], ans;
int get(int a[]){
int sum[2] = {0}, ret = 0;
for(int i = n; i >= 1; --i){
sum[a[i]]++;
if(a[i] == 1) ret += sum[0];
}
return ret;
}
void swap(int op){
int x = a[op].top(), y = b[op ^ 1].top();
a[op].pop(), b[op ^ 1].pop();
a[op ^ 1].push(x), b[op].push(y);
}
int main(){
cin.tie(nullptr)->sync_with_stdio(0);
cin >> n;
for(int i = 1; i <= n; ++i) cin >> x[i], a[x[i]].emplace(i);
for(int i = 1; i <= n; ++i) cin >> y[i], b[y[i]].emplace(i);
An = get(x), Bn = get(y);
while(1){
bool fl = 0;
if(!a[0].empty() && !b[1].empty()){
int x = a[0].top(), y = b[1].top(),
nxtAn = An + n - x - a[1].size(), nxtBn = Bn + y - 1 - b[0].size();
int delta = abs(nxtAn - nxtBn) - abs(An - Bn), step = n - x + y;
if(delta < 0 && -delta > step)
swap(0), An = nxtAn, Bn = nxtBn, fl = 1, ans += step;
}
if(!a[1].empty() && !b[0].empty()){
int x = a[1].top(), y = b[0].top(),
nxtAn = An - (n - x) + a[1].size() - 1, nxtBn = Bn - (y - 1) + b[0].size() - 1;
int delta = abs(nxtAn - nxtBn) - abs(An - Bn), step = n - x + y;
if(delta < 0 && -delta > step)
swap(1), An = nxtAn, Bn = nxtBn, fl = 1, ans += step;
}
if(!fl) break;
}
cout << abs(An - Bn) + ans;
return 0;
}
P5835 [USACO19DEC] Meetings S
当 \(w_i = 1\) 时,各个奶牛之间是没有区别的。当奶牛之间没有区别时,对于这种相遇之后要碰撞折返的题,有一个常见的考虑方式,就是让奶牛们由碰撞折返变成互相穿过。那么如果我们已经求出了 \(T\),就可以直接看有哪些奶牛是能在 \(T\) 时间内与它相遇而过的,这个可以简单模拟。
考虑如何求出 \(T\)。实际上上面的想法稍作扩展,可以发现我们只需要在两头奶牛彼此穿过的同时把他们的重量相互交换可以达到同样的效果。手玩之后发现,第 \(k\) 个 \(d_i = -1\) 到达 0 时的重量就是原来左侧第 \(k\) 个奶牛的重量。按到达时间排序之后,一直加,直到总重量达到一半即可,此时得到的时间就是 \(T\)。
Code
#include <bits/stdc++.h>
using namespace std;
const int N = 5e4 + 5;
typedef pair<int, int> pii;
typedef tuple<int, int, int> tpi;
int n, L, ans;
tpi p[N];
priority_queue<pii, vector<pii>, greater<pii> > q;
int main(){
cin.tie(nullptr)->sync_with_stdio(0);
cin >> n >> L;
int all = 0, l = 0, r = n + 1;
for(int i = 1; i <= n; ++i){
int w, x, d;
cin >> w >> x >> d;
p[i] = tpi{x, d, w};
all += w;
}
sort(p + 1, p + 1 + n);
for(int i = 1; i <= n; ++i){
int x, d;
tie(x, d, ignore) = p[i];
if(d == -1) q.emplace(x, get<2>(p[++l]));
}
for(int i = n; i >= 1; --i){
int x, d;
tie(x, d, ignore) = p[i];
if(d == 1) q.emplace(L - x, get<2>(p[--r]));
}
int sum = 0, T;
while(!q.empty()){
sum += q.top().second;
if(sum * 2 >= all){
T = q.top().first;
break;
}
q.pop();
}
queue<int> q;
for(int i = 1; i <= n; ++i){
int x, d;
tie(x, d, ignore) = p[i];
if(d == -1){
while(!q.empty() && q.front() < x - 2 * T) q.pop();
ans += q.size();
}
else q.push(x);
}
cout << ans;
return 0;
}
10.15
P6100 [USACO19FEB] Painting the Barn G
当多覆盖一个矩形时,矩形内本来已经被覆盖 \(k\) 层的会让答案 \(-1\),原来已经被覆盖 \(k - 1\) 层的会让答案 \(+1\)。我们 \(dp\) 这个增量。要求的就是两个互不相交的最大子矩阵。一道类似的题。
Code
#include <bits/stdc++.h>
using namespace std;
const int N = 2e2 + 5, v = 2e2;
int n, k, sum[N][N], suma[N][N], a[N][N], f[N][N], b[N], g[N], p[N], q[N], tmp[N][N];
int ans, tot;
void add(int a, int b, int c, int d){
sum[a][b]++, sum[a][d + 1]--, sum[c + 1][b]--, sum[c + 1][d + 1]++;
}
int ask(int sum[N][N], int a, int b, int c, int d){
return sum[c][d] - sum[a - 1][d] - sum[c][b - 1] + sum[a - 1][b - 1];
}
void chmax(int &x, int y){ x = max(x, y); }
void init(){
for(int i = 1; i <= v; ++i){
for(int j = 1; j <= v; ++j){
sum[i][j] = sum[i][j - 1] + sum[i - 1][j] - sum[i - 1][j - 1] + sum[i][j];
}
}
}
void rotate(){
for(int i = 1; i <= v; ++i){
for(int j = 1; j <= v; ++j){
tmp[i][j] = a[j][i];
}
}
for(int i = 1; i <= v; ++i){
for(int j = 1; j <= v; ++j)
a[i][j] = tmp[i][j];
}
}
void work(){
for(int i = 1; i <= v; ++i){
for(int j = 1; j <= v; ++j){
suma[i][j] = suma[i - 1][j] + suma[i][j - 1] - suma[i - 1][j - 1] + a[i][j];
}
}
memset(f, -0x3f, sizeof(f));
for(int j = 1; j <= v; ++j){
for(int i = j; i <= v; ++i){
memset(g, -0x3f, sizeof(g));
for(int k = 1; k <= v; ++k){
b[k] = ask(suma, j, k, i, k);
g[k] = max(0, g[k - 1]) + b[k];
chmax(f[j][i], g[k]);
}
}
}
memset(p, -0x3f, sizeof(p));
memset(q, -0x3f, sizeof(q));
for(int i = 1; i <= v; ++i){
p[i] = p[i - 1];
for(int j = 1; j <= i; ++j){
chmax(p[i], f[j][i]);
}
}
for(int i = v; i >= 1; --i){
q[i] = q[i + 1];
for(int j = i; j <= v; ++j){
chmax(q[i], f[i][j]);
}
}
for(int i = 1; i <= v; ++i){
chmax(ans, p[i]);
}
for(int i = 1; i < v; ++i){
chmax(ans, p[i] + q[i + 1]);
}
}
int main(){
cin.tie(nullptr)->sync_with_stdio(0);
cin >> n >> k;
for(int i = 1; i <= n; ++i){
int a, b, c, d;
cin >> a >> b >> c >> d;
++a, ++b;
add(a, b, c, d);
}
init();
int res = 0; tot = 0;
for(int i = 1; i <= v; ++i){
for(int j = 1; j <= v; ++j){
if(sum[i][j] == k) a[i][j] = -1, res++;
else if(sum[i][j] == k - 1) a[i][j] = 1;
else a[i][j] = 0;
}
}
work();
rotate();
work();
cout << ans + res;
return 0;
}
10.16
P6099 [USACO19FEB] Dishwashing G
不要想当然啊,以为只有第一个栈能做弹栈的操作。
考虑模拟这个过程,策略是能先不弹就不弹。发现在这种策略下,每个点放入的位置是固定的。然后当发现要放入的那个栈的栈顶 \(x\) 比当前要放入的数 \(a_i\) 要小,那就得把这个栈前面的栈先全部都弹出来,再把这个栈中比它小的那些也弹出来,再把这个数放进去。然后如果后来来了一个数,比已经弹出的数的最大值要小的话,那一定就不合法了。
Code
#include <bits/stdc++.h>
using namespace std;
const int N = 1e5 + 5;
vector<int> st[N], tp;
int n, a[N], cnt, lim;
int main(){
cin.tie(nullptr)->sync_with_stdio(0);
cin >> n;
for(int i = 1; i <= n; ++i){
cin >> a[i];
if(a[i] < lim) return cout << i - 1, 0;
auto it = upper_bound(tp.begin(), tp.end(), a[i]);
if(it == tp.end()) st[cnt++].emplace_back(a[i]), tp.emplace_back(a[i]);
else{
int x = it - tp.begin();
while(a[i] > st[x].back()){
lim = st[x].back();
st[x].pop_back();
}
st[x].push_back(a[i]);
}
}
cout << n;
return 0;
}
P11674 [USACO25JAN] Reachable Pairs G
如果只有类型 0 的点,那直接反过来加点做就好了。同时,发现一个类型为 1 的点删掉之后不影响连通性的。那么我们在反过来加点的时候就得考虑到前面 1 的影响,可能使现在一些 0 的点互相连起来。手玩一下发现,如果两个 0 的点能通过一些 1 的点互相到达,那么他们两个就得被计算为连通。因此正过来做一遍,把为 1 的那些点之间互相有边的先并成一个连通块(类似那个点权转化为连通性的 trick)。然后在反过来加点时,维护连通性,但是答案只维护 \(\ge\) 编号 \(i\) 的那些点的答案。具体来说是通过这个 add 函数实现的,可以看看代码。
这给了我们一种建“虚点”维护连通性但不把它算进并查集 \(siz\) 里的启发。
Code
#include <bits/stdc++.h>
using namespace std;
#define int long long
const int N = 2e5 + 5;
vector<int> e[N];
int n, m, f[N], siz[N], ans;
bitset<N> s;
int getf(int u){ return (f[u] != u ? f[u] = getf(f[u]) : f[u]); }
void init(){
for(int i = 1; i <= n; ++i) f[i] = i, siz[i] = 0;
}
void merge(int u, int v){
u = getf(u), v = getf(v);
if(u == v) return;
ans += siz[u] * siz[v];
f[v] = u, siz[u] += siz[v], siz[v] = 0;
}
void add(int u){
u = getf(f[u]);
ans += siz[u];
siz[u]++;
}
signed main(){
cin.tie(nullptr)->sync_with_stdio(0);
cin >> n >> m;
for(int i = 1; i <= n; ++i){
char c;
cin >> c;
if(c == '1') s[i] = 1;
}
init();
for(int i = 1; i <= m; ++i){
int u, v;
cin >> u >> v;
e[u].emplace_back(v);
e[v].emplace_back(u);
if(s[u] && s[v]) merge(u, v);
}
vector<int> tmp;
for(int i = n; i >= 1; --i){
for(int j : e[i]) if(j > i || s[j]) merge(i, j);
add(i);
tmp.emplace_back(ans);
}
while(!tmp.empty()) cout << tmp.back() << '\n', tmp.pop_back();
return 0;
}
P11673 [USACO25JAN] Median Heap G
单次 \(O(N)\) 的 dp 是 trivial 的。
我们发现每次初始点权的结果只与 \(a_i\) 与 \(m\) 的大小关系有关,并且当点权发生变化时,只影响到他祖先的 dp 值。具体来说,当 \(m\) 从 \(a_i - 1 \to a_i\) 时,它的初始点权会发生一次变化,当 \(m\) 从 \(a_i \to a_i + 1\) 时,又发生一次变化。
我们把这些变化的点存下来,去重后从小到大遍历,每次对影响到的点和他们的祖先做更新,把答案存下来,这样就是 \(O(N \log N)\) 的预处理。
回答询问时,就找到这个 \(m\) 对应的那个答案即可。
Code
#include <bits/stdc++.h>
using namespace std;
#define int long long
const int N = 2e5 + 5;
#define ls(u) (u << 1)
#define rs(u) (u << 1 | 1)
#define fa(u) (u >> 1)
int n, a[N], c[N], f[N][3], g[N][3], ans[2 * N], buc[2 * N], tot, m;
map<int, vector<int> > mp;
vector<int> ver;
bitset<N> fl;
void upd(int i){
f[i][0] = (a[i] != m) * c[i];
f[i][1] = (a[i] >= m) * c[i];
f[i][2] = (a[i] <= m) * c[i];
}
void dfs(int u){
if(u > n) return;
int x = ls(u), y = rs(u);
if(fl[x]) dfs(x);
if(fl[y]) dfs(y);
upd(u);
if(x > n) return;
g[u][0] = min({f[u][0] + min(f[x][0], f[x][1]) + min(f[y][0], f[y][2]),
f[u][0] + min(f[x][0], f[x][2]) + min(f[y][0], f[y][1]),
f[x][0] + min(f[u][0], f[u][1]) + min(f[y][0], f[y][2]),
f[x][0] + min(f[u][0], f[u][2]) + min(f[y][0], f[y][1]),
f[y][0] + min(f[u][0], f[u][1]) + min(f[x][0], f[x][2]),
f[y][0] + min(f[u][0], f[u][2]) + min(f[x][0], f[x][1])});
g[u][1] = min({f[u][1] + f[x][1] + min({f[y][0], f[y][1] , f[y][2]}),
f[u][1] + f[y][1] + min({f[x][0], f[x][1] , f[x][2]}),
f[x][1] + f[y][1] + min({f[u][0], f[u][1], f[u][2]})});
g[u][2] = min({f[u][2] + f[x][2] + min({f[y][0], f[y][1] , f[y][2]}),
f[u][2] + f[y][2] + min({f[x][0], f[x][1] , f[x][2]}),
f[x][2] + f[y][2] + min({f[u][0], f[u][1], f[u][2]})});
f[u][0] = g[u][0], f[u][1] = g[u][1], f[u][2] = g[u][2];
}
signed main(){
cin.tie(nullptr)->sync_with_stdio(0);
cin >> n;
for(int i = 1; i <= n; ++i){
cin >> a[i] >> c[i];
mp[a[i]].emplace_back(i);
buc[++tot] = a[i], buc[++tot] = a[i] + 1;
}
sort(buc + 1, buc + 1 + tot);
tot = unique(buc + 1, buc + 1 + tot) - buc - 1;
fl.set();
m = -1;
dfs(1);
ans[0] = f[1][0];
fl.reset();
for(int i = 1; i <= tot; ++i){
m = buc[i]; ver.clear();
for(int j : mp[m]) ver.emplace_back(j);
for(int j : mp[m - 1]) ver.emplace_back(j);
for(int u : ver){
for(int i = u; i; i = fa(i)) fl[i] = 1;
}
dfs(1);
ans[i] = f[1][0];
for(int u : ver){
for(int i = u; i; i = fa(i)) fl[i] = 0;
}
}
int q; cin >> q;
while(q--){
int x; cin >> x;
int pos = upper_bound(buc + 1, buc + 1 + tot, x) - buc - 1;
cout << ans[pos] << '\n';
}
return 0;
}
P11675 [USACO25JAN] Photo Op G
首先可以发现,Bessie 走的路径一定是 \((X, 0) \to (x_i, 0) \to (0, y_i) \to (0, Y)\)。
一些相交的线段会让一些区域不可行走,比如下图中的蓝色区域。

反过来考虑更自然一些,如果一个极大的区域 \(\forall x \in [x_1, x_2]\) 都可以到 \(\forall y \in [y_1, y_2]\) 那么称 \([x_1, x_2, y_1, y_2]\) 是一个好区域。定义一个 \(\text{to_range}(i, l, r) = \begin{cases} l & i < l \\
i & l \le i \le r \\
r & i > r \end{cases}\)。
那么通过这个好区域的最短距离 \((X, 0) \to (\text{to_range}(X, x_1, x_2), 0) \to (0, \text{to_range}(Y, y_1, y_2)) \to (0, Y)\)。
考虑维护不可通行的区域,由于这些区域有单调性。我们考虑按 \(x_2\) 从小到大维护。加入一条线段时,二分找到前后的两个已经存在的不可通行区域,如果他们有相交,尝试合并。由于好的区域就是夹在两个不可通行区域之间的,合并的同时也可以维护好的区域,但注意我们不需要真的维护出来,因为这只是方便计算答案,因此我们只要维护通过每个好区域的最短距离集合,每次找最小值就是真正的答案了。每次删除插入好区域时同时删除插入它对应的贡献。
不可通行区域可以用 \(set\) 维护,类似珂朵莉,而答案集合用 \(multiset\) 维护。
Code
#include <bits/stdc++.h>
using namespace std;
#define int long long
typedef pair<int, int> pii;
const int N = 3e5 + 5, inf = 1e9;
struct node{
int lx, rx, ly, ry;
bool operator < (const node &b) const{
return rx < b.rx;
}
};
set<node> s;
multiset<int> ans;
vector<pii> op[N];
int n, T, X, Y;
bool cross(node a, node b){
if(a.rx < b.lx && a.ry < b.ly) return 0;
return 1;
}
void merge(node &x, node y){
x = {min(x.lx, y.lx), max(x.rx, y.rx),
min(x.ly, y.ly), max(x.ry, y.ry)};
}
int cost(node x, node y){
int lx = x.rx, rx = y.lx, ly = x.ry, ry = y.ly;
int st = X, en = Y, ret = 0;
auto to_range = [&](int &x, int l, int r){
if(x < l) ret += l - x, x = l;
if(x > r) ret += x - r, x = r;
};
to_range(st, lx, rx);
to_range(en, ly, ry);
ret += (int)floor(sqrt(st * st + en * en));
return ret;
}
void erase(int x){ ans.erase(ans.find(x)); }
void del(node a){
auto it = s.find(a);
assert(it != s.end());
erase(cost(*prev(it), *it));
erase(cost(*it, *next(it)));
ans.insert(cost(*prev(it), *next(it)));
s.erase(it);
}
void ins(node a){
auto it = s.insert(a).first;
ans.insert(cost(*prev(it), *it));
ans.insert(cost(*it, *next(it)));
erase(cost(*prev(it), *next(it)));
}
signed main(){
cin >> n >> T >> X >> Y;
for(int x : {0ll, inf}) s.insert({x, x, x, x});
ans.insert(cost({0, 0, 0, 0}, {inf, inf, inf, inf}));
for(int i = 1; i <= n; ++i){
int s, x, y;
cin >> s >> x >> y;
op[s].emplace_back(x, y);
}
for(int i = 0; i < T; ++i){
for(auto [x, y] : op[i]){
node p = {x, x, y, y};
while(1){
auto it = s.lower_bound(p);
if(cross(p, *it)){
node t = *it;
merge(p, t);
del(t);
continue;
}
if(cross(*prev(it), p)){
node t = *prev(it);
merge(p, t);
del(t);
continue;
}
ins(p);
break;
}
}
cout << *ans.begin() << '\n';
}
return 0;
}

浙公网安备 33010602011771号