「赛后总结」20211019(重拾20200906)
写在前面
我竟然做过这套题还写过题解
这次主要是补充一下 T2
和我 T3
的乱搞做法。
T1
不再赘述。
得分情况
预计得分:\(100 + 0 \sim 100 + 0 \sim 100 = 100 \sim 300\)。
实际得分:\(100 + 70 + 100 = 270\)。
考场上
开 T1,签到题。
开 T2,草,好 TM 眼熟啊,但是我不会,光知道要二分 L,不会 check。
开 T3,严格次短路,不会,写玄学算法希望多骗点分。
T2 神光
设 \(f_{i,j}\) 表示用了 \(i\) 次红光和 \(j\) 次绿光后第一个没被破坏的法坛最远在哪。
状态转移:
f[0][0] = 第一个法坛的位置
在已知左端点和 L
的时候能用 upper_bound()
推出下次的左端点。
f[i][j] = max(a[upper_bound(排好序的法坛,f[i - 1][j] + x - 1) - 数组名],a[upper_bound(排好序的法坛,f[i][j - 1] + 2 * x - 1) - 数组名])
转移的时候注意一下边界。
时间复杂度:\(O(30 n^2logn)\)。
Code:
#include <cstdio>
#include <cstring>
#include <string>
#include <iostream>
#include <algorithm>
#define M 2001
#define inf 1061109567
typedef long long ll;
int min(int a, int b) { return a < b ? a : b; }
int max(int a, int b) { return a > b ? a : b; }
inline void read(int &T) {
int x = 0; bool f = 0; char c = getchar();
while (c < '0' || c > '9') { if (c == '-') f = !f; c = getchar(); }
while (c >= '0' && c <= '9') { x = x * 10 + c - '0'; c = getchar(); }
T = f ? -x : x;
}
int n, r, g, a[M], f[M][M];
bool check(int x) {
f[0][0] = a[1];
int s1 = min(r, n), s2 = min(g, n);
for (int i = 0; i <= s1; ++i) {
for (int j = 0; j <= s2; ++j) {
if (i == 0 && j > 0) {
int pos = std::upper_bound(a + 1, a + n + 1, f[i][j - 1] + 2 * x - 1) - a;
if (pos > n) return true;
else f[i][j] = a[pos];
}
if (j == 0 && i > 0) {
int pos = std::upper_bound(a + 1, a + n + 1, f[i - 1][j] + x - 1) - a;
if (pos > n) return true;
else f[i][j] = a[pos];
}
if (i > 0 && j > 0) {
int pos = max(std::upper_bound(a + 1, a + n + 1, f[i - 1][j] + x - 1) - a, std::upper_bound(a + 1, a + n + 1, f[i][j - 1] + 2 * x - 1) - a);
if (pos > n) return true;
else f[i][j] = a[pos];
}
}
}
return false;
}
int main() {
read(n), read(r), read(g);
for (int i = 1; i <= n; ++i) read(a[i]);
std::sort(a + 1, a + n + 1);
int L = 1, R = a[n] + 1;
while (L <= R) {
int mid = (L + R) >> 1;
if (check(mid)) R = mid - 1;
else L = mid + 1;
}
printf("%d\n", L);
return 0;
}
上面复杂度估计要跑十几秒,想办法看能不能搞掉一个 \(log\)。
可以发现上面使用 upper_bound()
来转移的时候可能做了重复的工作,考虑能否预处理然后转移的时候直接调用。
发现位置的数字太大了上限是 \(10^9\),不好搞,于是我们令 \(f_{i,j}\) 表示用了 \(i\) 次红光和 \(j\) 次绿光第一个没被摧毁的法坛的编号,知道了编号自然知道了位置。
于是我们可以 \(n^2\) 来枚举左端点法坛的编号和右端点法坛的编号来判断能否全部摧毁,即预处理。
时间复杂度:\(O(30 n^2)\)
Code:
#include <cstdio>
#include <cstring>
#include <string>
#include <iostream>
#include <algorithm>
#define M 2001
typedef long long ll;
int min(int a, int b) { return a < b ? a : b; }
int max(int a, int b) { return a > b ? a : b; }
inline void read(int &T) {
int x = 0; bool f = 0; char c = getchar();
while (c < '0' || c > '9') { if (c == '-') f = !f; c = getchar(); }
while (c >= '0' && c <= '9') { x = x * 10 + c - '0'; c = getchar(); }
T = f ? -x : x;
}
int l1[M], l2[M];
int n, r, g, a[M], f[M][M];
bool check(int x) {
l1[0] = 1, l2[0] = 1;
for (int i = 1; i <= n; ++i) {
for (int j = 1; j <= n; ++j) {
if (a[j] <= a[i] + x - 1) l1[i] = j + 1;
if (a[j] <= a[i] + 2 * x - 1) l2[i] = j + 1;
}
}
int s1 = min(n, r), s2 = min(n, g);
f[0][0] = 1;
for (int i = 0; i <= s1; ++i) {
for (int j = 0; j <= s2; ++j) {
if (i == 0 && j > 0) f[i][j] = l2[f[i][j - 1]];
if (j == 0 && i > 0) f[i][j] = l1[f[i - 1][j]];
if (i > 0 && j > 0) f[i][j] = max(l2[f[i][j - 1]], l1[f[i - 1][j]]);
if (f[i][j] > n) return true;
}
}
return false;
}
int main() {
read(n), read(r), read(g);
for (int i = 1; i <= n; ++i) read(a[i]);
std::sort(a + 1, a + n + 1);
int L = 1, R = a[n] + 1;
while (L <= R) {
int mid = (L + R) >> 1;
if (check(mid)) R = mid - 1;
else L = mid + 1;
}
printf("%d\n", L);
return 0;
}
T3 迷宫
啊,次短路。
这里只说一下做法,如果有大佬能 hack 或者证明正确性欢迎联系。
首先以 \(1\) 为源点跑一遍最短路记为 dis1[i]
,以 \(n\) 为源点跑一遍最短路记为 dis2[i]
。
然后讨论三种情况:
1.次短路上的每条边都只走了一遍,这种情况枚举点 \(u\),再枚举连接 \(u\) 的一条边,假设连接了 \((u,v)\),这条路径长为 dis1[u] + w + dis2[v]
2.次短路上有一条边走了两遍,这种情况枚举点 \(u\),再枚举连接 \(u\) 的一条边走两遍,这条路径长为 dis1[u] + w * 2 + dis2[u]
3.次短路上有一条边走了三遍,这种情况枚举边走三遍,假设连接了 \((u,v)\),要考虑是 \(1\rightarrow u\) 还是 \(1\rightarrow v\)两种情况,路径长为 dis1[u] + w * 3 + dis2[v]
以及 dis1[v] + w * 3 + dis2[u]
。
在上述的所有情况中取大于最短路的最小值。
Code:
#include <cstdio>
#include <cstring>
#include <string>
#include <iostream>
#include <algorithm>
#define M 100001
#define inf 1061109567
typedef long long ll;
int min(int a, int b) { return a < b ? a : b; }
int max(int a, int b) { return a > b ? a : b; }
inline void read(int &T) {
int x = 0; bool f = 0; char c = getchar();
while (c < '0' || c > '9') { if (c == '-') f = !f; c = getchar(); }
while (c >= '0' && c <= '9') { x = x * 10 + c - '0'; c = getchar(); }
T = f ? -x : x;
}
bool vis[M];
int n, m, cnt, head[M], dis1[M], dis2[M];
struct Edge {
int u, v, w, nxt;
}e[M << 1];
void add(int u, int v, int w) {
e[++cnt].u = u, e[cnt].v = v, e[cnt].w = w;
e[cnt].nxt = head[u], head[u] = cnt;
}
void Dijkstra(int t) {
memset(vis, 0, sizeof vis);
if (t == 1) {
memset(dis1, 0x3f3f3f, sizeof dis1);
dis1[1] = 0;
for (int i = 1; i <= n; ++i) {
int k = 0;
for (int j = 1; j <= n; ++j) {
if (!vis[j] && dis1[j] < dis1[k]) k = j;
}
vis[k] = true;
for (int j = head[k]; j; j = e[j].nxt) {
if (!vis[e[j].v] && dis1[e[j].v] > dis1[k] + e[j].w) {
dis1[e[j].v] = dis1[k] + e[j].w;
}
}
}
} else {
memset(dis2, 0x3f3f3f, sizeof dis2);
dis2[n] = 0;
for (int i = 1; i <= n; ++i) {
int k = 0;
for (int j = 1; j <= n; ++j) {
if (!vis[j] && dis2[j] < dis2[k]) k = j;
}
vis[k] = true;
for (int j = head[k]; j; j = e[j].nxt) {
if (!vis[e[j].v] && dis2[e[j].v] > dis2[k] + e[j].w) {
dis2[e[j].v] = dis2[k] + e[j].w;
}
}
}
}
}
int main() {
read(n), read(m);
for (int i = 1, u, v, w; i <= m; ++i) {
read(u), read(v), read(w);
add(u, v, w), add(v, u, w);
}
Dijkstra(1), Dijkstra(n);
int ans = inf;
for (int i = 1; i <= n; ++i) {
int minn = inf;
for (int j = head[i]; j; j = e[j].nxt) {
if (dis1[i] + e[j].w + dis2[e[j].v] != dis1[n]) ans = min(ans, dis1[i] + e[j].w + dis2[e[j].v]);
minn = min(minn, e[j].w);
}
ans = min(ans, minn * 2 + dis1[i] + dis2[i]);
}
for (int i = 1; i <= m; ++i) {
ans = min(ans, e[i].w * 3 + dis1[e[i].u] + dis2[e[i].v]);
ans = min(ans, e[i].w * 3 + dis2[e[i].u] + dis1[e[i].v]);
}
std::cout << ans << '\n';
return 0;
}
After
发现自己对之前做过的题都没啥印象了,之前能切的题不会了,之前不会的题乱搞搞出来了,挺怪的,好像我把忘记技能返还的点数点到了乱搞上一样。