T1
给定一张 n n n 个点 m m m 条边的无向图,边有边权。规定一个参数 x x x,在经过 ( u i , v i ) (u_i,v_i) (ui,vi) 这条边时,会使 x ← x − w i x\gets x-w_i x←x−wi;而任何时刻都需要保证 x ≥ 0 x\ge 0 x≥0。现在需要求出一个 x 0 x_0 x0,使得对于任意一对点 ( u , v ) (u,v) (u,v),都可以合法的从 u u u 走到 v v v,而每当到达一个点都可以操作一次使得 x ← x 0 x\gets x_0 x←x0,但是在一次行走中最多操作 k k k 次。求最小的 x 0 x_0 x0。
n , k ≤ 100 n,k\le 100 n,k≤100, 0 ≤ w i ≤ 1 0 9 0\le w_i\le 10^9 0≤wi≤109。保证图联通。
上来就写了一个假做法:求出最小生成树然后直接二分 + O ( n 2 ) O(n^2) O(n2) 判定,这显然是错的,反例就是 u , v u,v u,v 之间有一条三条边、边权为 6 6 6 的路径和一条两条边、边权为 7 7 7 的路径,最小生成树不会保留 7 7 7 边权的边,但是显然走后者比走前者优。致敬 XYD 传奇随机数据假做法拿了 90 p t s 90\mathrm{pts} 90pts。
首先 n n n 很小就可以考虑乱搞,先 O ( n 3 ) O(n^3) O(n3) 算一遍全源最短路,然后考虑二分这个 x 0 x_0 x0,此时有一个很精妙的转化:对于 u , v u,v u,v 之间的最短路,若长度 > x 0 >x_0 >x0 那么我们将边权更改为 ∞ \infty ∞,否则为 1 1 1,这样跑出来后 u , v u,v u,v 之间新最短路的长度就是令 x ← x 0 x\gets x_0 x←x0 的操作次数。为什么?因为我们不能在边上行走时执行操作,于是对于一段 u → v u\to v u→v 的路径,我们一定是将路径在点上划分为若干段,使得每段的边权和 < x <x <x 且尽可能长。若有一段的长度比 x 0 x_0 x0 要大,那么即使 x x x 是满的也走不过去,故设为 ∞ \infty ∞;否则我们就将这一段视为划分的一段,消耗一次操作,故边权为 1 1 1。再跑一次 Floyd 然后 O ( n 2 ) O(n^2) O(n2) check 即可。
namespace STD {
const ll inf = 1e18;
ll g[maxn][maxn], f[maxn][maxn];
int main() {
scanf("%d %d %d", &n, &m, &k);
for (int i = 1; i <= n; i ++)
for (int j = 1; j <= n; j ++)
g[i][j] = i == j ? 0 : inf;
for (int i = 1, u, v, w; i <= m; i ++)
scanf("%d %d %d", &u, &v, &w), g[u][v] = g[v][u] = min(g[u][v], 1ll * w);
for (int _ = 1; _ <= n; _ ++)
for (int i = 1; i <= n; i ++)
for (int j = 1; j <= n; j ++)
g[i][j] = min(g[i][j], g[i][_] + g[_][j]);
auto check = [&](ll x) { // 新语法
for (int i = 1; i <= n; i ++)
for (int j = 1; j <= n; j ++)
f[i][j] = i == j ? 0 : g[i][j] <= x ? 1 : inf;
for (int _ = 1; _ <= n; _ ++)
for (int i = 1; i <= n; i ++)
for (int j = 1; j <= n; j ++)
f[i][j] = min(f[i][j], f[i][_] + f[_][j]);
bool ok = 1;
for (int i = 1; i <= n; i ++)
for (int j = 1; j <= n; j ++)
ok &= (f[i][j] <= k);
return ok;
}; ll ans = 1e18;
for (ll L = 0, R = 1e18; L <= R; ) {
ll mid = (L + R) >> 1;
if (check(mid)) ans = mid, R = mid - 1;
else L = mid + 1;
} printf("%lld\n", ans);
return 0;
}
}
T2
有一个 n × m n\times m n×m 的棋盘和一个棋子,每个格子有非负权值,Alice 和 Bob 进行博弈:
- 首先,Alice 在第 1 1 1 行中选择一个格子放下棋子,然后获得那个格子的权值;
- 然后,Bob 选择一个不在最后一行的格子,设为 ( i , j ) (i,j) (i,j),在 ( i , j ) (i,j) (i,j) 与 ( i + 1 , j ) (i+1,j) (i+1,j) 之间放一堵墙,使得放下之后棋子无法从 ( i , j ) (i,j) (i,j) 直接走到 ( i + 1 , j ) (i+1,j) (i+1,j),也可以选择不放置墙,放完墙后需保证存在至少一条去最后一行的路径;
- 接着,Alice 选择 4 4 4 个可以走的存在的相邻格子中的一个,将棋子挪过去,获得那个格子的权值;
- 重复操作直到棋子到达最后一行,定义获得的分数为途径所有格子的权值之和,若多次途径同一格则多次计算权值。
Alice 的目标是最小化分数,Bob 的目标是最大化分数,求最优操作下最终获得的分数。
1 ≤ n , m ≤ 100 1\le n,m\le 100 1≤n,m≤100。
一开始读错题以为横竖都能放墙,硬控半小时,然后认为除了第一行和最后一行其余格子都会拿且仅拿一次,又硬控一小时。最后得到的策略是:Alice 若往左走,那么 Bob 就顺着她走直到碰到边缘,然后再让她飘回到另一端下去到下一行,当然还有直接放 Alice 下去的选择。做个 dp 即可,dp 过程中转移是取 max \max max 还是 min \min min 看是谁在操作。
#include <bits/stdc++.h>
using namespace std;
#define ll long long
#define open(x) freopen(#x".in", "r", stdin), freopen(#x".out", "w", stdout)
const int maxn = 105;
int a[maxn][maxn], g[maxn][maxn], f[maxn][maxn], n, m;
const int inf = 1e9;
int main() {
open(board);
scanf("%d %d", &n, &m);
for (int i = 1; i <= n; i ++)
for (int j = 1; j <= m; j ++)
scanf("%d", &a[i][j]);
for (int i = 1; i <= n; i ++)
for (int j = 1; j <= m; j ++)
g[i][j] = a[i][j] + g[i][j - 1];
auto getsum = [&](int id, int L, int R) { return g[id][R] - g[id][L - 1]; };
for (int i = 1; i <= m; i ++)
f[n][i] = a[n][i];
for (int i = n - 1; i > 0; i --)
for (int j = 1; j <= m; j ++) {
// (i, j) -> (i + 1, k)
int c1 = 0, c2 = 0, c3 = 0; // c1: -> c2: || c3: <-
// \/
for (int k = 1; k < j; k ++)
c1 = max(c1, f[i + 1][k] + getsum(i, j, m) + getsum(i, k, m - 1));
c2 = a[i][j] + f[i + 1][j];
for (int k = j + 1; k <= m; k ++)
c3 = max(c3, f[i + 1][k] + getsum(i, 1, j) + getsum(i, 2, k));
// cout << i << ' ' << j << ' ' << c1 << ' ' << c2 << ' ' << c3 << '\n';
f[i][j] = max(c2, min(j == 1 ? inf : c1, j == m ? inf : c3));
}
int ans = inf;
for (int i = 1; i <= m; i ++)
ans = min(ans, f[1][i]);
printf("%d\n", ans);
}
然而虽然 AC 但是策略是错的,每次不一定要折返到边缘。正解是考虑在每一行做区间 dp,令 f ( i , l , r , 0 / 1 ) f(i,l,r,0/1) f(i,l,r,0/1) 表示第 i i i 行已经在 [ l , r ] [l,r] [l,r] 上放了墙,此时棋子在 l − 1 / r + 1 l-1/r+1 l−1/r+1。转移很好做。致敬传奇 XYD 数据写假两题获 190 p t s 190\mathrm{pts} 190pts。
T3
有 n n n 位入侵者,当你遇到第 i i i 个入侵者时,如果你此前受到的总伤害不大于 H i H_i Hi,你将会战胜这个入侵者并受到 P i P_i Pi 点伤害,否则你会战败。战败后你会受到 P i P_i Pi 点伤害并仍可以和其他入侵者战斗。
你可以以任意顺序和入侵者战斗,并且一开始没有受到任何伤害。求最多能战胜多少个入侵者。
n ≤ 1 0 5 n\le 10^5 n≤105, 0 ≤ H i ≤ 1 0 15 0\le H_i\le 10^{15} 0≤Hi≤1015, 0 ≤ P i ≤ 1 0 9 0\le P_i\le 10^9 0≤Pi≤109。
赛时糊了一档 O ( n ! ) O(n!) O(n!) 暴力分和乱搞获 40 p t s 40\mathrm{pts} 40pts。正解是反悔贪心。容易证明,如果 H i + P i < H j + P j H_i+P_i<H_j+P_j Hi+Pi<Hj+Pj,那么如果 j j j 选之后 i i i 还能选,那么交换它们的选择顺序仍然合法。所以将所有入侵者按 H i + P i H_i+P_i Hi+Pi 排序,答案是其中一个子序列。
考虑反悔堆贪心。维护一个堆表示当前战胜的敌人,从前往后时刻保证当前堆是最大战胜数量且受到伤害最小的。如果当前的入侵者 i i i 无法战胜,那么退而求其次,我们尝试弹出一个元素使受到伤害减少。一个显然的想法是弹出 P j P_j Pj 最大的元素 j j j。不妨 i ≠ j i\ne j i=j(否则等价于舍弃当前元素),接下来证明这个策略仍然保证是最优解。首先,由于弹出的是最大值,所以减少的伤害必然不小于上一次受到的伤害,即当前总伤害 P ∗ ≤ H i P^∗≤H^i P∗≤Hi,故方案合法。其次,如果存在更优的方案修改了原方案,则考虑被修改元素中最靠前的序号 x x x 和被修改的元素中最靠前的序号 y y y,则 x x x 一定会先于 y y y 被弹出,故原方案不可能是经过上述策略的方案。
复杂度 O ( n log n ) O(n\log n) O(nlogn)。
#include <bits/stdc++.h>
using namespace std;
const int maxn = 1e5 + 5;
#define ll long long
#define open(x) freopen(#x".in", "r", stdin), freopen(#x".out", "w", stdout)
struct Invader {
ll h; int p;
bool operator<(const Invader &oth) const {
return h + p < oth.h + oth.p;
}
} a[maxn];
int n; priority_queue<int> q;
int main() {
open(invader);
scanf("%d", &n);
for (int i = 1; i <= n; i ++)
scanf("%lld %d", &a[i].h, &a[i].p);
sort(a + 1, a + n + 1); ll sum = 0;
for (int i = 1; i <= n; i ++) {
if (sum <= a[i].h)
sum += a[i].p, q.push(a[i].p);
else if (q.top() > a[i].p)
sum += a[i].p - q.top(), q.pop(), q.push(a[i].p);
} printf("%d\n", q.size());
return 0;
}