2019-2020 ICPC Northwestern European Regional Programming Contest (NWERC 2019) 部分题解
2019-2020 ICPC Northwestern European Regional Programming Contest (NWERC 2019)
A. Average Rank
题意:\(n\) 个参赛选手将开展 \(w\) 个星期的比赛,每个星期的比赛都会有一些选手加一分,选手按照分数从大到小排名(分数相等并列)。计算每个选手所有星期排名的平均值。
分析:考虑一下当某个人加了一分后,他的名次将发生什么变化,假设他当前分数为 \(x\) ,则 \(x\) 分数段的人数减一, \(x+1\) 分数段人数加一,因此他的名次将会提升至分数为 \(x+1\) 这档的排名,这一轮中分数仍为 \(x\) 的人的排名加一。于是就得出了 \(O(nw)\) 的模拟,但是这样的复杂度是不能接受的。对于每一轮的更新,我们实际上不需要对每一个人的排名进行更新,只需要对有分数变化的人进行操作,不变化的人只有到了他发生变化时再更新(类似于线段树 \(lazy\) 标记)。给每个人打一个标记,标记他上次更新前的分数;每个分数段也打一个标记,标记这个分数上次更新的时间;然后就能 \(O(n)\) 更新了。
#define _CRT_SECURE_NO_WARNINGS
#pragma GCC optimize(3)
#pragma GCC optimize("Ofast")
#pragma GCC target("sse,sse2,sse3,ssse3,sse4,popcnt,abm,mmx,avx,tune=native")
#pragma comment(linker, "/stack:200000000")
#include <bits/stdc++.h>
#define SIZE 300010
#define rep(i, a, b) for (int i = a; i <= b; ++i)
#define mp make_pair
#define ll long long
using namespace std;
void io() { ios::sync_with_stdio(false); cin.tie(nullptr); cout.tie(nullptr); }
ll n, w, k;
ll num[SIZE], p[SIZE], rk[SIZE], lazy[SIZE], pre[SIZE];
double sum[SIZE];
int main() {
io(); cin >> n >> w;
rep(i, 0, (w - 1)) {
cin >> k;
rep(j, 0, (k - 1)) {
int x; cin >> x;
num[p[x]] += rk[p[x]] * (i - lazy[p[x]]);
lazy[p[x]] = i;
++rk[p[x]];
sum[x] += num[p[x]] - pre[x];
++p[x];
num[p[x]] += rk[p[x]] * (i - lazy[p[x]]);
lazy[p[x]] = i;
pre[x] = num[p[x]];
}
}
rep(i, 1, n) {
num[p[i]] += rk[p[i]] * (w - lazy[p[i]]);
lazy[p[i]] = w;
sum[i] += num[p[i]] - pre[i];
}
rep(i, 1, n) cout << fixed << setprecision(9) << (1.0 + sum[i] / w) << '\n';
}
C. Canvas Line
题意:一条直线上挂着很多油画,每幅油画覆盖了一段区间 \([l_i,r_i]\) ,任意两幅油画没有重叠部分。为了挂起一幅油画,我们需要用两个架子夹住油画,但不能用多于两个夹子夹住同一幅油画。已知有 \(p\) 个位置 \(x_i\) 上已经存在夹子了,现在你需要给出一种使用夹子数量最少的构造来挂起所有油画,或者判断不存在这种构造。
分析:很明显的贪心,我们尽可能夹住两幅油画的相交处,因此我们先遍历一遍油画的相交点,注意判断合法性,最后再给每幅不满足要求的油画添上夹子。构造不存在当且仅当一开始就有某幅油画上存在多于两个夹子。
#define _CRT_SECURE_NO_WARNINGS
#pragma GCC optimize(3)
#pragma GCC optimize("Ofast")
#pragma GCC target("sse,sse2,sse3,ssse3,sse4,popcnt,abm,mmx,avx,tune=native")
#pragma comment(linker, "/stack:200000000")
#include <bits/stdc++.h>
#define SIZE 200005
#define rep(i, a, b) for (int i = a; i <= b; ++i)
#define mp make_pair
#define ll long long
using namespace std;
void io() { ios::sync_with_stdio(false); cin.tie(nullptr); cout.tie(nullptr); }
int n, m, ans;
struct Seg {
int x, y;
Seg() {}
Seg(int x_, int y_) : x(x_), y(y_) {}
bool operator < (const Seg& b) const {
return x < b.x;
}
}p[SIZE];
int MP[SIZE];
map<int, int> vis;
int main() {
io(); cin >> n;
rep(i, 1, n) cin >> p[i].x >> p[i].y;// , vis[p[i].x] = vis[p[i].y] = 0;
sort(p + 1, p + 1 + n);
cin >> m;
rep(i, 1, m) {
int x; cin >> x;
int pos = upper_bound(p + 1, p + 1 + n, Seg(x, 0)) - p - 1;
if (p[pos].y < x) continue;
vis[x]++, MP[pos]++;
if (p[pos - 1].y == x) MP[pos - 1]++;
}
rep(i, 1, n) {
if (MP[i] > 2) {
cout << "impossible";
return 0;
}
}
vector<int> res;
rep(i, 2, n) {
if (p[i - 1].y == p[i].x) {
if (MP[i - 1] >= 2 || MP[i] >= 2 || vis[p[i].x]) continue;
++ans;
res.push_back(p[i].x);
vis[p[i].x] = 1;
MP[i - 1]++, MP[i]++;
}
}
rep(i, 1, n) {
if (MP[i] >= 2) continue;
else {
int tot = 2 - MP[i], now = p[i].x + 1;
while (tot) {
if (vis[now]) { ++now; continue; }
vis[now] = 1;
res.push_back(now++);
--tot;
}
}
}
cout << res.size() << '\n';
for (auto i : res) cout << i << ' ';
}
D. Disposable Switches
题意:给定一个 \(n\) 个点,\(m\) 条边的无向图,每条边的边权是 \(\frac{l_i}{u}+c\)(其中 \(u,c\) 是两个任意正数,\(l_i\) 给定)。询问当 \(u,c\) 任意取值时,哪些边不可能在最短路上。
分析:由于 \(u,c>0\) 我们将所有边边权 \(\times u\) 。然后考虑:如果一条最短路经过了恰好 \(k\) 条边,那么其权值可以表示为 \(\sum^k_{i=1}l'_i +kuc\Leftrightarrow minl_k + kx\) ,其中 \(minl_k\) 代表用 \(l_i\) 作为边权建图时,恰好经过 \(k\) 条边的最短路权值,\(x=uc\) 。这意味着,如果我们处理出所有的 \(minl_k\) ,那么我们就能将只取 \(k\) 条边时的最短路权值用一个一次函数 \(y=kx+minl_k\) 表示。如果我们将所有这样的一次函数画出来,做一个半平面交,如下图:
所有能暴露在下方的边(图中虚线部分)都是可以通过合适的取值,成为最短路的。因此,剩下的问题就是如何求出所有的 \(minl_k\) 。
这一过程可以通过一个类似于 \(Floyd\) 的 \(dp\) 推出,可参考代码。
#define _CRT_SECURE_NO_WARNINGS
#pragma GCC optimize(3)
#pragma GCC optimize("Ofast")
#pragma GCC target("sse,sse2,sse3,ssse3,sse4,popcnt,abm,mmx,avx,tune=native")
#pragma comment(linker, "/stack:200000000")
#include <bits/stdc++.h>
#define rep(i, a, b) for (long long i = a; i <= b; ++i)
#define mp make_pair
#define SIZE 2010
#define ll long long
using namespace std;
void io() { ios::sync_with_stdio(false); cin.tie(nullptr); cout.tie(nullptr); }
typedef pair<ll, ll> pll;
const ll INF = 1e18;
int n, m;
vector<pll> vec[SIZE];
ll dis[SIZE][SIZE];
bool res[SIZE], vis[SIZE][SIZE];
bool cross(pll a, pll b, pll c) {
long double xa = c.second - a.second, ya = a.first - c.first,
xb = b.second - a.second, yb = a.first - b.first;
return xa * yb < xb * ya;
}
void Floyd() {
for (int i = 0; i <= n; ++i) {
for (int j = 0; j <= n; ++j) {
dis[i][j] = INF;
}
}
dis[0][1] = 0;
for (int i = 0; i <= n - 2; ++i) {
for (int j = 1; j <= n; ++j) {
for (auto k : vec[j]) {
dis[i + 1][k.first] = min(dis[i + 1][k.first], dis[i][j] + k.second);
}
}
}
/*rep(i, 1, n) {
rep(j, 1, n) {
cout << dis[i][j] << ' ';
}
cout << '\n';
}*/
}
void dfs(int now, int to) {
res[to] = vis[now][to] = true;
if (!now) return;
for (auto i : vec[to]) {
if (dis[now - 1][i.first] + i.second == dis[now][to]) {
if (vis[now - 1][i.first]) continue;
dfs(now - 1, i.first);
}
}
}
int main() {
io(); cin >> n >> m;
rep(i, 1, m) {
int u, v, c;
cin >> u >> v >> c;
vec[u].emplace_back(mp(v, c));
vec[v].emplace_back(mp(u, c));
}
Floyd();
vector<pll> stk;
for (int i = n - 1; i >= 1; --i) {
pll p = mp(i, dis[i][n]);
//cout << p.first << ' ' << p.second << '\n';
while (stk.size() >= 2) {
if (cross(stk[stk.size() - 2], stk[stk.size() - 1], p)) stk.pop_back();
else break;
}
if (stk.size() == 1 && stk.back().second > p.second) stk.pop_back();
stk.emplace_back(p);
}
//cout << '\n';
//for (auto i : stk) cout << i.first << ' ' << i.second << '\n';
for (auto i : stk) dfs(i.first, n);
vector<int> ans;
rep(i, 1, n) {
if (res[i]) continue;
ans.emplace_back(i);
}
cout << ans.size() << '\n';
for (auto i : ans) cout << i << ' ';
}
E. Expeditious Cubing
题意:略。
分析:简单分类讨论,不过需要注意精度,转化成整形再进行运算。
#define _CRT_SECURE_NO_WARNINGS
#pragma GCC optimize(3)
#pragma GCC optimize("Ofast")
#pragma GCC target("sse,sse2,sse3,ssse3,sse4,popcnt,abm,mmx,avx,tune=native")
#pragma comment(linker, "/stack:200000000")
#include <bits/stdc++.h>
#define SIZE 200010
#define rep(i, a, b) for (int i = a; i <= b; ++i)
#define mp make_pair
#define ll long long
using namespace std;
void io() { ios::sync_with_stdio(false); cin.tie(nullptr); cout.tie(nullptr); }
int n, t, cnt;
double a[10], T, inf = 2100;
int b[10];
int main() {
io(); rep(i, 1, 4) cin >> a[i], b[i] = round(a[i] * 100.0);
cin >> T; t = round(T * 300.0);
sort(b + 1, b + 1 + 5);
int tmp = 0;
rep(i, 2, 4) tmp += b[i];
if (tmp > t) { cout << "impossible"; return 0; }
b[1] = inf;
sort(b + 1, b + 1 + 5);
tmp = 0;
rep(i, 2, 4) tmp += b[i];
if (tmp <= t) { cout << "infinite"; return 0; }
tmp = 0;
rep(i, 2, 3) tmp += b[i];
cout << fixed << setprecision(2) << (t - tmp) / 100.0;
}
F. Firetrucks Are Red
题意:给定 \(n\) 个人,每个人有一个数字集合,我们称两个人是有关系的当这两个人拥有至少一个相同的数字,现在请你证明这 \(n\) 个人之间是否全部直接或者间接地存在关系,如果存在给出一种构造。
分析:其实就是按照数字关系建图找最小生成树,直接跑并查集就可以解决了。
#define _CRT_SECURE_NO_WARNINGS
#pragma GCC optimize(3)
#pragma GCC optimize("Ofast")
#pragma GCC target("sse,sse2,sse3,ssse3,sse4,popcnt,abm,mmx,avx,tune=native")
#pragma comment(linker, "/stack:200000000")
#include <bits/stdc++.h>
#define SIZE 200010
#define rep(i, a, b) for (int i = a; i <= b; ++i)
#define mp make_pair
#define ll long long
using namespace std;
void io() { ios::sync_with_stdio(false); cin.tie(nullptr); cout.tie(nullptr); }
ll n, t, cnt, a[SIZE];
struct UnionSet { //并查集
int pa[SIZE];
void init(int n) {
for (int i = 1; i <= n; i++) pa[i] = i;
}
int find(int x) {
return pa[x] == x ? x : pa[x] = find(pa[x]);
}
bool IsSame(int x, int y) { return find(x) == find(y); }
int union_vertices(int x, int y) {
int xr = find(x), yr = find(y);
if (xr != yr) {
pa[yr] = xr;
return 1;
}
return 0;
}
} U;
map<int, int> MP;
vector<tuple<int, int, int> > ans;
int main() {
io(); cin >> t;
U.init(200000);
rep(ii, 1, t) {
cin >> n;
rep(i, 1, n) {
int x; cin >> x;
if (MP.count(x)) {
if (U.IsSame(MP[x], ii)) continue;
else U.union_vertices(MP[x], ii), ans.emplace_back(tuple<int, int, int>(MP[x], ii, x));
}
else MP[x] = ii;
}
}
if (ans.size() < t - 1) { cout << "impossible"; return 0; }
for (auto i : ans) cout << get<0>(i) << ' ' << get<1>(i) << ' ' << get<2>(i) << '\n';
}
G. Gnoll Hypothesis
题意:怪物池中有 \(n\) 只怪物,每只怪物的生成概率为 \(p_i\) 。现在怪物池中的怪物只有 \(k\) 只会生成(任意 \(k\) 只),不生成的怪物原本的生成概率将会向后转移给下一只会可能生成的怪物(如果最后的怪物不会生成,那么概率会转移给第一只可能生成的)。求所有怪物现在的生成概率。
分析:这题的思路有点像 Atcoder Dwango Programming Contest 6th 里的 B 题,核心思想都是如何处理将前面的怪物合并到后面。如果 \(p_i\) 需要合并到 \(p_j\) \((i<j)\) ,那么 \(p_k(i\leq k<j)\) 必定是不能生成的怪物,根据这一性质我们就能对每一位 \(p_i'\) 进行计算了:\(p_i'=\sum^{n-k}_{j=0}p_{i-j}\cdot\frac{C^{k-1}_{n-j-1}}{C^{k}_{n}}\) 。
#define _CRT_SECURE_NO_WARNINGS
#pragma GCC optimize(3)
#pragma GCC optimize("Ofast")
#pragma GCC target("sse,sse2,sse3,ssse3,sse4,popcnt,abm,mmx,avx,tune=native")
#pragma comment(linker, "/stack:200000000")
#include <bits/stdc++.h>
#define SIZE 505
#define rep(i, a, b) for (int i = a; i <= b; ++i)
#define mp make_pair
#define ll long long
using namespace std;
void io() { ios::sync_with_stdio(false); cin.tie(nullptr); cout.tie(nullptr); }
int n, k, cnt;
long double dp[SIZE][SIZE];
void init(int n) { //dp[n][m] 即为 nCm
rep(i, 0, n) dp[i][0] = dp[i][i] = 1;
rep(i, 1, n) {
rep(j, 1, i) {
dp[i][j] = dp[i - 1][j] + dp[i - 1][j - 1];
}
}
}
int main() {
cin >> n >> k;
init(n);
vector<long double> a(n + n), b(n + n), ans(n);
rep(i, 0, (n - 1)) cin >> a[i], a[i + n] = a[i];
rep(i, (n - k), (n + n - k - 1)) {
rep(j, 0, (n - k)) b[i] += a[i - j] * dp[n - j - 1][k - 1] / dp[n][k];
ans[i % n] = b[i];
}
rep(i, 0, (n - 1)) printf("%.12Lf ", ans[i]);
}
H. Height Profile
题意:给出一个数列 \(h_n\) ,\(h_i\) 表示第 \(i\) \(km\) 处的海拔高度,相邻两个位置之间的道路用线段表示。给出 \(k\) 个询问,每个询问给出一个坡度 \(g\) ,要求你找出坡度大于等于 \(g\) 的道路最大长度,或者判断不存在这样的道路。
分析:注意到 \(k\leq 50\) ,因此可以是 \(O(knlogn)\) 的一个算法。我们可以将这个高度序列看作是一个一次的分段函数,直接对于整个分段函数全部减去函数 \(y=gx\) ,那么我们只需要找到满足 \(y_r-y_l\geq 0\) 的 \(|r-l|_{max}\) ,这个过程只需要对序列排序后就能快速查找了。但是这样还会漏掉点落在线段上的一些特判,我们用差值比例计算一下多余部分就行了。
#define _CRT_SECURE_NO_WARNINGS
#pragma GCC optimize(3)
#pragma GCC optimize("Ofast")
#pragma GCC target("sse,sse2,sse3,ssse3,sse4,popcnt,abm,mmx,avx,tune=native")
#pragma comment(linker, "/stack:200000000")
#include <bits/stdc++.h>
#define SIZE 200010
#define rep(i, a, b) for (int i = a; i <= b; ++i)
#define mp make_pair
#define ll long long
using namespace std;
void io() { ios::sync_with_stdio(false); cin.tie(nullptr); cout.tie(nullptr); }
int n, k, maxs = -2e9;
int h[SIZE], p[SIZE];
double sol(int dy) {
double ans = -1.0;
vector<pair<int, int> > vec(n + 1), tmp(n + 1);
rep(i, 0, n) vec[i] = mp(h[i] - i * dy, i), tmp[i] = vec[i];
sort(vec.begin(), vec.end());
int L = n, R;
for (auto& it : vec) {
R = it.second;
if (L < R) {
double res = 0;
if (L > 0) res = max(res, (double)(tmp[R].first - tmp[L].first) / fabs(tmp[L].first - tmp[L - 1].first));
if (R < n) res = max(res, (double)(tmp[R].first - tmp[L].first) / fabs(tmp[R + 1].first - tmp[R].first));
if (res > 1.0) res = 1.0;
ans = max(ans, R - L + res);
}
L = min(L, R);
}
return ans;
}
int main() {
io(); cin >> n >> k;
rep(i, 0, n) cin >> h[i];
rep(i, 1, n) maxs = max(maxs, h[i] - h[i - 1]);
rep(i, 1, k) {
double g; cin >> g;
ll dy = round(10.0 * g);
if (maxs < dy) { cout << "-1\n"; continue; }
cout << fixed << setprecision(12) << sol(dy) << '\n';
}
}
I. Inverted Deck
题意:给定一个序列 \(a_n\) ,判断能否翻转其中一个区间使得整个序列变成单调不减的。
分析:我们直接 \(sort\) 一遍,然后遍历判断是否存在这一个区间就好了。
#define _CRT_SECURE_NO_WARNINGS
#pragma GCC optimize(3)
#pragma GCC optimize("Ofast")
#pragma GCC target("sse,sse2,sse3,ssse3,sse4,popcnt,abm,mmx,avx,tune=native")
#pragma comment(linker, "/stack:200000000")
#include <bits/stdc++.h>
#define SIZE 1000005
#define rep(i, a, b) for (int i = a; i <= b; ++i)
#define mp make_pair
#define ll long long
#define mod 1000000007
using namespace std;
void io() { ios::sync_with_stdio(false); cin.tie(nullptr); cout.tie(nullptr); }
ll n, t, m, a[SIZE];
int main() {
io(); cin >> n;
vector<int> b(n + 1);
rep(i, 1, n) cin >> a[i], b[i] = a[i];
sort(a + 1, a + 1 + n);
vector<int> dif;
rep(i, 1, n) {
if (a[i] == b[i]) continue;
else dif.emplace_back(i);
}
if (dif.size() == 0) { cout << "1 1\n"; return 0; }
reverse(b.begin() + *dif.begin(), b.begin() + *dif.rbegin() + 1);
bool f = true;
rep(i, 1, n) {
if (a[i] != b[i]) {
f = false;
break;
}
}
if (f) cout << *dif.begin() << ' ' << *dif.rbegin() << '\n';
else cout << "impossible\n";
}
J. Jackdaws And Crows
题意:给定一个长度为 \(n\) 的数组 \(a\) 和两个参数 \(r,c\) ,你可以使用 \(r\) 秒时间删除数组中的一个数字,或者使用 \(c\) 秒时间将数组中的任意个数字加一或减一。询问将数组变成相邻异号的数组所需的最少时间。
分析:待补。