【题解】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]\),以及区间内所有下标组成的一个排列
它表示
要求构造一个满足全部限制,并且逆序对数量最少的排列。如果不存在合法排列,输出 -1。
2. 分析
首先只考虑怎样满足大小关系。对于一条限制,只需要按照给出的顺序连边:
如果最后得到的图中存在环,说明某些位置互相要求比对方小,不可能构造出合法排列,直接输出 -1。
如果图中没有环,我们求出一个拓扑序
然后令
就可以满足所有边对应的大小关系。
接下来考虑怎样让逆序对最少。对于两个原下标 \(i<j\),如果在拓扑序中 \(j\) 出现在 \(i\) 前面,那么最终就有 \(p_i>p_j\),产生一个逆序对。所以我们希望拓扑序尽量按照原下标从小到大排列,由此想到使用小根堆维护所有入度为零的点,每次取编号最小的点。
正确性证明
对于一般的有向无环图,字典序最小拓扑序不一定拥有最少逆序对。本题能够这样做,是因为每条限制会将一个完整连续区间中的所有位置全部排好顺序。
考虑三个下标 \(i<j<k\)。如果限制能够推出 \(k\) 必须在 \(i\) 前面,那么从 \(k\) 到 \(i\) 的路径一定会经过一条跨过位置 \(j\) 的边。这条边来自某个完整区间,而 \(j\) 也在这个区间中,所以该区间一定同时确定了 \(j\) 与这条边两端的顺序。由此可知,必然满足下面两种情况中的至少一种:
现在假设小根堆得到的拓扑序中出现了一个逆序对 \(i<j\),即 \(j\) 比 \(i\) 更早出队。当 \(j\) 出队时,\(i\) 还不是零入度点。沿着 \(i\) 的前驱一直向前,可以找到剩余图中的一个零入度点 \(k\),并且 \(k\) 能够到达 \(i\)。
小根堆选择了 \(j\) 而没有选择 \(k\),说明 \(k>j\)。此时有
并且 \(k\to i\)。根据上面的区间性质,要么 \(k\to j\),要么 \(j\to i\)。第一种情况与 \(j\) 已经是零入度点矛盾,因此只能是
也就是说,小根堆拓扑序中产生的每一个逆序对,都是题目限制强制要求的。任何合法排列都无法消除这些逆序对,所以该拓扑序的逆序对数量一定最少。
每条长度为 \(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\),要求计算
2. 分析
首先考虑怎样描述 \(f_{i,j}\)。如果一个前缀 \(p\) 在前 \(i\) 个字符串中出现了至少 \(j\) 次,我们就可以从这些字符串中任选 \(j\) 个,它们的最长公共前缀长度至少为 \(|p|\)。
反过来,如果选出的 \(j\) 个字符串有长度为 \(len\) 的公共前缀,那么这个前缀一定在前 \(i\) 个字符串中出现了至少 \(j\) 次。
所以有
问题就从“枚举选择哪些字符串”变成了“统计每种前缀出现了多少次”。
假设一个长度为 \(len\) 的前缀原来出现了 \(c-1\) 次,插入当前字符串以后变成了 \(c\) 次。在此之前,它已经可以贡献给 \(f_1,f_2,\ldots,f_{c-1}\),所以这次只会新增一个可能的转移:
由此可知,每个字符对应的前缀只需要进行一次单点取最大值,总操作次数就是所有字符串的长度之和。
为了快速判断两个前缀是否相同,使用字符串哈希。
处理第 \(i\) 个字符串时,答案中新出现了 \(j=i\) 这一项。因为它的初值是 \(f_i=0\),所以先加入 \(0\oplus i=i\)。如果某次更新令 \(f_c\) 从 \(old\) 变成 \(len\),则将总答案修改为
设所有字符串的长度之和为 \(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_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,那么
因此,我们可以从 \(a=0\) 开始枚举,找到第一个满足
的 \(a\)。这个 \(a\) 就是两种字符数量中较小的那个,另一个为 \(n-a\)。
接下来令 \(c_i\) 表示数值 \(i\) 在多重集中还剩多少个。假设当前两种字符分别出现了 \(i\) 次和 \(now\) 次,并且 \(i\le now\)。
注意到,想要继续产生数值 \(i\),只能加入当前数量较多的字符,所以剩余的 \(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}\) 时把编号压入辅助栈。如果当前栈顶满足
说明它要求保留到的最后一次成功查询已经结束,可以立即弹出。弹出后新的栈顶可能也早已完成任务,所以继续判断,直到栈顶仍然需要留到未来。
每弹出一个区间,就令 del[i]++,最后在第 \(i\) 个原操作后输出对应数量的 -。
这个过程相当于给每次入栈选择最早的合法出栈时间。如果遇到新的 + x 或 F 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\) 同理。
如果一个连续段满足
就称它为红色段。求自由分段后所有红色段长度之和的最大值。
2. 分析
首先考虑一个朴素的划分 DP。设 \(dp_i\) 表示前 \(i\) 个位置中,能够被红色段覆盖的最大长度。对于最后一段 \((j,i]\),如果它是红色段,就有
当然,也可以不覆盖位置 \(i\),令 \(dp_i\gets f_{dp-1}\)。直接枚举 \(j\) 的复杂度为 \(O(n^2)\),无法通过 \(n\le10^6\)。
注意到区间需要同时满足红色数量不少于黄色和蓝色,所以定义两种前缀差:
区间 \((j,i]\) 是红色段,当且仅当
原来的区间判断就变成了二维偏序。
为了让维护的值更简单,定义
表示前 \(i\) 个位置中最少有多少个位置没有被红色段覆盖。显然有
所以我们只需要维护已经出现的点 \((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 严格更小时才插入当前点。
最终答案为
时间复杂度为 \(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\) 都恰好增加 \(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\) 的后半段费用。
两段路径没有重复计算任何点,因此完整费用直接为
合法条件为
也就是
将分界线两边的费用数组排序。对于每条跨层边,有两种合并方法:
- 两个数组大小相差较大时,枚举较小的数组,在较大的数组中使用
upper_bound; - 两个数组大小接近时,直接使用双指针线性统计。
根据两个数组的大小选择估算操作次数更少的一种。每条最短路线只会经过一条分界线上的边,所以不会重复统计。这个题目非常的坑,如果不加这个优化会TLE,我吃了一发之后才改成了这种写法,如果是赛时,PTA 的机子会更慢,远不如 QOJ 的评测机,所以这个代码仅作参考,可能还要加优化。
假设所有中间点中较少一侧的路径总数为 \(P_{\min}\),较多一侧的路径总数为 \(P_{\max}\),则时间复杂度可以写为
点击查看代码
#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;
}

浙公网安备 33010602011771号