T1
给定一张 nnn 个点 mmm 条边的无向图,边有边权。规定一个参数 xxx,在经过 (ui,vi)(u_i,v_i)(ui,vi) 这条边时,会使 x←x−wix\gets x-w_ix←x−wi;而任何时刻都需要保证 x≥0x\ge 0x≥0。现在需要求出一个 x0x_0x0,使得对于任意一对点 (u,v)(u,v)(u,v),都可以合法的从 uuu 走到 vvv,而每当到达一个点都可以操作一次使得 x←x0x\gets x_0x←x0,但是在一次行走中最多操作 kkk 次。求最小的 x0x_0x0。
n,k≤100n,k\le 100n,k≤100,0≤wi≤1090\le w_i\le 10^90≤wi≤109。保证图联通。
上来就写了一个假做法:求出最小生成树然后直接二分 + O(n2)O(n^2)O(n2) 判定,这显然是错的,反例就是 u,vu,vu,v 之间有一条三条边、边权为 666 的路径和一条两条边、边权为 777 的路径,最小生成树不会保留 777 边权的边,但是显然走后者比走前者优。致敬 XYD 传奇随机数据假做法拿了 90pts90\mathrm{pts}90pts。
首先 nnn 很小就可以考虑乱搞,先 O(n3)O(n^3)O(n3) 算一遍全源最短路,然后考虑二分这个 x0x_0x0,此时有一个很精妙的转化:对于 u,vu,vu,v 之间的最短路,若长度 >x0>x_0>x0 那么我们将边权更改为 ∞\infty∞,否则为 111,这样跑出来后 u,vu,vu,v 之间新最短路的长度就是令 x←x0x\gets x_0x←x0 的操作次数。为什么?因为我们不能在边上行走时执行操作,于是对于一段 u→vu\to vu→v 的路径,我们一定是将路径在点上划分为若干段,使得每段的边权和 <x<x<x 且尽可能长。若有一段的长度比 x0x_0x0 要大,那么即使 xxx 是满的也走不过去,故设为 ∞\infty∞;否则我们就将这一段视为划分的一段,消耗一次操作,故边权为 111。再跑一次 Floyd 然后 O(n2)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×mn\times mn×m 的棋盘和一个棋子,每个格子有非负权值,Alice 和 Bob 进行博弈:
- 首先,Alice 在第 111 行中选择一个格子放下棋子,然后获得那个格子的权值;
- 然后,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 选择 444 个可以走的存在的相邻格子中的一个,将棋子挪过去,获得那个格子的权值;
- 重复操作直到棋子到达最后一行,定义获得的分数为途径所有格子的权值之和,若多次途径同一格则多次计算权值。
Alice 的目标是最小化分数,Bob 的目标是最大化分数,求最优操作下最终获得的分数。
1≤n,m≤1001\le n,m\le 1001≤n,m≤100。
一开始读错题以为横竖都能放墙,硬控半小时,然后认为除了第一行和最后一行其余格子都会拿且仅拿一次,又硬控一小时。最后得到的策略是:Alice 若往左走,那么 Bob 就顺着她走直到碰到边缘,然后再让她飘回到另一端下去到下一行,当然还有直接放 Alice 下去的选择。做个 dp 即可,dp 过程中转移是取 max\maxmax 还是 min\minmin 看是谁在操作。
#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) 表示第 iii 行已经在 [l,r][l,r][l,r] 上放了墙,此时棋子在 l−1/r+1l-1/r+1l−1/r+1。转移很好做。致敬传奇 XYD 数据写假两题获 190pts190\mathrm{pts}190pts。
T3
有 nnn 位入侵者,当你遇到第 iii 个入侵者时,如果你此前受到的总伤害不大于 HiH_iHi,你将会战胜这个入侵者并受到 PiP_iPi 点伤害,否则你会战败。战败后你会受到 PiP_iPi 点伤害并仍可以和其他入侵者战斗。
你可以以任意顺序和入侵者战斗,并且一开始没有受到任何伤害。求最多能战胜多少个入侵者。
n≤105n\le 10^5n≤105,0≤Hi≤10150\le H_i\le 10^{15}0≤Hi≤1015,0≤Pi≤1090\le P_i\le 10^90≤Pi≤109。
赛时糊了一档 O(n!)O(n!)O(n!) 暴力分和乱搞获 40pts40\mathrm{pts}40pts。正解是反悔贪心。容易证明,如果 Hi+Pi<Hj+PjH_i+P_i<H_j+P_jHi+Pi<Hj+Pj,那么如果 jjj 选之后 iii 还能选,那么交换它们的选择顺序仍然合法。所以将所有入侵者按 Hi+PiH_i+P_iHi+Pi 排序,答案是其中一个子序列。
考虑反悔堆贪心。维护一个堆表示当前战胜的敌人,从前往后时刻保证当前堆是最大战胜数量且受到伤害最小的。如果当前的入侵者 iii 无法战胜,那么退而求其次,我们尝试弹出一个元素使受到伤害减少。一个显然的想法是弹出 PjP_jPj 最大的元素 jjj。不妨 i≠ji\ne ji=j(否则等价于舍弃当前元素),接下来证明这个策略仍然保证是最优解。首先,由于弹出的是最大值,所以减少的伤害必然不小于上一次受到的伤害,即当前总伤害 P∗≤HiP^∗≤H^iP∗≤Hi,故方案合法。其次,如果存在更优的方案修改了原方案,则考虑被修改元素中最靠前的序号 xxx 和被修改的元素中最靠前的序号 yyy,则 xxx 一定会先于 yyy 被弹出,故原方案不可能是经过上述策略的方案。
复杂度 O(nlogn)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;
}
2517

被折叠的 条评论
为什么被折叠?



