NAQ 2019
Contents
I. Slow Leak
题意
$n$ 个节点的带权无向图中有 $m$ 条边,有一个汽车从 $1$ 出发要到 $n$,汽车有油箱,油箱一开始是满的,最多可以装 $d$ 升油。
每走 $1$ 单位距离就要消耗 $1$ 升油,图中有 $t$ 个节点是加油站,分别为 $a_1,a_2,…,a_t$。
求 $1$ 到 $n$ 的最短路径?如果不可能,输出 “stuck”。
其中,$n \in [2, 500], m \in (0, \frac{n(n-1)}{2}], t \in (0, n), d \in (0, 2^{31})$
题解
经典套路题:两次最短路。
加油站是关键,如果把 $1,n$ 也视作加油站,那么最后求出的路径一定是以加油站作为关键节点,从 $1$ 到 $n$ 的。
不妨考虑建立一个只有加油站的图,怎么建立呢?
首先,跑一个 floyd 最短路求出每两点之间的最短距离。
然后,只保留加油站进行建图,如果两个加油站之间的距离 $\leq d$,就连一条边。
再跑一次最短路即可。
代码
#include <bits/stdc++.h>
using namespace std;
const int maxn = 505;
int n, m, t, d;
bool gas[maxn];
ll dp[maxn][maxn];
ll dp2[maxn][maxn];
int main() {
fastio;
cin >> n >> m >> t >> d;
for (int i = 1; i <= t; i++) {
int x; cin >> x;
gas[x] = 1;
}
gas[1] = 1;
gas[n] = 1;
memset(dp, -1, sizeof(dp));
for (int i = 1; i <= n; i++) dp[i][i] = 0;
for (int i = 1; i <= m; i++) {
int u,v,w; cin >> u >> v >> w;
if (w <= d) {
dp[u][v] = dp[v][u] = w;
}
}
for (int k = 1; k <= n; k++) {
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= n; j++) {
if ((dp[i][k] >= 0 && dp[k][j] >= 0)) {
if (dp[i][j] < 0) dp[i][j] = dp[i][k] + dp[k][j];
else if (dp[i][k] + dp[k][j] < dp[i][j]) dp[i][j] = dp[i][k] + dp[k][j];
}
}
}
}
memset(dp2, -1, sizeof(dp2));
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= n; j++) {
if (gas[i] && gas[j] && dp[i][j] <= d) {
dp2[i][j] = dp[i][j];
}
}
}
for (int k = 1; k <= n; k++) {
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= n; j++) {
if (gas[i] && gas[j] && gas[k]) {
if ((dp2[i][k] >= 0 && dp2[k][j] >= 0)) {
if (dp2[i][j] < 0) dp2[i][j] = dp2[i][k] + dp2[k][j];
else if (dp2[i][k] + dp2[k][j] < dp2[i][j]) dp2[i][j] = dp2[i][k] + dp2[k][j];
}
}
}
}
}
if (dp2[1][n] >= 0) cout << dp2[1][n] << endl;
else cout << "stuck\n";
}
J. Stop Counting!
题意
给定一个 $n$ 个元素的整数数组 $a_1,…,a_n$,求:
$$\max\limits_{L \in [0,n], R \in [L+1, n]} \frac{(a_1+a_2+…a_L) + (a_R + a_{R+1} + a_n)}{L + (n-R+1)}$$
即:求左边一段加右边一段,使得平均值最大。
其中,$n \in [1, 10^6], a_i \in [-10^9, 10^9]$,要求答案的绝对误差在 $10^{-6}$ 以内或者相对误差在 $10^{-9}$ 以内。
题解
经典套路题,看见 最大/最小平均值 就想到 二分答案。
即二分答案,然后给所有元素减去这个值,判断是否存在左边一段加上右边一段的和 $\geq 0$ 即可,也就转变成求最小和的 subarray。
这题主要的坑在于不能用绝对误差,得用相对误差!因为绝对误差在数字较大的时候精度不够!
long double low = 0.0, high = 1e9; // 应该用 long double!
while (high - low > 1e-7) { // 这样写是错误的!
double mid = (low + high) * 0.5;
if (check(mid)) {
low = mid;
ans = mid;
} else {
high = mid;
}
}
如上是不行的,注意到相对误差的定义是:如果答案为 $x$,相对误差为 $10^{-9}$,意味着答案在 $(x (1-10^{-9}), x (1+10^{-9}))$ 之间。
所以在答案可能较大时,应该限制二分次数 $T$,如下:
long double low = 0.0, high = 1e9; // long double 是正确的
int T = 100;
while (high - low > 1e-7 && T--) { // 当相对误差 <= 1e-7 或者 次数超过 100次就停下来
long double mid = (low + high) * 0.5;
if (check(mid)) {
low = mid;
ans = mid;
} else {
high = mid;
}
}
代码
#include <bits/stdc++.h>
using namespace std;
const int maxn = 1e6+5;
int n;
int a[maxn];
// 是否存在大于0的subarray (左右两边)
double sm[maxn];
bool check(double x) {
double mn = 0;
double res = 1e18;
for (int i = 1; i <= n-1; i++) {
sm[i] = sm[i-1] - x + (double)a[i];
mn = max(mn, sm[i]);
res = min(res, sm[i] - mn);
}
sm[n] = sm[n-1] - x + (double)a[n];
for (int j = 1; j <= n; j++) {
res = min(res, sm[n] - sm[j]);
}
return sm[n] - res >= 0;
}
int main() {
scanf("%d", &n);
for (int i = 1; i <= n; i++) scanf("%d", &a[i]);
long double ans = 0.0;
long double low = 0.0, high = 1e9;
int T = 100;
while (high - low > 1e-7 && T--) { // 当相对误差 <= 1e-7 或者 次数超过 100次就停下来
long double mid = (low + high) * 0.5;
if (check(mid)) {
low = mid;
ans = mid;
} else {
high = mid;
}
}
printf("%.15lf\n", ans);
}