2023-10-09 模拟赛总结
模拟赛链接
排名:\(\text{rank 1}\)
分数:\(100+100+70+20=290\)
终于有一次模拟赛不掉分了。
T1:最后一课 / dist
题目描述:
在一个平面直角坐标系上,给定一条直线 \(y=k\) 和两个点 \(P(x_1,y_1),Q(x_2,y_2)\),求经过水平线的两点的最短距离。(\(k,x_1,y_1,x_2,y_2\le5\times10^8\))
思路:
先考虑最简单的情况,两个点分别在直线两侧,我们知道两点之间线段最短,那么就直接连一条线段即可。
另一种情况两个点在直线同侧,这样就变成了一个经典的题目:将军饮马。我们可以对其中一个点做的对称轴为该直线的对称,再将两点连线,就可以了。
代码:
#include <bits/stdc++.h>
using namespace std;
long long k, xp, yp, xq, yq;
int main() {
freopen("dist.in", "r", stdin);
freopen("dist.out", "w", stdout);
cin >> k >> xp >> yp >> xq >> yq;
(yp >= k) == (yq >= k) && (yq -= (yq - k) * 2);
cout << (xp - xq) * (xp - xq) + (yp - yq) * (yp - yq);
return 0;
}
时间复杂度:\(O(1)\),空间复杂度:\(O(1)\)
T2:日常 / shoot
题目描述
Kiana 正在基地里打靶。在一条长度为 \(m\) 的线段上,有 \(n\) 个靶子,第 \(i\) 个靶子的覆盖了 \([l_i,r_i]\) 这一段区间,且靶子之间不存在交(注意交为一个点也算有交)。对于第 \(i\) 个靶子,其中的 \([x_i, y_i]\) 这一段区间被标成了红色。
接下来,Kiana 进行了 \(k\) 次射击,会发射一发子弹打在线段的某个位置。对于每次射击,基地的人工智能--你需要输出对应的结果。具体的,假如射到了已经射过的靶子,输出 Again
,假如没有射在靶子上,输出 Failed
。假如不符合以上两条,且打到了红色区域,则输出 Perfect
,否则输出 Normal
。(\(1 \le n, k \le 10^5, 1 \le l_i \le x_i \le y_i \le r_i \le m \le 10^9\))
思路:
看到这么多区间,还有多次询问点的状态,很容易想到二分(也可以想到线段树),由于这题的区间没有交集,所以很好处理。先二分找到这个点所在的区间,在看它是否在这个区间里,然后模拟即可。
代码:
#include <bits/stdc++.h>
using namespace std;
const int kMaxN = 1e5 + 5;
int n, m, k;
bool vis[kMaxN];
struct Line {
int l, x, y, r;
bool operator<(const Line &y) const {
return l < y.l;
}
} a[kMaxN];
int main() {
freopen("shoot.in", "r", stdin);
freopen("shoot.out", "w", stdout);
ios::sync_with_stdio(0), cin.tie(0);
cin >> n >> m >> k;
for (int i = 1; i <= n; i++) {
cin >> a[i].l >> a[i].x >> a[i].y >> a[i].r;
}
sort(a + 1, a + 1 + n);
for (int p, p1; k; k--) {
cin >> p;
p1 = upper_bound(a + 1, a + 1 + n, (Line){p, -1, -1, -1}) - a - 1;
if (a[p1].r < p) {
cout << "Failed\n";
} else if (vis[p1]) {
cout << "Again\n";
} else if (a[p1].x <= p && p <= a[p1].y) {
cout << "Perfect\n", vis[p1] = 1;
} else {
cout << "Normal\n", vis[p1] = 1;
}
}
return 0;
}
时间复杂度:\(O(n+n\log n+k\log n)\),空间复杂度:\(O(n)\)
T3:渡尘 / max
题目描述:
给你一个长度为 \(n\) 的序列 \(a\),有 \(m\) 次询问,每次询问给出 \(l_i,r_i\),让你求出这一段区间的绝对值最大子段和,也就是取所有子段和的绝对值的最大值。(\(n\le2\times10^5,m\le3\times10^6,|a_i|\le10^9\))
思路:
- \(\text{40 pts}\) 的思路:
我们可以发现绝对值最大子段和就是最大子段和的绝对值和最小子段和的绝对值的最大值,我们对于每一次询问,可以 \(O(n)\) 动态规划求出最大子段和和最小子段和,然后没了。
该算法的时间复杂度:\(O(n\cdot m)\),空间复杂度:\(O(n)\)
- \(\text{70 pts}\) 的思路:
区间查询最大子段和和最小子段和,我们很容易想到用线段树维护区间最大子段和和最小子段和。
#include <bits/stdc++.h>
using namespace std;
using LL = long long;
const int kMaxN = 1e5 + 5;
LL n, m, a[kMaxN];
struct Seg {
LL maxn, minn, premax, sufmax, premin, sufmin, sum;
} t[kMaxN << 2], ans;
void PushUp(Seg& u, Seg& ls, Seg& rs) {
u.premax = max(ls.premax, ls.sum + rs.premax);
u.sufmax = max(rs.sufmax, rs.sum + ls.sufmax);
u.premin = min(ls.premin, ls.sum + rs.premin);
u.sufmin = min(rs.sufmin, rs.sum + ls.sufmin);
u.sum = ls.sum + rs.sum;
u.maxn = max(max(u.premax, u.sufmax), max(ls.sufmax + rs.premax, max(ls.maxn, rs.maxn)));
u.minn = min(min(u.premin, u.sufmin), min(ls.sufmin + rs.premin, min(ls.minn, rs.minn)));
}
void Build(int u, int l, int r) {
if (l == r) {
return (t[u].maxn = t[u].minn = t[u].premax = t[u].sufmax = t[u].premin = t[u].sufmin = t[u].sum = a[l], (void)0);
}
int mid = l + r >> 1;
Build(u << 1, l, mid), Build(u << 1 | 1, mid + 1, r);
PushUp(t[u], t[u << 1], t[u << 1 | 1]);
}
Seg Query(int u, int L, int R, int l, int r) {
if (L <= l && r <= R) {
return t[u];
}
int mid = l + r >> 1;
if (L <= mid && R > mid) {
Seg ls = Query(u << 1, L, R, l, mid), rs = Query(u << 1 | 1, L, R, mid + 1, r), ans;
PushUp(ans, ls, rs);
return ans;
} else if (L <= mid) {
return Query(u << 1, L, R, l, mid);
} else {
return Query(u << 1 | 1, L, R, mid + 1, r);
}
}
int main() {
freopen("max.in", "r", stdin);
freopen("max.out", "w", stdout);
cin >> n >> m;
for (int i = 1; i <= n; i++) {
cin >> a[i];
}
Build(1, 1, n);
for (int l, r; m; m--) {
cin >> l >> r, ans = Query(1, l, r, 1, n);
cout << max(llabs(ans.maxn), llabs(ans.minn)) << '\n';
}
return 0;
}
该算法的时间复杂度:\(O(m\log n)\),空间复杂度:\(O(n)\)。(只不过常数有点大而已)
- \(\text{100 pts}\) 的思路:
一、猫树:
我们发现这道题没有修改,用线段树可能有点浪费,于是我们可以用猫树来处理(本人不会猫树,这只是一种做法而已)
该算法的时间复杂度:\(O(n\log n+m)\),空间复杂度:\(O(n)\)。
二、前缀和 + st 表:
看到连续区间和,首先可以想到用前缀和处理,若查询区间为 \([l,r]\),那么其中的小区间 \((x,y]\)(\(l-1\le x\le y\le r\)) 的和为 \(s_y-s_x\)(\(s\) 为前缀和数组),由于这道题的子段和要加上绝对值,那么 \((x,y]\) 的绝对值最大子段和就是 \(\max\{s_y-s_x,s_x-s_y\}\),也就相当于 \(x,y\) 没有互相约制的条件,所以所求的值就变成了如下式子 :
再变一下型:
里面的区间最大最小值就可以用 st 表实现了。
代码:
#include <bits/stdc++.h>
using namespace std;
using LL = long long;
const int kMaxN = 2e5 + 5, kL = 20;
LL n, m, a[kMaxN], f[kMaxN][kL], g[kMaxN][kL], lg[kMaxN];
int main() {
freopen("max.in", "r", stdin);
freopen("max.out", "w", stdout);
ios::sync_with_stdio(0), cin.tie(0);
cin >> n >> m, lg[0] = -1;
for (int i = 1; i <= n; i++) {
cin >> a[i];
g[i][0] = f[i][0] = (a[i] += a[i - 1]);
lg[i] = lg[i / 2] + 1;
}
lg[n + 1] = lg[(n + 1) / 2] + 1;
for (int j = 1; j < kL; j++) {
for (int i = 0; i <= n - (1 << j) + 1; i++) {
f[i][j] = max(f[i][j - 1], f[i + (1 << (j - 1))][j - 1]);
g[i][j] = min(g[i][j - 1], g[i + (1 << (j - 1))][j - 1]);
}
}
for (int l, r, t; m; m--) {
cin >> l >> r, l--, t = lg[r - l + 1];
cout << max(f[l][t], f[r - (1 << t) + 1][t]) - min(g[l][t], g[r - (1 << t) + 1][t]) << '\n';
}
return 0;
}
时间复杂度:\(O(n\log n+m)\),空间复杂度:\(O(n\log n)\)