CrCPC 2024 做题记录
CrCPC 2024 做题记录
D. 排序(DrevniDiskovi)
双向 bfs
数据范围比较小,猜测是搜索。状态空间的大小 \(\le 10! = 3628800\),所以完全可以把整个状态空间遍历一遍。
考虑直接 bfs,对于一个排列,用 \(O(n^{3})\) 的时间枚举四段的三个分界点,再用 \(O(n)\) 的时间拼出新的排列,总时间复杂度 \(O(n! \cdot n^{4})\),不可通过。(如果用 map
记录是否访问过某状态,还要多一个 \(\log n!\),但可以用字符串存排列,然后用 unordered_map
代替 map
。)
如果把排列看作状态空间中的点,转移关系看作边,到这一步我们有两种优化的方向:一种是减少边数,即避免无效转移;另一种是减少点数,即避免访问无效的状态。vp 时我们一直在尝试前者,想办法减少一个状态能转移出的后继状态数量,但这似乎是不可行的。但这题的关键点在于终态是唯一确定的,这启示我们使用双向 bfs(这也是官方题解的做法),这本质上减少了访问的状态数。修改后确实可以通过本题。AC 记录
(但是我现在仍然不会严谨地分析这种做法的时间复杂度。std 似乎认为答案不会超过 \(6\),因为它双向搜索的边界都是 \(3\)。测试发现,对于答案为 \(6\) 的输入,入队的状态数大概为 \(70\) 万,远小于上界。)
Code
#include<bits/stdc++.h>
using namespace std;
int n;
string s0, sn;
unordered_map<string, int> f, g;
vector<string> getNext(string s, array<int, 4> p) {
vector<string> res;
for(int a = 0; a <= n; a++) {
for(int b = a; b <= n; b++) {
for(int c = b; c <= n; c++) {
array<string, 4> sub;
sub[0] = s.substr(0, a); // [0, a)
sub[1] = s.substr(a, b - a); // [a, b)
sub[2] = s.substr(b, c - b); // [b, c)
sub[3] = s.substr(c, n - c); // [c, n)
string nxt = "";
for(int i = 0; i < 4; i++) {
nxt += sub[p[i]];
}
res.push_back(nxt);
}
}
}
return res;
}
vector<string> getNextForwards(string s) {
return getNext(s, array<int, 4>{2, 0, 3, 1});
}
vector<string> getNextBackwards(string s) {
return getNext(s, array<int, 4>{1, 3, 0, 2});
}
void bfs() {
array<queue<string>, 2> que;
que[0].push(s0), f[s0] = 0;
que[1].push(sn), g[sn] = 0;
while(!que[0].empty() || !que[1].empty()) {
int d;
if(que[1].empty() || (!que[0].empty() && f[que[0].front()] < g[que[1].front()])) d = 0;
else d = 1;
if(d == 0) {
auto now = que[0].front();
que[0].pop();
if(g.count(now)) {
cout << f[now] + g[now] << '\n'; return;
}
vector<string> nxt(getNextForwards(now));
for(auto tmp: nxt) {
if(!f.count(tmp)) {
f[tmp] = f[now] + 1;
que[0].push(tmp);
}
}
} else {
auto now = que[1].front();
que[1].pop();
if(f.count(now)) {
cout << f[now] + g[now] << '\n'; return;
}
vector<string> nxt(getNextBackwards(now));
for(auto tmp: nxt) {
if(!g.count(tmp)) {
g[tmp] = g[now] + 1;
que[1].push(tmp);
}
}
}
}
}
int main() {
cin.tie(nullptr) -> sync_with_stdio(false);
f.max_load_factor(0.3f);
g.max_load_factor(0.3f);
cin >> n;
s0.resize(n), sn.resize(n);
for(int i = 0, x; i < n; i++) {
cin >> x;
s0[i] = (char)(x - 1 + '0');
sn[i] = (char)(i + '0');
}
bfs();
return 0;
}
H. 信步山中(Hoditi Hribima)
图论建模,最短路
同时考虑两个图是不方便的,不妨合成到一个图考虑。注意到每次移动能到达的点和移动次数的奇偶性有关,所以把每个点拆成两个,分别代表第奇数次移动和第偶数次移动。下面用 \((u, 1 / 0)\) 表示在点 \(u\),且下一次移动是第奇数/偶数次。
先用 Dijkstra 求出两个图中所有节点到 \(t\) 的距离,然后就可以按题目要求建出新图 \(G\),它有 \(2n\) 个点和不超过 \(m_1 + m_2\) 条边。那么我们要求的就是 \((s, 1)\) 到 \((t, 0)\) 或 \((t, 1)\) 的最长路。这里有两种情况使得答案为无穷大:
-
新图中 \((s, 1)\) 与 \((t, 0)\) 和 \((t, 1)\) 都不连通。
-
从 \((s, 1)\) 出发可以到达一个不经过 \((t, 0)\) 和 \((t, 1)\) 的正环。(根据题目描述,到达 \((t, 0)\) 或 \((t, 1)\) 之后就不能再移动了。)
第一种情况容易判断。对于第二种情况,我们可以直接删除 \(G\) 中 \((t, 0)\) 和 \((t, 1)\) 的所有出边,然后再判断是否存在正环,这样就不用考虑环经过了 \((t, 0)\) 和 \((t, 1)\) 的情况。由于边权都为正,所以只需判断是否存在 \((s, 0)\) 能到达的环,可以用 SPFA 或者拓扑排序解决。前者的时间复杂度为 \(O(nm)\),但本题似乎没有特意去卡(其实我也不知道能不能卡,因为 \(G\) 是自己建的图),因此可以通过;后者的时间复杂度为 \(O(n + m)\)。
我们可以在判断这些特殊情况的同时求出最长路。总时间复杂度为 \(O(n + m \log m)\)(如果使用拓扑排序)。
SPFA
#include<bits/stdc++.h>
using namespace std;
constexpr int INF = 0x3f3f3f3f;
int n, s, t;
struct Edge {
int v, w;
};
vector<vector<Edge>> G;
array<vector<vector<Edge>>, 2> Gs;
array<int, 2> ms;
array<vector<int>, 2> diss;
vector<int> ans;
struct Node {
int u, d;
bool operator < (const Node &rhs) const {
return d > rhs.d;
}
};
void Dijkstra(vector<vector<Edge>> &T, vector<int> &dis) {
dis.resize(n, INF);
vector<int> vis(n);
priority_queue<Node> heap;
heap.push({t, 0}), dis[t] = 0;
while(!heap.empty()) {
auto [u, d] = heap.top();
heap.pop();
if(vis[u]) {
continue;
}
vis[u] = true;
for(auto [v, w]: T[u]) {
if(dis[u] + w < dis[v]) {
dis[v] = dis[u] + w;
heap.push({v, dis[v]});
}
}
}
}
bool spfa() {
vector<int> vis(2 * n);
vector<int> cnt(2 * n);
ans.resize(2 * n, INF);
fill(ans.begin(), ans.end(), -INF);
queue<int> que;
que.push(s + n), ans[s + n] = 0;
while(!que.empty()) {
int u = que.front();
que.pop();
vis[u] = false;
for(auto [v, w]: G[u]) {
if(ans[u] + w > ans[v]) {
ans[v] = ans[u] + w;
if(++cnt[v] == 2 * n) {
return true;
}
que.push(v);
}
}
}
return false;
}
int main() {
cin.tie(nullptr) -> sync_with_stdio(false);
cin >> n >> s >> t;
--s, --t;
for(int i: {1, 0}) {
cin >> ms[i];
Gs[i].resize(n);
for(int j = 0, u, v, w; j < ms[i]; j++) {
cin >> u >> v >> w;
--u, --v;
Gs[i][u].push_back({v, w}), Gs[i][v].push_back({u, w});
}
Dijkstra(Gs[i], diss[i]);
}
G.resize(2 * n);
for(int i = 0; i < 2 * n; i++) {
int o = i / n, u = i % n; // o = 0: even; o = 1: odd
for(auto [v, w]: Gs[o][u]) {
if(diss[o][u] > diss[o][v]) {
G[i].push_back({v + (o ^ 1) * n, w});
}
}
}
G[t].clear(), G[n + t].clear();
if(spfa() || max(ans[t], ans[t + n]) == -INF) {
cout << -1 << '\n';
} else {
cout << max(ans[t], ans[t + n]) << '\n';
}
return 0;
}
拓扑排序
#include<bits/stdc++.h>
using namespace std;
constexpr int INF = 0x3f3f3f3f;
int n, s, t;
struct Edge {
int v, w;
};
vector<vector<Edge>> G;
array<vector<vector<Edge>>, 2> Gs;
array<int, 2> ms;
array<vector<int>, 2> diss;
vector<int> ans, deg, reach;
struct Node {
int u, d;
bool operator < (const Node &rhs) const {
return d > rhs.d;
}
};
void Dijkstra(vector<vector<Edge>> &T, vector<int> &dis) {
dis.resize(n, INF);
vector<int> vis(n);
priority_queue<Node> heap;
heap.push({t, 0}), dis[t] = 0;
while(!heap.empty()) {
auto [u, d] = heap.top();
heap.pop();
if(vis[u]) {
continue;
}
vis[u] = true;
for(auto [v, w]: T[u]) {
if(dis[u] + w < dis[v]) {
dis[v] = dis[u] + w;
heap.push({v, dis[v]});
}
}
}
}
void dfs(int u) {
reach[u] = 1;
for(auto [v, w]: G[u]) {
deg[v]++;
if(!reach[v]) {
dfs(v);
}
}
}
bool toposort() {
ans.resize(2 * n);
queue<int> que;
for(int i = 0; i < 2 * n; i++) {
if(!deg[i] && reach[i]) {
que.push(i);
}
}
while(!que.empty()) {
int u = que.front();
que.pop();
for(auto [v, w]: G[u]) {
ans[v] = max(ans[v], ans[u] + w);
if(--deg[v] == 0) {
que.push(v);
}
}
}
for(int i = 0; i < 2 * n; i++) {
if(deg[i] && reach[i]) {
return true;
}
}
return false;
}
int main() {
cin.tie(nullptr) -> sync_with_stdio(false);
cin >> n >> s >> t;
--s, --t;
for(int i: {1, 0}) {
cin >> ms[i];
Gs[i].resize(n);
for(int j = 0, u, v, w; j < ms[i]; j++) {
cin >> u >> v >> w;
--u, --v;
Gs[i][u].push_back({v, w}), Gs[i][v].push_back({u, w});
}
Dijkstra(Gs[i], diss[i]);
}
G.resize(2 * n), deg.resize(2 * n);
for(int i = 0; i < 2 * n; i++) {
int o = i / n, u = i % n; // o = 0: even; o = 1: odd
for(auto [v, w]: Gs[o][u]) {
if(diss[o][u] > diss[o][v]) {
G[i].push_back({v + (o ^ 1) * n, w});
}
}
}
reach.resize(2 * n);
dfs(s + n);
if(!reach[t] && !reach[t + n]) {
cout << "-1\n"; return 0;
}
G[t].clear(), G[n + t].clear();
if(toposort()) {
cout << -1 << '\n';
} else {
cout << max(ans[t], ans[t + n]) << '\n';
}
return 0;
}