Down Below
因为看到题解区全是二分的 \(nm\log V\) 做法, 感觉不太优秀,所以就写了这篇题解。
观察题目,很容易联想到最短路,走到某个点的 \(val\) 就相当于路径长度,而由于有 \(a_i\) 的限制,所以我们要尽量让 \(val\) 大,也就相当于要求最长路径,而 \(b_i\) 就相当于是路径长度。
先考虑转化问题,最优问题通过二分转化成可行性问题。接着按照刚才的思路,运用类似于最短路的方法去处理询问。这里涉及到一个选择算法的问题,但由于求的是最长路径,首先排除 dijkstra ,接着排除肯定超时的 Floyd ,那结果显而易见是只能是 SPFA 。具体为什么不是 Bellman–Ford 我们后面会说明。
但我们进行最短路的时候会发现,\(b_i\) 只能算一次的性质让我们这个算法显得没那么正确了, 因为如果我们走了一个环, 那么我们就可以一直循环,再走出去。我们再回到题目,环其实是一个很特殊的东西,如果从 \(1\) 能走到一个环,那么我们可以把这个环及其到 \(1\) 的路径一起合并成一个新的 \(1\) ,原因就在于本来我们不能走回头路,但是有了环以后我们就可以利用环掉头。又因为每个点的度都是大于 \(2\) 的,那么每个点就一定至少在一个环上或者在一条环到 \(1\) 的路径上, 否则一定存在度数为 \(1\) 的点。所以我们的目标就从求最长路变成了求正环,这个问题似乎正是 SPFA 擅长的。
当我们开始用 SPFA 去求解正环的时候却发现,并不是每个正环都是可行的,有时候我们会因为重复跑环而跑到一些本来到不了的点,从而合并到了本来到不了的环,并且 SPFA 跑环的时间也并不正确,每次时间复杂度 \(O(nk)\) ,最多跑 \(m\) 次。我们需要发现些性质才能解决这个问题。由于我们一条边可以走的条件是 \(val > a_i\) ,那么我们考虑如果一个点在 SPFA 的过程中被更新了两次,那么设这个点为 \(u\) ,两次尝试更新它的点分别是 \(v, w\) (假设这个点是第一个被尝试更新两次的点,那么不可能从同一个人尝试更新两次, 否则更新它的那个点一定被更新过至少两次),假设 \(val_{w} \ge val_{v}\) ,那么有 \(val_{u} = b_{u} + val_{w} > val_{w} \ge val_{v} > a_{v}\) 即 \(val_{u} > a_{v}\) ,所以 \(u\) 可以从 \(w\) 走过来并且再从 \(v\) 走回点 \(1\) , 所以 \(u\) 在一个环上, 那么我们直接记录从 \(1\) 走到 \(v, w\) 的路径即可, 由于这是第一个环,所以 \(v,w\) 都有唯一的从 \(1\) 来的路径。
因为我们每次合并至少会减少一个点,那么我们可以在每次合并后重新做 SPFA ,就可以了。这里解释一下刚才没有说的问题, 为什么要用 SPFA 而不用 Bellman–Ford 。可以发现在 SPFA 中一个点如果第二次进入队列那么就一定至少被尝试更新两次,那么就已经找到环了。这样来看的话 SPFA 的总时间应该是 \(m\) 的,而 Bellman–Ford 每次会用 \(m\) 的时间进行一个点的更新,那么对于类似于树的图的时候,他找到环的时间复杂度会退化成 \(nm\) 。
至此我们已经得到了一个 \(\mathrm{O}(nm\log V)\) 的能过的算法,而且这个算法如果打的漂亮也可以很快,但是很明显我们的目标并不是这样的一个算法,因为我们可以发现似乎二分的过程并不是必须的,我们明明知道要扩展更多需要使答案增加多少,我们二分却完全没有用到这个性质。
考虑我们最开始假如 \(1\) 的 \(val\) 为 \(0\) 那么我们一定无法继续进行寻找最长路径和缩环,那么假设 \(u\) 是一个我们能到达的点,\(v\) 为一个 \(u\) 的邻点且满足 \(v\) 不能到达,则我们要能使更多的点被到达至少要将答案增加的值就是 \(\min\{a_v - val_u\}\) 。所以只要我们没能将所有点都合成一个点,我们就尝试缩环或者无法缩环的时候选择增加答案,然后再重新做 SPFA ,那么缩点只会缩 \(n\) 次,更新答案只会因为一条边更新一次,总共就只会最多更新 \(m\) 次。所以最终也是有了一个 \(\mathrm{O}(m^2)\) 的算法了,虽然因为每次更新其实增加的能扩展的边可以直接加入队列中,所以可以优化到 \(\mathrm{O}(nm)\) ,但是因为懒就没有打了( \(\mathrm{O}(m^2)\) 已经可以将本来跑 200ms 多的快到最劣解的算法优化成 2023.10.19 之前的最优解我觉得已经足够了,而且再优化因为常数的原因可能时间并不会有太大差别)。
code:
#include <cstdio>
#include <algorithm>
#include <stack>
#include <vector>
using namespace std;
#define MAXN 1000
#define MAXM 2000
#define INF 0x3f3f3f3f
vector<pair<int, int> > s[MAXN + 5];
long long dp[MAXN + 5];
long long vis[MAXN + 5];
pair<int, int> sed[MAXM + 5];
int Fa[MAXN + 5];
long long sa[MAXN + 5], sb[MAXN + 5];
int main () {
int t;
scanf ("%d", &t);
while (t --) {
int n, m;
scanf ("%d %d", &n, &m);
for (int i = 2; i <= n; i ++) {
scanf ("%lld", &sa[i]);
}
for (int i = 2; i <= n; i ++) {
scanf ("%lld", &sb[i]);
}
for (int i = 1; i <= n; i ++) {
s[i].clear();
}
for (int i = 1; i <= m; i ++) {
scanf ("%d %d", &sed[i].first, &sed[i].second);
}
long long ans = 0;
int num = n - 1;
for (int i = 1; i <= n; i ++) {
vis[i] = 0;
}
while (num) {
long long smin = INF;
int Last = n;
while (num && num != Last){
Last = num;
for (int i = 1; i <= n; i ++) {
dp[i] = 0;
s[i].clear();
Fa[i] = 0;
}
dp[1] = ans;
for (int i = 1; i <= m; i ++) {
int u = sed[i].first, v = sed[i].second;
if (vis[u]) {
u = 1;
}
if (vis[v]) {
v = 1;
}
if (u != v) {
s[u].push_back (make_pair(v, i));
s[v].push_back (make_pair (u, i));
}
}
for (int i = 2; i <= n; i ++) {
if (vis[i]) {
dp[1] += sb[i];
}
}
stack<pair<int, int> > S;
S.push(make_pair (1, 0));
vis[1] = 1;
while (!S.empty()) {
pair<int, int> p = S.top();
S.pop();
for (auto i = s[p.first].begin(); i != s[p.first].end(); i ++) {
if (i->second == p.second) {
continue;
}
else if (Fa[i->first] || i->first == 1) {
for (int j = i->first; j != 1; j = Fa[j]) {
if (!vis[j]) {
vis[j] = 1;
num --;
}
else {
break;
}
}
for (int j = p.first; j != 1; j = Fa[j]) {
if (!vis[j]) {
vis[j] = 1;
num --;
}
else {
break;
}
}
}
else if (dp[p.first] > sa[i->first]) {
dp[i->first] = sb[i->first] + dp[p.first];
Fa[i->first] = p.first;
S.push(*i);
}
else {
smin = min (smin, sa[i->first] + 1 - dp[p.first]);
}
}
}
}
if (num) {
ans += smin;
}
}
printf ("%lld\n", ans);
}
}