NHSPC 2023 做题记录

NHSPC 2023 做题记录

洛谷 Unofficial Mirror | 官方题解 | Archive

赛时做了 BGI。

下文中题目顺序大致按难度升序。

B. 人工智能模拟(AI Simulation)

直接枚举就行。

G. 博物馆(Museum)

选价值最大的 \(k\) 个展品,如果有相同的就选择靠近左边的。当选择的展品确定后,最优移动顺序是唯一的:从左往右依次搬动展品。

H. 整数的回文分解法(Palindrome)

递推

我怎么这都不会/kk

考虑递推。设答案为 \(D_{n}\)。由于回文序列有这样的递归性质:删去一个回文序列两端的数,剩下的数仍然构成回文序列。所以我们可以枚举在两端填的数,即:

\[D_{n} = \begin{cases} (D_{n - 2} + D_{n - 4} + \cdots + D_{1}) + 1, \quad n \text{ is odd} \\ (D_{n - 2} + D_{n - 4} + \cdots + D_{0}) + 1, \quad n \text{ is even} \end{cases} \]

\(+ 1\) 是考虑到序列长度为 \(1\) 的 corner case。边界情况是 \(D_{0} = D_{1} = 1\)

把递推式做差得 \(D_{n} - D_{n - 2} = D_{n - 2}\),即 \(D_{n} = 2D_{n - 2}\),因此

\[\boxed{D_{n} = 2^{\lfloor \frac{n}{2} \rfloor}} \]

用快速幂加速即可,时间复杂度 \(O(\log n)\)AC 记录

本题到这里就结束了,但有一些东西是值得深入讨论的:

写递推式的时候,考虑枚举什么量是很重要的。枚举了某个量之后,就可以把问题缩减到更小的规模。比较容易想到的切入点有两个:序列中间的数和序列两端的数,这两种都可能是可行的,而我第一反应是选择前者。(实际上我还是经验太少,处理回文序列时第二种方式更常见)

如果以“序列中间的数”为切入点,还能得到递推式吗?很遗憾不行。当 \(n\) 为奇数时,回文序列的长度也一定为奇数,但 \(n\) 为偶数时回文序列的奇偶性则不确定。删去回文序列中间的一或两个数,剩下的必须是一个长度为偶数的回文序列。但 \(D_{n}\) 表示的不是“长度为偶数的回文序列的个数”,所以没法转移,多设状态的话又太过麻烦。遇到这种情况,不妨回头看看一开始有没有更好的方向。

H. 迷宫钥匙圈(Maze)

bfs,模拟

直接 bfs 就好。

由于小钢珠的个数不超过 \(3\) 个,所以可以用坐标集合 \(\{(x_{1}, y_{1}), (x_{2}, y_{2}), (x_{3}, y_{3})\}\) 加上方向来表示一个状态。对于已经掉出迷宫的小钢珠,不妨用 \((-1, -1)\) 来表示。因此状态的个数为 \(O(n^3m^3)\)

转移时,不妨把旋转迷宫想象成改变重力的方向。可以直接模拟求出旋转后小钢珠的新位置。如果只有一个小钢珠,模拟沿重力方向下落的过程,直到掉出迷宫或者碰到障碍。如果有多个小钢珠,还要考虑小钢珠之间互相阻挡的问题。我的做法比较暴力:对每个小钢珠分别模拟掉落的过程,但途中遇到别的小钢珠也要停止。由于只有 \(3\) 个小钢珠,所以模拟 \(3\) 次以后一定能得到正确的位置。精细实现的话肯定更快,但能过就不要给自己增加代码难度了。

细节可能有点多,下面列举几个,其余见代码:

  1. 所有小钢珠都是相同的,所以存储小钢珠坐标时应该使用无序容器(set 等),而非 vector 等有序容器。

  2. 又由于用 \((-1, -1)\) 表示掉出去的小钢珠,所以小钢珠的坐标可能相同,因此应该用 multiset 存储小钢珠的坐标。

AC 记录

Code
#include<iostream>
#include<map>
#include<set>
#include<vector>
#include<queue>

using namespace std;

constexpr array<int, 4> dx{1, 0, -1, 0}, dy{0, -1, 0, 1};
int n, m, cnt;
vector<vector<char>> s;

struct Node {
    int dir; // dir in {0, 1, 2, 3} (D L U R)
    multiset<pair<int, int>> pos;
    Node(int _dir, multiset<pair<int, int>> &_pos): dir(_dir), pos(_pos) {}
    Node(multiset<pair<int, int>> &_pos): dir(0), pos(_pos) {}
    bool operator < (const Node &rhs) const {
        return dir == rhs.dir ? (pos < rhs.pos) : (dir < rhs.dir);
    }
};

queue<Node> que;
map<Node, int> step;

bool out(int x, int y) {
    return x < 1 || x > n || y < 1 || y > m;
}

bool valid(int x, int y, multiset<pair<int, int>> &pos) {
    return out(x, y) || (s[x][y] == 's' && !pos.count(make_pair(x, y)));
}

void bfs() {
    while(!que.empty()) {
        auto now = que.front();
        auto [dir, pos] = que.front();
        que.pop();

        bool flag = true;
        for(auto [x, y]: pos) {
            if(!out(x, y)) {
                flag = false; break;
            } 
        }
        if(flag) {
            cout << step[now] << '\n';
            return;
        }

        for(int d: {-1, 1}) {
            int td = (dir + d + 4) % 4;
            auto tmp(pos);
            for(int i = 0; i < cnt; i++) {
                multiset<pair<int, int>> nxt(tmp);
                for(auto it = tmp.begin(); it != tmp.end(); it++) {
                    auto [x, y] = *it;
                    auto self = nxt.find(make_pair(x, y));
                    nxt.erase(self);
                    if(x == -1 && y == -1) {
                        nxt.emplace(-1, -1); continue;
                    }
                    int tx = x + dx[td], ty = y + dy[td];
                    while(valid(tx, ty, nxt)) {
                        x = tx, y = ty;
                        if(out(x, y)) {
                            x = -1, y = -1;
                            break;
                        }
                        tx = x + dx[td], ty = y + dy[td];
                    }
                    nxt.emplace(x, y);
                }
                tmp.swap(nxt);
            }
            Node nxt(td, tmp);
            if(step.count(nxt)) {
                continue;
            } else {
                step[nxt] = step[now] + 1;
                que.push(nxt);
            }
        }

    }

    cout << -1 << '\n';
}

int main() {
    cin.tie(nullptr) -> sync_with_stdio(false);

    cin >> n >> m;
    s.resize(n + 1, vector<char>(m + 1));
    for(int i = 1; i <= n; i++) {
        for(int j = 1; j <= m; j++) {
            cin >> s[i][j];
        }
    }

    multiset<pair<int, int>> pos0;
    for(int i = 1; i <= n; i++) {
        for(int j = 1; j <= m; j++) {
            if(s[i][j] == 'b') {
                cnt++, s[i][j] = 's';
                pos0.emplace(i, j);
            }
        }
    }
    que.emplace(pos0), step[que.front()] = 0;
    bfs();

    return 0;
}

I. 对战机器马(Race)

枚举,线段树

对于第 \(i\) 场比赛,如果使用了魔力值为 \(x\) 的燃料,B 想要获胜,则 \((b_{i} + x) \bmod P > a_{i}\),解得 \(a - b < x < P - b\)\(x > P + a - b\)(解集可能为空,下界还要和 \(0\)\(\max\),这些细节就不多提了)。

在数轴上考虑,就相当于有 \(n\) 个点集,每个点集形如两条线段,要选择两个点,使得这两个点被尽可能多的点集覆盖(每个点集只计算一次)。

如果只选择一个点,做法是简单的:用线段树维护每个点被覆盖的次数,则答案为全局 \(\max\)。由于魔力值的范围在 \([0, P)\) 之间,所以需要先离散化。(动态开点线段树的时间复杂度过高,不过你要是使劲卡常或许可以 AC,反正我是没过

现在我们要选择两个点,不妨先枚举一个点,然后快速求出在已经选定一个点的情况下的最优答案。对于第一个点,直接枚举肯定是不行的,但离散化之后,整个数轴被分成了 \(O(n)\) 段,每段内的所有点都是等价的,所以枚举第一个点在哪一段即可。对于第二个点,还是用线段树维护。在枚举第一个点的过程中,在线段树上动态维护没有覆盖第一个点的点集,这样就不会重复计算同一个点集的贡献。具体而言,当某个点集覆盖了第一个点时,在线段树上删去这个点集,没有覆盖第一个点时则加回来。求第二个点的答案是还是直接取全局 \(\max\)

时间复杂度 \(O(n \log n)\)AC 记录

Code
#include<bits/stdc++.h>

using namespace std;

int n, m, P;
vector<int> a, b, num;

struct Node {
    int val, op, id;
    bool operator < (const Node &rhs) const {
        return (val == rhs.val) ? (op < rhs.op) : (val < rhs.val);
    }
};
vector<Node> c;

struct Seg {
    int l, r;
};
vector<vector<Seg>> seg;

constexpr int SZ = 1e7;
struct SegmentTree {
    #define lson (id << 1)
    #define rson (id << 1 | 1)
    vector<int> mx, tag;

    void init() {
        mx.resize(m << 2), tag = mx;
    }

    void update(int id) {
        mx[id] = max(mx[lson], mx[rson]);
    }

    void apply(int id, int x) {
        mx[id] += x, tag[id] += x;
    }

    void pushdown(int id) {
        apply(lson, tag[id]), apply(rson, tag[id]);
        tag[id] = 0;
    }

    void change(int id, int l, int r, int L, int R, int x) {
        if(l == L && r == R) {
            apply(id, x); return;
        }
        pushdown(id);
        int mid = (l + r) >> 1;
        if(R <= mid) change(lson, l, mid, L, R, x);
        else if(L > mid) change(rson, mid + 1, r, L, R, x);
        else change(lson, l, mid, L, mid, x), change(rson, mid + 1, r, mid + 1, R, x);
        update(id);
    }
    void change(int L, int R, int x) {
        change(1, 0, m - 1, L, R, x);
    }

    int query() {
        return mx[1];
    }
    #undef lson
    #undef rson
}tr;

void modify(int id, int o) {
    for(auto [l, r]: seg[id]) {
        tr.change(l, r, o);
    }
}
void add(int id) { modify(id, 1); }
void erase(int id) { modify(id, -1); }

int getrk(int val) {
    auto it = lower_bound(num.begin(), num.end(), val);
    val = (int)distance(num.begin(), it);
    return val;
}

int main() {
    // freopen("y.in", "r", stdin);
    cin >> n >> P;
    a.resize(n + 1), b.resize(n + 1);
    for(int i = 1; i <= n; i++) {
        cin >> a[i];
    }
    for(int i = 1; i <= n; i++) {
        cin >> b[i];
    }

    seg.resize(n + 1);
    for(int i = 1; i <= n; i++) {
        int l1 = max(a[i] - b[i] + 1, 0), r1 = P - b[i] - 1;
        if(l1 <= r1) {
            c.push_back(Node{l1, 1, i});
            c.push_back(Node{r1 + 1, -1, i});
            seg[i].push_back(Seg{l1, r1});
            num.push_back(l1), num.push_back(r1), num.push_back(r1 + 1);
        }
        int l2 = P + a[i] - b[i] + 1, r2 = P - 1;
        if(l2 <= r2) {
            c.push_back(Node{l2, 1, i});
            c.push_back(Node{r2 + 1, -1, i});
            seg[i].push_back(Seg{l2, r2});
            num.push_back(l2), num.push_back(r2), num.push_back(r2 + 1);
        }
    }
    sort(c.begin(), c.end());
    sort(num.begin(), num.end());
    num.erase(unique(num.begin(), num.end()), num.end());
    // for(int x: num) { cerr << x << ' '; } cerr << '\n';
    m = (int)num.size();
    for(auto &[val, op, id]: c) {
        val = getrk(val);
    }
    for(int i = 1; i <= n; i++) {
        for(auto &[l, r]: seg[i]) {
            l = getrk(l), r = getrk(r);
        }
    }

    // cerr << "Ok\n";

    int cnt = 0, ans = 0;
    tr.init();
    for(int i = 1; i <= n; i++) {
        add(i);
    }
    for(auto it = c.begin(); it != c.end(); ) {
        int now = it -> val;
        while(it != c.end() && it -> val == now) {
            if(it -> op == 1) {
                cnt++;
                erase(it -> id);
            } else {
                cnt--;
                add(it -> id);
            }
            it++;
        }
        int tmp = cnt + tr.query();
        ans = max(ans, tmp);
        // cerr << now << ' ' << tmp << ' ' << cnt << ' ' << tr.query() << '\n';
    }

    cout << ans << '\n';

    return 0;
}

F. 恐怖的黑色魔物(Monster)

图论建模,多源 BFS,最小瓶颈路,Kruskal 重构树

赛时完全没想到往图论方向考虑,一直在想是不是有什么数据结构/kk

先考虑第二个 Subtask,即求出每个点到最近的餐厅的距离。把房间看作点,餐厅看作关键点,问题就变成了:给定一个无向无权图,有若干个关键点,求出每个点到最近的关键点的距离。由于边无权,所以直接使用多源 BFS 即可。

记节点 \(u\) 的“黑色恐怖距离”为 \(d_{u}\),则原问题相当于:给定一个无向图,每个点有点权,多次询问某两个点之间的路径,使得路径上的最小点权最大。如果把点权换成边权,这个问题就是 最大瓶颈路 问题,可以求出任意一棵最大生成树以后倍增解决,或者建 Kruskal 重构树。那么怎样把点权转换成边权呢?对于两个相邻的点 \(u\)\(v\),令它们之间的边权为 \(\min(d_{u}, d_{v})\),正确性显然。(需要特判起点和终点相同的情况)转化之后就可以用上文提到的方法解决了。

如果用最大生成树 + 倍增解决,要先把所有边权从大到小排序再依次选择,但通过一个技巧,不需要把所有边的边权都显式地求出来。由于 bfs 按 \(d_{u}\) 从小到大的顺序访问节点,所以按 bfs 序的倒序枚举就可以把 \(d_{u}\) 从大到小排序。对于当前枚举的 \(u\),枚举与它相邻的节点 \(v\),如果 \(v\) 已经被访问过,就说明 \(d_{v} \ge d_{u}\),因此 \(\min(d_{u}, d_{v}) = d_{u}\),于是就可以尝试在最大生成树中加入边 \((u, v)\)。容易看出这样做等价于按 \(\min(d_{u}, d_{v}) = d_{u}\) 降序排序后枚举,所以一定是正确的。

\(V = FMN\),则时间复杂度为 \(O(V (\alpha(V) + \log V))\)AC 记录

Code
#include<vector>
#include<array>
#include<algorithm>
#include<iostream>
#include<queue>
#include<numeric>

using namespace std;

constexpr int INF = 0x3f3f3f3f;
int N, M, F, V, R, Q;
vector<int> dis, dep;
vector<vector<int>> G, anc, mn;

int getid(int x, int y, int z) {
    return x * M * N + y * N + z;
}
array<int, 3> getco(int u) {
    int z = u % N, y = u % (M * N) / N, x = u / (M * N);
    return array<int, 3>{x, y, z};
}

int query(int a, int b, int c, int x, int y, int z) {
    int u = getid(a, b, c), v = getid(x, y, z), ans = INF;
    if(dep[u] < dep[v]) swap(u, v);
    int t = dep[u] - dep[v];
    ans = min(dis[v], dis[u]);
    for(int i = 0; (1 << i) <= t; i++) {
        if(t & (1 << i)) {
            ans = min(ans, mn[u][i]);
            u = anc[u][i];
        }
    }
    if(u == v) return ans;
    for(int i = __lg(V); i >= 0; i--) {
        if(anc[u][i] != anc[v][i]) {
            ans = min({ans, mn[u][i], mn[v][i]});
            u = anc[u][i], v = anc[v][i];
        }
    }
    ans = min(ans, dis[anc[u][0]]);
    return ans;
}

void dfs(int u, int fa = -1) {
    for(int i = 1; (1 << i) <= dep[u]; i++) {
        anc[u][i] = anc[anc[u][i - 1]][i - 1];
        mn[u][i] = min(mn[u][i - 1], mn[anc[u][i - 1]][i - 1]);
    }
    for(int v: G[u]) {
        if(v == fa) continue;
        dep[v] = dep[u] + 1, anc[v][0] = u, mn[v][0] = min(dis[v], dis[u]);
        dfs(v, u);
    }
}

struct DSU {
    vector<int> fa;

    DSU(int n): fa(n) {
        iota(fa.begin(), fa.end(), 0);
    }

    int getfa(int u) {
        return u == fa[u] ? u : fa[u] = getfa(fa[u]);
    }

    void merge(int x, int y) {
        int fx = getfa(x), fy = getfa(y);
        fa[fy] = fx;
    }

    bool same(int x, int y) {
        return getfa(x) == getfa(y);
    }
};

int main() {
    cin.tie(nullptr) -> sync_with_stdio(false);
    
    cin >> F >> M >> N >> R;
    V = F * M * N;
    queue<int> que;
    dis.resize(V, INF);
    for(int i = 1, x, y, z; i <= R; i++) {
        cin >> x >> y >> z;
        x--, y--, z--;
        int id = getid(x, y, z);
        que.push(id), dis[id] = 0;
    }

    vector<int> p;
    while(!que.empty()) {
        int u = que.front();
        que.pop();
        p.push_back(u);

        auto add = [&](int tx, int ty, int tz) {
            if(tx < 0 || tx >= F || ty < 0 || ty >= M || tz < 0 || tz >= N) {
                return;
            }
            int nxt = getid(tx, ty, tz);
            if(dis[nxt] != INF) return;
            que.push(nxt), dis[nxt] = dis[u] + 1;
        };

        auto [x, y, z] = getco(u);
        add(x - 1, y, z), add(x + 1, y, z);
        add(x, y - 1, z), add(x, y + 1, z);
        add(x, y, z - 1), add(x, y, z + 1);
    }


    reverse(p.begin(), p.end());
    DSU dsu(V);
    G.resize(V);
    vector<int> vis(V);
    for(int u: p) {
        vis[u] = 1;
        auto [x, y, z] = getco(u);
        
        auto add = [&](int tx, int ty, int tz) {
            if(tx < 0 || tx >= F || ty < 0 || ty >= M || tz < 0 || tz >= N) {
                return;
            }
            int v = getid(tx, ty, tz);
            if(!vis[v]) return;
            if(dsu.same(u, v)) return;
            G[u].push_back(v), G[v].push_back(u);
            dsu.merge(u, v);
        };

        add(x - 1, y, z), add(x + 1, y, z);
        add(x, y - 1, z), add(x, y + 1, z);
        add(x, y, z - 1), add(x, y, z + 1);
    }

    dep.resize(V), anc.resize(V, vector<int>(__lg(V) + 1, -1));
    mn.resize(V, vector<int>(__lg(V) + 1, INF));
    dfs(0);

    cin >> Q;
    for(int i = 1; i <= Q; i++) {
        int a, b, c, x, y, z;
        cin >> a >> b >> c >> x >> y >> z;
        --a, --b, --c, --x, --y, --z;
        cout << query(a, b, c, x, y, z) << '\n';
    }

    return 0;
}

C. 与自动辅助驾驶畅游世界(Autocopilot)

图论,dp

非常有趣的题目。下面我们按 subtask 做。

Subtask 2 有向无环图

看到 DAG 自然想到按(反)拓扑序 dp。设 \(f(u)\) 表示从 \(u\) 出发到终点所需的最少代币数量。对于除了终点之外的每个点 \(u\),有两种选择:

  1. 随机前往下一个点。此时最坏情况下需要花费 \(\max_{(u, v) \in E} f(v)\) 个代币。
  2. 支付一个代币,此时最优策略是选择前往花费最小的点,花费 \(1 + \min_{(u, v) \in E} f(v)\) 个代币。

综上所述转移方程为:

\[f(u) = \min(\max_{(u, v) \in E} f(v), 1 + \min_{(u, v) \in E} f(v)) \]

初始化:当 \(u = t\)\(f(u) = 0\),否则 \(f(u) = +\infty\)

Subtask 3 \(n \le 100, m \le 1000\)

有环了之后我们似乎找不到一个固定的 dp 顺序了。不妨想一想 Dijkstra 是怎么在一般图上运行的:求解最短路问题时,我们按距离从小到大依次确定。当没有负边时,Dijkstra 就是正确的。能不能把这种思想应用到本题中呢?

秉承这种思想,我们按 \(f(u)\) 从小到大转移。一个点 \(u\) 只有两种决策:要么随机走,要么付一个代币选择一个点 \(v\) 走。当付钱走时,\(f(u) = f(v) + 1\);而随机走时,由于不花费额外的代币,所以会转移到某个使得 \(f(u) = f(v)\) 的点 \(v\)

既然如此,我们这样转移:首先假设我们已经计算出了所有满足 \(f(u) < x\) 的点 \(u\),也就是说剩下的点至少要花费 \(x\) 个代币。此时我们要确定所有满足 \(f(u) = x\) 的点。重要的一点是:在这些点中,最优策略为随机游走的点可以从最优策略为付钱的点转移过来,反之则不行。(因为最优策略为付钱的点的 \(f\) 值比它的后继都大,这样就不满足 \(f(u) = x\))所以我们只能先确定最优策略为付钱的点,再确定最优策略为随机游走的点。也就是说我们的转移顺序如下:

\(x = 0\)(终点)\(\to x = 1\) (付钱)\(\to x = 1\) (随机游走) \(\to x = 2\)(付钱)\(\to x = 2\)(随机游走)……

对于最优策略为付钱的点:遍历所有未确定 \(f\) 值的点 \(u\),如果它的后继中存在点 \(v\) 满足 \(f(v) < x\),那么它的最优策略就是付钱,因此有 \(f(u) = x\)。这是因为根据假设,所有满足 \(f(u) < x\) 的点都被计算完了,所以 \(f(u)\) 只能是 \(x\)

对于最优策略为随机游走的点:删去图中所有答案已经确定的点,遍历所有剩下的 \(u\),从 \(u\) 开始 dfs。如果 \(u\) 可以到达某个不能到达 \(t\) 的点,就说明 \(f(u) > x\)。由于此时图中最大的 \(f\) 值为 \(x\),所以 \(f(u)\) 不可能大于 \(x\),因此 \(f(u) = x\)

为什么在 dfs 之前要删掉答案已经确定的点呢?这是因为我们只要求从 \(u\) 开始随机游走,在遇到某个答案已经确定的点之前 \(v\),不会走到某个不可达 \(t\) 的点。换言之我们只在 \(u \rightsquigarrow v\) 路径中随机游走,因此途中不能到达不可达 \(t\) 的点,但在到达 \(v\) 之后是可以花钱的,所以没有限制。

显然有 \(f(u) < n\),所以总共会 dp \(n\) 轮。每一轮中,确定最优策略为付钱的点需要 \(O(n)\),确定最优策略为随机游走的点需要 \(O(n^{2}(n + m))\),因此总时间复杂度为 \(O(n^{2}m)\)提交记录

Code
#include<bits/stdc++.h>

using namespace std;

constexpr int INF = 0x3f3f3f3f;
int n, m, s, t;
vector<vector<int>> G;
vector<int> f, vis, reach;
bool flag;

void dfs(int u) {
    if(!reach[u]) flag = true;
    vis[u] = 1;
    for(int v: G[u]) {
        if(f[v] == INF && !vis[v]) {
            dfs(v);
        }
    }
}

int main() {
    cin.tie(nullptr) -> sync_with_stdio(false);

    cin >> n >> m;
    G.resize(n + 1);
    for(int i = 1, u, v; i <= m; i++) {
        cin >> u >> v;
        G[u].push_back(v);
    }
    cin >> s >> t;

    vis.resize(n + 1), reach.resize(n + 1);
    f.resize(n + 1, INF);
    for(int i = 1; i <= n; i++) {
        fill(vis.begin(), vis.end(), 0);
        dfs(i);
        if(vis[t]) reach[i] = 1;
    }

    f[t] = 0;
    for(int x = 0; x <= n; x++) {
        // pay a coin, f[u] = x
        if(x == 0) goto Random;
        for(int i = 1; i <= n; i++) {
            if(f[i] != INF) continue;
            for(int u: G[i]) {
                if(f[u] < x) {
                    f[i] = x;
                    break;
                }
            }
        }
        // random, f[u] = x
        Random:
        for(int i = 1; i <= n; i++) {
            if(f[i] != INF) continue;
            fill(vis.begin(), vis.end(), 0);
            flag = false, dfs(i);
            if(!flag) f[i] = x;
        }
    }

    cout << (f[s] == INF ? -1 : f[s]) << '\n';
    
    return 0;
}

Subtask 4 正解

我们的时间复杂度瓶颈主要在于,确定最优策略为随机游走的点时,对每个点都要 dfs 一遍。优化是简单的:记所有不可达 \(t\) 的点构成的点集为 \(S\),在反图上以 \(S\) 为起点 dfs,那么 \(S\) 可达的点就对应那些不能随机游走的点,更新剩下的点的 \(f\) 值即可。

时间复杂度 \(O(n(n + m))\),可以通过。AC 记录

Code
#include<bits/stdc++.h>

using namespace std;

constexpr int INF = 0x3f3f3f3f;
int n, m, s, t;
vector<vector<int>> G, G2;
vector<int> f, vis, reach;
bool flag;

void dfs(int u) {
    vis[u] = 1;
    for(int v: G[u]) {
        if(!vis[v]) dfs(v);
    }
}

void dfs2(int u) {
    vis[u] = 1;
    for(int v: G2[u]) {
        if(!vis[v] && f[v] == INF) {
            dfs2(v);
        }
    }
}

int main() {
    cin.tie(nullptr) -> sync_with_stdio(false);

    cin >> n >> m;
    G.resize(n + 1), G2.resize(n + 1);
    for(int i = 1, u, v; i <= m; i++) {
        cin >> u >> v;
        G[u].push_back(v), G2[v].push_back(u);
    }
    cin >> s >> t;

    vis.resize(n + 1), reach.resize(n + 1);
    for(int i = 1; i <= n; i++) {
        fill(vis.begin(), vis.end(), 0);
        dfs(i);
        if(vis[t]) reach[i] = 1;
    }

    f.resize(n + 1, INF), f[t] = 0;
    for(int x = 0; x <= n; x++) {
        // pay a coin, f[u] = x
        if(x == 0) goto Random;
        for(int i = 1; i <= n; i++) {
            if(f[i] != INF) continue;
            for(int u: G[i]) {
                if(f[u] < x) {
                    f[i] = x;
                    break;
                }
            }
        }
        // random, f[u] = x
        Random:
        for(int i = 1; i <= n; i++) {
        }
        fill(vis.begin(), vis.end(), 0);
        for(int i = 1; i <= n; i++) {
            if(!reach[i]) {
                dfs2(i);
            }
        }
        for(int i = 1; i <= n; i++) {
            if(f[i] == INF && !vis[i]) {
                f[i] = x;
            }
        }
    }

    cout << (f[s] == INF ? -1 : f[s]) << '\n';
    
    return 0;
}

D. 共同子凸包(Convex Hull)

太好了是计算几何,我们没救了

posted @ 2025-03-18 21:53  DengStar  阅读(143)  评论(0)    收藏  举报