USACO22DEC G【杂题】
A. [USACO22DEC] Bribing Friends G
有 \(n\) 个朋友,第 \(i\) 个朋友有人气值 \(p_i\)。第 \(i\) 个朋友能出现,当且仅当你给它 \(c_i\) 块钱。如果你给第 \(i\) 个朋友 \(x_i\) 个冰淇淋,那么他会给你 \(1\) 块钱的折扣。现在你有 \(a\) 块钱和 \(b\) 个冰淇淋,问能出现的朋友人气之和的最大值。
\(n,p_i,c_i,x_i,a,b \leq 2 \times 10^3\)。
假设已经确定了最后出现的朋友集合 \(S\) ,由于每个朋友都只会给 \(1\) 的折扣,那么我们分配冰淇淋的方案一定是:将 \(S\) 内的元素按照 \(x_i\) 从小到大排序,然后优先使用冰淇淋。换言之,一定存在一个分界点 \(i\),使得其之前的朋友都用冰淇淋,其之后的朋友都用钱。
于是可以设 \(f_{i,j}\) 表示前 \(i\) 个朋友,用 \(j\) 个冰淇淋得到的最大人气值和,\(g_{i,j}\) 表示 \(i\) 的后缀,用 \(j\) 块前得到的最大人气值和,转移就是一个背包。最后枚举分界点把两部分拼起来即可。时间复杂度 \(\mathcal{O}(n^2)\)。
code
#include <bits/stdc++.h>
using namespace std;
const int N = 2e3 + 5;
struct dat {
int p, c, x;
} d[N];
int n, A, B;
int f[N][N], g[N][N];
int main() {
ios :: sync_with_stdio(false);
cin.tie(nullptr);
cin >> n >> A >> B;
for (int i = 1; i <= n; i++) {
cin >> d[i].p;
cin >> d[i].c;
cin >> d[i].x;
}
sort(d + 1, d + n + 1, [&](dat a, dat b){
return a.x < b.x;
});
for (int i = 1; i <= n; i++) {
for (int j = 0; j <= B; j++) {
f[i][j] = f[i - 1][j];
}
for (int j = d[i].x * d[i].c; j <= B; j++) {
f[i][j] = max(f[i][j], f[i - 1][j - d[i].x * d[i].c] + d[i].p);
}
}
for (int i = n; i >= 1; i--) {
for (int j = 0; j <= A; j++) {
g[i][j] = g[i + 1][j];
}
for (int j = d[i].c; j <= A; j++) {
g[i][j] = max(g[i][j], g[i + 1][j - d[i].c] + d[i].p);
}
}
int ans = 0;
for (int i = 0; i <= n; i++) {
ans = max(ans, f[i][B] + g[i + 1][A]);
if (i == 0)
continue;
for (int j = 0; j <= min(A, d[i].c); j++) {
if (d[i].x * (d[i].c - j) > B)
continue;
ans = max(ans, f[i - 1][B - d[i].x * (d[i].c - j)] + g[i + 1][A - j] + d[i].p);
}
}
cout << ans << "\n";
return 0;
}
B. [USACO22DEC] Mountains G
有 \(n\) 座山,第 \(i\) 座山高度为 \(h_i\)。对于两座山 \(i,j(i<j)\),我们称它们能相互看见,当且仅当不存在某座山 \(k(i<k<j)\),使得在平面直角坐标系中 \((i,h_k)\) 位于连接 \((i,h_i),(j,h_j)\) 的线段之上。
\(q\) 次操作,每次增加某座山的高度,每次操作后求能相互看见的山的无序对个数。
\(n,q \leq 2 \times 10^3\),\(h_i \leq 10^9\)。
先考虑如何求出 \(i\) 右边能和它互相看到的山。这比较简单,我们维护一个序列 \(s_i\),依次考虑 \(j\),如果 \(s_i\) 为空或 \(k(i,p) \leq k(i,j)\) 就将 \(j\) 加入 \(s_i\),其中 \(p\) 为 \(s_i\) 的末尾元素,\(k(i,j)\) 表示连接 \((i,h_i),(j,h_j)\) 的线段的斜率。
因为允许 \(\mathcal{O}(n^2)\) 的复杂度,所以我们考虑对于每座山 \(i\) 维护序列 \(s_i\)。对于修改 \((x,y)\),我们重构 \(s_x\),然后对于 \(j < x\),由于 \(h_x\) 增加,则 \(k(j,x)\) 一定会增加,因此我们检查能否将 \(x\) 插入 \(s_j\),并且插入 \(s_j\) 后可能需要删除 \(x\) 之后的一段元素。上述操作容易通过 set
维护。
虽然看起来很暴力,但复杂度其实是对的:重构部分复杂度 \(\mathcal{O}(nq)\),初始时最多有 \(\mathcal{O}(n^2)\) 个元素,每次操作最多加入 \(\mathcal{O}(n)\) 个元素,它们最多被插入一次,删除一次,因此这一部分的总复杂度为 \(\mathcal{O}(n(n+q) \log n)\)。
视 \(n,q\) 同阶,则总复杂度为 \(\mathcal{O}(n^2 \log n)\),可以通过本题。
code
#include <bits/stdc++.h>
#define fi first
#define se second
#define lob lower_bound
using namespace std;
const int N = 2e3 + 5, inf = 2e9;
typedef double db;
typedef pair <int, db> pi;
int n, q, a[N];
set <pi> v[N];
int main() {
ios :: sync_with_stdio(false);
cin.tie(nullptr);
cin >> n;
for (int i = 1; i <= n; i++)
cin >> a[i];
for (int i = 1; i <= n; i++) {
for (int j = i + 1; j <= n; j++) {
db k = (db)(a[j] - a[i]) / (j - i);
if (v[i].empty() || (*prev(v[i].end())).se <= k)
v[i].emplace(j, k);
}
}
cin >> q;
for (int i = 1, x, y; i <= q; i++) {
cin >> x >> y;
a[x] += y;
for (int j = 1; j < x; j++) {
db k = (db)(a[x] - a[j]) / (x - j);
bool ok = false;
auto it = v[j].lob(pi(x, -inf));
if (it != v[j].end() && it -> fi == x) {
v[j].erase(it);
}
it = v[j].lob(pi(x, k));
auto r = it;
if (it == v[j].begin()) {
r = v[j].emplace(pi(x, k)).fi;
ok = true;
} else if ((*(--it)).se <= k) {
r = v[j].emplace(pi(x, k)).fi;
ok = true;
}
if (ok == false)
continue;
auto le = next(r);
auto ri = next(r);
while (ri != v[j].end() && (*ri).se < k) {
ri++;
}
v[j].erase(le, ri);
}
v[x].clear();
for (int j = x + 1; j <= n; j++) {
db k = (db)(a[j] - a[x]) / (j - x);
if (v[x].empty() || (*prev(v[x].end())).se <= k)
v[x].emplace(j, k);
}
int ans = 0;
for (int j = 1; j <= n; j++) {
ans += v[j].size();
}
cout << ans << "\n";
}
return 0;
}
C. [USACO22DEC] Strongest Friendship Group G
给定一个 \(n\) 个点 \(m\) 条边的无向图 \(G\),定义 \(G\) 的连通导出子图 \(S\) 的权值 \(f(S) = |S| \times \min_{i \in S} d_i\),其中 \(d_i\) 为点 \(i\) 在 \(S\) 中的度数。求 \(f(S)\) 的最大值。
\(n \leq 10^5\),\(m \leq 2 \times 10^5\)。
有两种思考的角度:枚举 \(|S|\) 或者枚举 \(\min_{i \in S} d_i\)。前者对简化问题并没有帮助,我们考虑枚举 \(\min_{i \in S} d_i\)。
考虑 \(G\) 中度数最小的点 \(u\)(多个最小值则任取一个),显然包含 \(u\) 的极大联通子图 \(S\) 即为所有包含 \(u\) 的点集中 \(f(S)\) 最大的一个。由此不难得到以下做法:每次取出当前度数最小的点 \(u\),计算包含 \(u\) 的极大联通子图的 \(f\) 值,再将 \(u\) 及其相连的边删去,重复操作直到所有点被删完。
剩下的问题是如何维护这些操作。考虑到删边难以维护连通块大小,但我们可以先对每个点 \(u\) 求出枚举到 \(u\) 时剩余的度数 \(r_u\),然后倒过来加边,用并查集维护连通块大小即可。时间复杂度 \(O((n+m) \log n)\)。
code
#include <bits/stdc++.h>
using namespace std;
const int N = 1e5 + 5;
#define pb push_back
#define mk make_pair
#define fi first
#define se second
typedef long long LL;
typedef pair <int, int> pi;
int n, m;
int f[N], sz[N], d[N], rd[N];
bool era[N];
vector <int> v, e[N];
priority_queue <pi, vector <pi>, greater <pi>> q;
int gf(int x) {
return x == f[x] ? x : f[x] = gf(f[x]);
}
int main() {
ios :: sync_with_stdio(false);
cin.tie(nullptr);
cin >> n >> m;
for (int i = 1, x, y; i <= m; i++) {
cin >> x >> y;
e[x].pb(y), d[y]++;
e[y].pb(x), d[x]++;
}
for (int i = 1; i <= n; i++)
q.push(mk(d[i], i));
for (int i = 1; i <= n; i++) {
int u = q.top().se;
q.pop();
while (era[u]) {
u = q.top().se;
q.pop();
}
era[u] = true, rd[u] = d[u];
v.pb(u);
for (auto x : e[u]) {
d[x]--;
q.push(mk(d[x], x));
}
}
for (int i = 1; i <= n; i++)
f[i] = i, sz[i] = 1;
LL ans = 0;
reverse(v.begin(), v.end());
for (auto u : v) {
era[u] = false;
for (auto x : e[u]) {
if (era[x] == true)
continue;
int fu = gf(u), fv = gf(x);
if (fu != fv) {
f[fu] = fv, sz[fv] += sz[fu];
}
}
ans = max(ans, 1LL * rd[u] * sz[gf(u)]);
}
cout << ans << "\n";
return 0;
}