【题解】2026ICPC网络赛1

写在前面

补题链接
六题手速场,有一百个左右的学校是六题打的校排
这里按难度给出以下题目的题解(后续可能会更新):
\(F M C L D A N G\)

F. 50 Years of Excellence

1.题面简述

给定 \(n\) 年的 \(ICPC\) 赛题,每年的赛题都含有 \(m\) 个题目,每个题目有一个整数的 \(rating\),定义每年的 \(rating\) 为当年 \(m\) 个赛题 \(rating\) 之合,定义优秀赛年如下:

  • 该年 \(rating\) 严格小于上一年的 \(rating\)
  • 第一年的 \(rating\)\(0\) 比较

问有多少优秀赛年

2.分析

简单的签到题,模拟即可,时间复杂度 \(O(nm)\)

点击查看代码
n, m = map(int, input().split())
a = [0] + list(sum(map(int, input().split())) for i in range(n))
ans = sum(int(a[i + 1] < a[i]) for i in range(n))
print(ans)

M. Check In

1.题面简述

\(n\) 支队伍报名了某场 \(ICPC\) 比赛,现在他们要报到,你是报到的工作人员,有 \(m\) 队次报到信息,你需队伍的状态:

  • 成功报到 \(OK\),该队伍报名了这场比赛且第一次报到
  • 重复报到 \(REPEAT\),该队伍报名了这场比赛且之前已经报到
  • 报到失败 \(WRONG\),该队伍未报名比赛

输出 \(m\) 条报到信息对应的状态。

2.分析

简单的签到题,使用 \(set\) 模拟,时间复杂度 \(O(m + n)\)\(O(m \cdot logm + n \cdot logn)\)

点击查看代码
n, m = map(int, input().split())
st1 = set()
st2 = set()
for i in range(n):
    st1.add(input())
for i in range(m):
    s = input()
    if s in st1 and s not in st2:
        print('OK')
    elif s in st1 and s in st2:
        print('REPEAT')
    elif s not in st1:
        print('WRONG')
    st2.add(s)

C. Permutation Inversions

1. 题面简述

现在有一个未知排列 \(p_1,p_2,\ldots,p_n\),题目给出 \(m\) 条限制。

每条限制包含一个区间 \([l_i,r_i]\),以及区间内所有下标组成的一个排列

\[q_{i,1},q_{i,2},\ldots,q_{i,r_i-l_i+1}, \]

它表示

\[p_{q_{i,1}}<p_{q_{i,2}}<\cdots<p_{q_{i,r_i-l_i+1}}. \]

要求构造一个满足全部限制,并且逆序对数量最少的排列。如果不存在合法排列,输出 -1

2. 分析

首先只考虑怎样满足大小关系。对于一条限制,只需要按照给出的顺序连边:

\[q_1\to q_2\to\cdots\to q_k. \]

如果最后得到的图中存在环,说明某些位置互相要求比对方小,不可能构造出合法排列,直接输出 -1

如果图中没有环,我们求出一个拓扑序

\[v_1,v_2,\ldots,v_n, \]

然后令

\[p_{v_i}=i, \]

就可以满足所有边对应的大小关系。

接下来考虑怎样让逆序对最少。对于两个原下标 \(i<j\),如果在拓扑序中 \(j\) 出现在 \(i\) 前面,那么最终就有 \(p_i>p_j\),产生一个逆序对。所以我们希望拓扑序尽量按照原下标从小到大排列,由此想到使用小根堆维护所有入度为零的点,每次取编号最小的点。

正确性证明

对于一般的有向无环图,字典序最小拓扑序不一定拥有最少逆序对。本题能够这样做,是因为每条限制会将一个完整连续区间中的所有位置全部排好顺序。

考虑三个下标 \(i<j<k\)。如果限制能够推出 \(k\) 必须在 \(i\) 前面,那么从 \(k\)\(i\) 的路径一定会经过一条跨过位置 \(j\) 的边。这条边来自某个完整区间,而 \(j\) 也在这个区间中,所以该区间一定同时确定了 \(j\) 与这条边两端的顺序。由此可知,必然满足下面两种情况中的至少一种:

\[k\to j\qquad\text{or}\qquad j\to i. \]

现在假设小根堆得到的拓扑序中出现了一个逆序对 \(i<j\),即 \(j\)\(i\) 更早出队。当 \(j\) 出队时,\(i\) 还不是零入度点。沿着 \(i\) 的前驱一直向前,可以找到剩余图中的一个零入度点 \(k\),并且 \(k\) 能够到达 \(i\)

小根堆选择了 \(j\) 而没有选择 \(k\),说明 \(k>j\)。此时有

\[i<j<k, \]

并且 \(k\to i\)。根据上面的区间性质,要么 \(k\to j\),要么 \(j\to i\)。第一种情况与 \(j\) 已经是零入度点矛盾,因此只能是

\[j\to i. \]

也就是说,小根堆拓扑序中产生的每一个逆序对,都是题目限制强制要求的。任何合法排列都无法消除这些逆序对,所以该拓扑序的逆序对数量一定最少。

每条长度为 \(len\) 的限制只需要加入 \(len-1\) 条边。设所有限制长度之和为 \(S\),建图复杂度为 \(O(S)\),小根堆拓扑排序复杂度为 \(O(n\log n+S)\),空间复杂度为 \(O(n+S)\)

点击查看代码
#include<bits/stdc++.h>
using namespace std;

using i32 = int;
using i64 = long long;
using i128 = __int128;
#define all(x) (x).begin(), (x).end()
#define dbg(x) cerr << #x << " = " << (x) << endl;

int T = 1, n, m, k, ans, cnt;

void solve() {
    cin >> n >> m;
    vector<vector<int>> g(n + 1);
    vector<int> deg(n + 1);
    for (int i = 1; i <= m; i++) {
        int l, r, pre;
        cin >> l >> r >> pre;
        for (int j = l + 1, x; j <= r; j++) {
            cin >> x;
            g[pre].push_back(x);
            deg[x]++;
            pre = x;
        }
    }

    priority_queue<int, vector<int>, greater<int>> q;
    for (int i = 1; i <= n; i++) {
        if (!deg[i]) q.push(i);
    }

    vector<int> p(n + 1);
    int cur = 0;
    while (!q.empty()) {
        int x = q.top();
        q.pop();
        p[x] = ++cur;
        for (auto y: g[x]) {
            if (!--deg[y]) q.push(y);
        }
    }

    if (cur < n) {
        cout << -1 << '\n';
        return;
    }
    for (int i = 1; i <= n; i++) {
        cout << p[i] << " \n"[i == n];
    }
}

int main() {
    ios::sync_with_stdio(false);
    cin.tie(nullptr);
    for(cin >> T; T--; solve());
    return 0;
}

L. Longest Common Prefix

1. 题面简述

依次给出 \(n\) 个字符串。对于前 \(i\) 个字符串,定义 \(f_{i,j}\) 为:从中恰好选择 \(j\) 个字符串,它们的最长公共前缀长度最大是多少。

对于每个 \(i\),要求计算

\[\sum_{j=1}^{i}(f_{i,j}\oplus j). \]

2. 分析

首先考虑怎样描述 \(f_{i,j}\)。如果一个前缀 \(p\) 在前 \(i\) 个字符串中出现了至少 \(j\) 次,我们就可以从这些字符串中任选 \(j\) 个,它们的最长公共前缀长度至少为 \(|p|\)

反过来,如果选出的 \(j\) 个字符串有长度为 \(len\) 的公共前缀,那么这个前缀一定在前 \(i\) 个字符串中出现了至少 \(j\) 次。

所以有

\[f_{i,j}=\max\{|p|\mid p\text{ 在前 }i\text{ 个字符串中至少出现 }j\text{ 次}\}. \]

问题就从“枚举选择哪些字符串”变成了“统计每种前缀出现了多少次”。

假设一个长度为 \(len\) 的前缀原来出现了 \(c-1\) 次,插入当前字符串以后变成了 \(c\) 次。在此之前,它已经可以贡献给 \(f_1,f_2,\ldots,f_{c-1}\),所以这次只会新增一个可能的转移:

\[f_c\gets\max(f_c,len). \]

由此可知,每个字符对应的前缀只需要进行一次单点取最大值,总操作次数就是所有字符串的长度之和。

为了快速判断两个前缀是否相同,使用字符串哈希。

处理第 \(i\) 个字符串时,答案中新出现了 \(j=i\) 这一项。因为它的初值是 \(f_i=0\),所以先加入 \(0\oplus i=i\)。如果某次更新令 \(f_c\)\(old\) 变成 \(len\),则将总答案修改为

\[sum\gets sum-(old\oplus c)+(len\oplus c). \]

设所有字符串的长度之和为 \(S\),时间复杂度为期望 \(O(S+n)\),空间复杂度为 \(O(S+n)\)

点击查看代码
#include<bits/stdc++.h>
using namespace std;

using i32 = int;
using i64 = long long;
using i128 = __int128;
using u64 = unsigned long long;
#define all(x) (x).begin(), (x).end()
#define dbg(x) cerr << #x << " = " << (x) << endl;

int T = 1, n, m, k, ans, cnt;

const int mod1 = 1E9 + 7, base1 = 127;
const int mod2 = 1E9 + 9, base2 = 131;
vector<int> val1, val2;

void init(int n) {
    val1.resize(n + 1);
    val2.resize(n + 1);
    val1[0] = val2[0] = 1;
    for (int i = 1; i <= n; i++) {
        val1[i] = 1LL * val1[i - 1] * base1 % mod1;
        val2[i] = 1LL * val2[i - 1] * base2 % mod2;
    }
}

struct String {
    vector<int> hash1;
    vector<int> hash2;
    string s;

    String(string s_) : s(s_), hash1{1}, hash2{1} {
        for (auto it: s) {
            hash1.push_back((1LL * hash1.back() * base1 + it) % mod1);
            hash2.push_back((1LL * hash2.back() * base2 + it) % mod2);
        }
    }
    pair<int, int> get() {
        return {hash1.back(), hash2.back()};
    }
    pair<int, int> substring(int l, int r) {
        if (l > r) swap(l, r);
        int len = r - l + 1;
        int ans1 = (hash1[r + 1] - 1LL * hash1[l] * val1[len] % mod1 + mod1) % mod1;
        int ans2 = (hash2[r + 1] - 1LL * hash2[l] * val2[len] % mod2 + mod2) % mod2;
        return {ans1, ans2};
    }
    pair<int, int> modify(int idx, char x) {
        int n = s.size() - 1;
        int ans1 = (hash1.back() + 1LL * val1[n - idx] * (x - s[idx]) % mod1 + mod1) % mod1;
        int ans2 = (hash2.back() + 1LL * val2[n - idx] * (x - s[idx]) % mod2 + mod2) % mod2;
        return {ans1, ans2};
    }
};

void solve() {
    cin >> n;
    init(500000);
    vector<int> f(n + 1);
    unordered_map<u64, int> mp;
    i64 sum = 0;
    for (int i = 1; i <= n; i++) {
        string s;
        cin >> s;
        String str(s);
        sum += i;
        auto getHash = [&](int len) -> u64{
            auto [h1, h2] = str.substring(0, len - 1);
            return u64(u64(h1) << 32 | h2);
        };
        for (int j = 1; j <= (int)s.size(); j++) {
            int c = ++mp[getHash(j)];
            if (f[c] < j) {
                sum -= f[c] ^ c;
                f[c] = j;
                sum += f[c] ^ c;
            }
        }
        cout << sum << '\n';
    }
}

int main() {
    ios::sync_with_stdio(false);
    cin.tie(nullptr);
    solve();
    return 0;
}

D. Sequence

1. 题面简述

给定一个长度为 \(n\) 的 01 串,定义

\[p_i=\sum_{j=1}^{i}[s_i\ne s_j]. \]

现在只知道 \(p_1,p_2,\ldots,p_n\) 组成的多重集,要求计算有多少个 01 串能够得到这个多重集,答案对 \(998244353\) 取模。

2. 分析

首先考虑 \(p_i\) 到底记录了什么。假设扫描到当前位置时,前面已经有 \(x\)0\(y\)1

  • 当前字符填 0,那么 \(p_i=y\)
  • 当前字符填 1,那么 \(p_i=x\)

换句话说,每加入一个字符,产生的 \(p_i\) 就是另一种字符当前出现的数量。

不难发现,每一对不同字符会被统计恰好一次。假设整个字符串中有 \(a\)0\(b\)1,那么

\[\sum p_i=ab=a(n-a). \]

因此,我们可以从 \(a=0\) 开始枚举,找到第一个满足

\[a(n-a)=\sum p_i \]

\(a\)。这个 \(a\) 就是两种字符数量中较小的那个,另一个为 \(n-a\)

接下来令 \(c_i\) 表示数值 \(i\) 在多重集中还剩多少个。假设当前两种字符分别出现了 \(i\) 次和 \(now\) 次,并且 \(i\le now\)

注意到,想要继续产生数值 \(i\),只能加入当前数量较多的字符,所以剩余的 \(c_i\) 个数值必须在这里一次用完:

\[now\gets now+c_i. \]

一旦数量较少的字符从 \(i\) 增加到 \(i+1\),以后两种字符的数量都大于 \(i\),再也不可能产生数值 \(i\)

如果 \(i<a\),说明数量较少的字符还要继续增加。此时加入一次较少字符,产生的数值就是当前的 \(now\),所以需要执行 c[now]--

特殊地,如果 \(i=now\),说明两种字符数量相等。下一步既可以填 0,也可以填 1,两种选择会进入同一个无序状态,但是对应两个不同字符串,所以答案乘二。

由此可知,整个重构过程除了两种字符数量相等时有两种选择,其他时刻都是唯一的,答案一定是若干个 \(2\) 相乘得到的结果。

时间复杂度为 \(O(n)\),空间复杂度为 \(O(n)\)

点击查看代码
#include<bits/stdc++.h>
using namespace std;

using i32 = int;
using i64 = long long;
using i128 = __int128;
#define all(x) (x).begin(), (x).end()
#define dbg(x) cerr << #x << " = " << (x) << endl;

int T = 1, n, m, k, ans, cnt;

const int mod = 998244353;

void solve() {
    cin >> n;
    vector<int> c(n + 1);
    i64 sum = 0;
    for (int i = 1, x; i <= n; i++) {
        cin >> x;
        c[x]++;
        sum += x;
    }

    int a = 0;
    while (1LL * a * (n - a) != sum) a++;

    i64 res = 1;
    int now = 0;
    for (int i = 0; i < a; i++) {
        if (now == i) res = res * 2 % mod;
        now += c[i];
        c[i] = 0;
        c[now]--;
    }
    if (now == a && c[a]) res = res * 2 % mod;
    cout << res << '\n';
}

int main() {
    ios::sync_with_stdio(false);
    cin.tie(nullptr);
    solve();
    return 0;
}

A. Recall

1. 题面简述

有一个初始为空的栈,原操作包括入栈 + x、查询 ? x 和弹栈 -。现在所有弹栈操作丢失了,只留下:

  • + x:将 \(x\) 入栈;
  • T x:查询时 \(x\) 在栈中;
  • F x:查询时 \(x\) 不在栈中。

要求重新插入若干个 -,使栈中元素始终互不相同,并保证所有查询结果正确。

2. 分析

注意到,一个入栈元素只有在成功查询 T x 时才被要求继续留在栈里。

我们为每次 + x 建立一个编号,并记录最极限的存续时间:[ \(l_{id}\)\(r_{id}\) ]

如果后面没有成功查询,初始令 \(r_{id}=l_{id}\)。读到 T x 时,找到 \(x\) 最近一次入栈的编号,将右端点更新为当前位置。

然后按照操作顺序扫描所有区间。遇到 \(l_{id}\) 时把编号压入辅助栈。如果当前栈顶满足

\[r_{id}\le i, \]

说明它要求保留到的最后一次成功查询已经结束,可以立即弹出。弹出后新的栈顶可能也早已完成任务,所以继续判断,直到栈顶仍然需要留到未来。

每弹出一个区间,就令 del[i]++,最后在第 \(i\) 个原操作后输出对应数量的 -

这个过程相当于给每次入栈选择最早的合法出栈时间。如果遇到新的 + xF x 时旧的 \(x\) 仍未弹出,说明它上面压着一个未来仍需成功查询的元素,任何方案都无法在不破坏查询的情况下删除旧 \(x\),与题目保证有解矛盾。因此,合法输入中这个贪心一定成立。

期望时间复杂度为 \(O(n)\),空间复杂度为 \(O(n)\),每个入栈元素最多只会进入和离开辅助栈各一次。

点击查看代码
#include<bits/stdc++.h>
using namespace std;

using i32 = int;
using i64 = long long;
using i128 = __int128;
#define all(x) (x).begin(), (x).end()
#define dbg(x) cerr << #x << " = " << (x) << endl;

int T = 1, n, m, k, ans, cnt;

void solve() {
    cin >> n;
    vector<char> op(n + 1);
    vector<int> l(n), r(n), del(n + 1);
    unordered_map<int, int> id;
    id.reserve(2 * n);

    int tot = 0;
    for (int i = 1, x; i <= n; i++) {
        cin >> op[i] >> x;
        if (op[i] == '+') {
            l[tot] = r[tot] = i;
            id[x] = tot++;
        } else if (op[i] == 'T') {
            r[id[x]] = i;
        }
    }

    vector<int> stk;
    int p = 0;
    for (int i = 1; i <= n; i++) {
        if (p < tot && l[p] == i) stk.push_back(p++);
        while (!stk.empty() && r[stk.back()] <= i) {
            stk.pop_back();
            del[i]++;
        }
    }

    for (int i = 1; i <= n; i++) {
        cout << (op[i] == '+' ? '+' : '?');
        while (del[i]--) cout << '-';
    }
    cout << '\n';
}

int main() {
    ios::sync_with_stdio(false);
    cin.tie(nullptr);
    for(cin >> T; T--; solve());
    return 0;
}

N. Red Sequence

1. 题面简述

给定一个长度为 \(n\) 的序列,每个位置可能包含红、黄、蓝三种颜色。需要把序列划分成若干连续段。
定义 \(R\) 为该段中含有红色的位置数量,\(Y,B\) 同理。

如果一个连续段满足

\[R\ge Y,\qquad R\ge B, \]

就称它为红色段。求自由分段后所有红色段长度之和的最大值。

2. 分析

首先考虑一个朴素的划分 DP。设 \(dp_i\) 表示前 \(i\) 个位置中,能够被红色段覆盖的最大长度。对于最后一段 \((j,i]\),如果它是红色段,就有

\[dp_i=\max(dp_i,dp_j+i-j). \]

当然,也可以不覆盖位置 \(i\),令 \(dp_i\gets f_{dp-1}\)。直接枚举 \(j\) 的复杂度为 \(O(n^2)\),无法通过 \(n\le10^6\)

注意到区间需要同时满足红色数量不少于黄色和蓝色,所以定义两种前缀差:

\[x_i=\sum_{k=1}^{i}(r_k-y_k),\qquad y_i=\sum_{k=1}^{i}(r_k-b_k). \]

区间 \((j,i]\) 是红色段,当且仅当

\[x_j\le x_i,\qquad y_j\le y_i. \]

原来的区间判断就变成了二维偏序。

为了让维护的值更简单,定义

\[g_i=i-dp_i, \]

表示前 \(i\) 个位置中最少有多少个位置没有被红色段覆盖。显然有

\[g_i=\min\left(g_{i-1}+1, \min_{j<i,\ x_j\le x_i,\ y_j\le y_i}g_j\right). \]

所以我们只需要维护已经出现的点 \((x_j,y_j)\),查询左下矩形中的最小 \(g_j\)

这里使用树状数组套树状数组。外层 BIT 维护 \(x\),每个外层节点再放一个维护 \(y\) 的 BIT。所有前缀点可以提前得到,所以先离线收集每个外层节点中会出现的 \(y\),排序去重后建立内层树状数组。

内层直接复用普通查询前缀最小值 BIT 的写法。单次修改和查询都会经过两层 BIT,复杂度为 \(O(\log^2 n)\)

还有一个小 Trick:如果二维查询得到的 best 已经成为当前的 \(g_i\),说明存在一个更早的点在两个坐标上都不大于当前点,并且代价不大于当前点。以后当前点能转移到的位置,那个更早的点也一定能够转移到,所以当前状态没有必要再次插入。只有 g[i-1]+1 严格更小时才插入当前点。

最终答案为

\[n-g_n. \]

时间复杂度为 \(O(n\log^2 n)\),空间复杂度为 \(O(n\log n)\)。由于使用的是 BIT,常数非常小,QOJ跑了800ms

点击查看代码
#include<bits/stdc++.h>
using namespace std;

using i32 = int;
using i64 = long long;
using i128 = __int128;
#define all(x) (x).begin(), (x).end()
#define dbg(x) cerr << #x << " = " << (x) << endl;

int T = 1, n, m, k, ans, cnt;

struct BIT {
    const int inf = 1e9;
    vector<int> w;
    int n;

    BIT(int n = 0) : n(n), w(n + 1, inf) {}
    void init(int n_) {
        n = n_;
        w.assign(n + 1, inf);
    }
    void add(int x, int v) {
        for (; x <= n; x += x & -x) {
            w[x] = min(w[x], v);
        }
    }
    int ask(int x) {
        int ans = inf;
        for (; x; x -= x & -x) {
            ans = min(ans, w[x]);
        }
        return ans;
    }
};

struct BIT2 {
    const int inf = 1e9;
    int n;
    vector<vector<int>> ys;
    vector<BIT> w;

    BIT2(int n, const vector<int> &x, const vector<int> &y) : n(n), ys(n + 1), w(n + 1) {
        for (int j = 0; j < (int)x.size(); j++) {
            for (int i = x[j]; i <= n; i += i & -i) {
                ys[i].push_back(y[j]);
            }
        }
        for (int i = 1; i <= n; i++) {
            sort(all(ys[i]));
            ys[i].erase(unique(all(ys[i])), ys[i].end());
            w[i].init(ys[i].size());
        }
    }

    void add(int x, int y, int v) {
        for (int i = x; i <= n; i += i & -i) {
            int p = lower_bound(all(ys[i]), y) - ys[i].begin() + 1;
            w[i].add(p, v);
        }
    }

    int ask(int x, int y) {
        int res = inf;
        for (int i = x; i; i -= i & -i) {
            int p = upper_bound(all(ys[i]), y) - ys[i].begin();
            res = min(res, w[i].ask(p));
        }
        return res;
    }
};

void solve() {
    cin >> n;
    vector<int> x(n + 1), y(n + 1);
    int mn = 0, mx = 0;
    for (int i = 1; i <= n; i++) {
        int r, a, b;
        cin >> r >> a >> b;
        x[i] = x[i - 1] + r - a;
        y[i] = y[i - 1] + r - b;
        mn = min(mn, x[i]);
        mx = max(mx, x[i]);
    }
    for (auto &v: x) v -= mn - 1;

    BIT2 bit(mx - mn + 1, x, y);
    bit.add(x[0], y[0], 0);
    int cost = 0;
    for (int i = 1; i <= n; i++) {
        int best = bit.ask(x[i], y[i]);
        int nxt = min(cost + 1, best);
        if (nxt < best) bit.add(x[i], y[i], nxt);
        cost = nxt;
    }
    cout << n - cost << '\n';
}

int main() {
    ios::sync_with_stdio(false);
    cin.tie(nullptr);
    for(; T--; solve());
    return 0;
}

G. Toll Gates on a Tight Schedule

1. 题面简述

给定一张有 \(n\) 个点、\(m\) 条有向边的图,每经过一条边需要一分钟,每个点 \(i\) 有一个收费 \(c_i\)

现在需要从点 \(1\) 到达点 \(n\),只考虑经过边数最少的路线。求这些最短路线中,经过点的费用之和不超过 \(C\) 的路线数量。起点和终点的费用也需要计算,答案对 \(2^{64}\) 取模。

2. 分析

2.1 最短路 DAG

注意到每条边的长度都是 \(1\),因此可以分别从点 \(1\) 和点 \(n\) 在反图上进行 BFS,得到:

  • \(d_1[u]\):从点 \(1\) 到点 \(u\) 的最短距离;
  • \(d_n[u]\):从点 \(u\) 到点 \(n\) 的最短距离。

设从 \(1\)\(n\) 的最短距离为 \(D\)。一条边 \(u\to v\) 能出现在某条最短路中,当且仅当

\[d_1[u]+1+d_n[v]=D. \]

只保留这些边。沿着保留下来的每条边,\(d_1\) 都恰好增加 \(1\),所以新图是一个按照 \(d_1\) 分层的 DAG。每条最短路线都会依次在每一层选择一个点。

2.2 折半枚举

费用 \(C\) 很大,不能把费用作为背包维度;最短路径的数量也可能非常多,不能完整枚举。

注意到 \(n\le 90\),适合使用折半搜索。这里不在某个点处拼接,而是在相邻两层之间选择一条“分界线”:

  • 从点 \(1\) 出发,枚举到达分界线左侧各点的所有前半段路径费用;
  • 从点 \(n\) 沿反图出发,枚举到达分界线右侧各点的所有后半段路径费用。

为什么要把分界线放在两层之间,而不是直接选一层作为中点?如果相邻两层之间的边很多,路径数量可能在跨过这一层时突然膨胀。在边上拼接可以停在膨胀发生之前,避免一侧产生大量不必要的半路径。

为了让两边的枚举量尽量均衡,可以先在最短路 DAG 上计算到达每个点的路径数量,以及每个点到终点的路径数量。枚举所有相邻层,根据生成半路径、排序以及跨边配对的次数估算总工作量,选择估算工作量最小的分界线。相比只按路径条数选择中间层,这样可以避开跨层边很多、配对开销很大的位置。

所有点费均非负。如果一条半路径的费用已经大于 \(C\),继续延伸也不可能合法,可以立刻剪枝。

2.3 合并答案

假设分界线位于第 \(k\) 层和第 \(k+1\) 层之间。对于一条跨越分界线的边 \(x\to y\),设:

  • left[x] 保存 \(1\to x\) 的前半段费用;
  • right[y] 保存 \(y\to n\) 的后半段费用。

两段路径没有重复计算任何点,因此完整费用直接为

\[L+R. \]

合法条件为

\[L+R\le C, \]

也就是

\[R\le C-L. \]

将分界线两边的费用数组排序。对于每条跨层边,有两种合并方法:

  • 两个数组大小相差较大时,枚举较小的数组,在较大的数组中使用 upper_bound
  • 两个数组大小接近时,直接使用双指针线性统计。

根据两个数组的大小选择估算操作次数更少的一种。每条最短路线只会经过一条分界线上的边,所以不会重复统计。这个题目非常的坑,如果不加这个优化会TLE,我吃了一发之后才改成了这种写法,如果是赛时,PTA 的机子会更慢,远不如 QOJ 的评测机,所以这个代码仅作参考,可能还要加优化。

假设所有中间点中较少一侧的路径总数为 \(P_{\min}\),较多一侧的路径总数为 \(P_{\max}\),则时间复杂度可以写为

\[O(n+m+P_{\min}\log P_{\min}+P_{\max}\log P_{\min}), \]

点击查看代码
#include<bits/stdc++.h>
using namespace std;

using i32 = int;
using i64 = long long;
using i128 = __int128;
using u64 = unsigned long long;
#define all(x) (x).begin(), (x).end()
#define dbg(x) cerr << #x << " = " << (x) << endl;

int T = 1, n, m, k, ans, cnt;

void solve() {
    i64 C;
    cin >> n >> m >> C;
    vector<i64> c(n + 1);
    for (int i = 1; i <= n; i++) cin >> c[i];
    vector<vector<int>> g(n + 1), rg(n + 1);
    vector<pair<int, int>> e(m);
    for (auto &[u, v]: e) {
        cin >> u >> v;
        g[u].push_back(v);
        rg[v].push_back(u);
    }

    auto bfs = [&](int s, const vector<vector<int>> &G) {
        vector<int> dis(n + 1, -1);
        queue<int> q;
        dis[s] = 0;
        q.push(s);
        while (!q.empty()) {
            int x = q.front();
            q.pop();
            for (auto y: G[x]) {
                if (dis[y] == -1) {
                    dis[y] = dis[x] + 1;
                    q.push(y);
                }
            }
        }
        return dis;
    };

    auto d1 = bfs(1, g);
    auto dn = bfs(n, rg);
    if (d1[n] == -1) {
        cout << 0 << '\n';
        return;
    }

    int D = d1[n];
    if (D == 0) {
        cout << u64(c[1] <= C) << '\n';
        return;
    }
    vector<vector<int>> dag(n + 1), rdag(n + 1), layer(D + 1);
    for (auto [u, v]: e) {
        if (d1[u] != -1 && dn[v] != -1 && d1[u] + 1 + dn[v] == D) {
            dag[u].push_back(v);
            rdag[v].push_back(u);
        }
    }
    for (int i = 1; i <= n; i++) {
        if (d1[i] != -1 && dn[i] != -1 && d1[i] + dn[i] == D) {
            layer[d1[i]].push_back(i);
        }
    }

    const u64 inf = numeric_limits<u64>::max();
    auto add = [&](u64 x, u64 y) {
        return inf - x < y ? inf : x + y;
    };
    vector<u64> pre(n + 1), suf(n + 1);
    pre[1] = suf[n] = 1;
    for (int i = 0; i < D; i++) {
        for (auto x: layer[i]) {
            for (auto y: dag[x]) pre[y] = add(pre[y], pre[x]);
        }
    }
    for (int i = D; i > 0; i--) {
        for (auto x: layer[i]) {
            for (auto y: rdag[x]) suf[y] = add(suf[y], suf[x]);
        }
    }

    int mid = 0;
    i128 best = -1;
    auto lg = [&](u64 x) {
        return 64 - __builtin_clzll(x);
    };
    for (int i = 0; i < D; i++) {
        u64 x = 0, y = 0;
        for (auto v: layer[i]) {
            x = add(x, pre[v]);
        }
        for (auto v: layer[i + 1]) y = add(y, suf[v]);
        i128 work = i128(x) + y;
        for (auto v: layer[i]) work += i128(pre[v]) * lg(pre[v]);
        for (auto v: layer[i + 1]) work += i128(suf[v]) * lg(suf[v]);
        for (auto u: layer[i]) {
            for (auto v: dag[u]) {
                u64 a = pre[u], b = suf[v];
                u64 small = min(a, b), large = max(a, b);
                work += min(i128(small) * lg(large), i128(a) + b);
            }
        }
        if (best == -1 || work < best) {
            best = work;
            mid = i;
        }
    }

    vector<vector<i64>> left(n + 1), right(n + 1);
    auto dfs1 = [&](auto &&self, int x, i64 sum) -> void {
        if (sum > C) return;
        if (d1[x] == mid) {
            left[x].push_back(sum);
            return;
        }
        for (auto y: dag[x]) self(self, y, sum + c[y]);
    };
    auto dfs2 = [&](auto &&self, int x, i64 sum) -> void {
        if (sum > C) return;
        if (d1[x] == mid + 1) {
            right[x].push_back(sum);
            return;
        }
        for (auto y: rdag[x]) self(self, y, sum + c[y]);
    };
    dfs1(dfs1, 1, c[1]);
    dfs2(dfs2, n, c[n]);

    u64 res = 0;
    for (auto x: layer[mid]) sort(all(left[x]));
    for (auto x: layer[mid + 1]) sort(all(right[x]));
    for (auto x: layer[mid]) {
        for (auto y: dag[x]) {
            auto &a = left[x], &b = right[y];
            if (a.empty() || b.empty()) continue;
            auto *small = &a, *large = &b;
            if (small->size() > large->size()) swap(small, large);
            int log = 64 - __builtin_clzll(large->size());
            if (small->size() * log <= small->size() + large->size()) {
                for (auto sum: *small) {
                    res += upper_bound(all(*large), C - sum) - large->begin();
                }
            } else {
                size_t p = large->size();
                for (auto sum: *small) {
                    while (p && large->at(p - 1) > C - sum) p--;
                    res += p;
                }
            }
        }
    }
    cout << res << '\n';
}

int main() {
    ios::sync_with_stdio(false);
    cin.tie(nullptr);
    solve();
    return 0;
}
posted @ 2026-09-08 18:05  薄荷味小哀  阅读(195)  评论(0)    收藏  举报