## 日记和最短路
首先一条边的边权字符串长度不一致很难搞,因为拼起来之后会错位,导致我们无法选择字典序最小的。因此可以将不同长度的字符串拆成多个字符,然后建虚点,这样就可以保证每条边i上边权都是一个字符。
现在对于第二问已经很好解决了。分层跑 BFS,每次让所有节点一起往后走一条边,但是必须满足走的边是所有可走的边当中字典序最小的那个。
这样我们每一步都走的字典序最小的,也就满足我们的任意一条路径任意前缀最小,也就有了答案最小。还原答案就是记录转移的上一个节点然后反向还原即可。
- [!] 需要注意的一条边只有在终点可以到达 $n$ 时才是有效的,其余直接忽略。我们是在所有有效的边当中选取字典序最小的若干条。
- [n] 所以我们需要需要建反向边,从 $n$ 开始跑 DFS 或 BFS 找到所有可以到 $n$ 的节点。
- [c] 赛时我建反向边写错了,但是样例全过,最终直接炸缸。
然后是第一问,其实大差不差。同样是在反向图上先跑一遍 BFS 求出任意节点到 $n$ 的最短路。然后同样套用上面的板子,但是第一比较关键字改成到 $n$ 的距离,其次才是字典序。这样我们就成功完成了两个询问。
最后就是漫长的卡常过程。
1. 开 `long long` 会 MLE,直接开 `int`;
2. 数组开 $3\times 10^6$ 会 MLE,卡在上界 $2.1\times 10^6$ 刚刚好;
3. 最后的分层 BFS 同一个点会在不同层里面访问多次,但是我们不能剪掉后来的因为后来的有可能字典序更小。所以我们只需要处理同层的,每一层每个点只能访问一次,这样子就可以保证入队次数不太多,剩下的就是玄学了。
时间复杂度 $\mathcal O(能过)$。赛后 AC 代码。
```cpp
#include <iostream>
#include <algorithm>
#include <utility>
#include <vector>
#include <string>
#include <queue>
#include <set>
using namespace std;
const int kMaxN = 3e6 + 5;
int f[kMaxN], d[kMaxN], vis[kMaxN], n, m, cnt;
string w, ans;
vector<pair<int, char>> e[kMaxN];
vector<int> q, nxt;
vector<int> g[kMaxN];
pair<int, char> p[kMaxN];
void cover(int x) {
if (f[x]) {
return;
}
f[x] = 1;
for (int i : g[x]) {
cover(i);
}
}
void bfs(int x) {
queue<int> q;
fill(d + 1, d + cnt + 1, 1e9);
for (q.push(x), d[x] = 0; q.size(); q.pop()) {
int t = q.front();
for (int i : g[t]) {
if (d[t] + 1 < d[i]) {
d[i] = d[t] + 1;
q.push(i);
}
}
}
}
int main() {
cin.tie(0)->sync_with_stdio(0);
cin >> n >> m;
cnt = n;
for (int i = 1, u, v; i <= m; i++) {
cin >> u >> v >> w;
if (w.size() == 1) {
e[u].emplace_back(v, w[0]);
g[v].emplace_back(u);
continue;
}
e[u].emplace_back(++cnt, w.front());
g[cnt].emplace_back(u);
for (int i = 1; i < w.size() - 1; i++) {
e[cnt].emplace_back(cnt + 1, w[i]);
g[cnt + 1].emplace_back(cnt);
cnt++;
}
e[cnt].emplace_back(v, w.back());
g[v].emplace_back(cnt);
}
cover(n);
bfs(n);
for (q.emplace_back(1); q.size(); ) {
nxt.clear();
int dis = 1e9;
char c = 'z';
for (int i : q) {
for (auto j : e[i]) {
if (d[j.first] < dis) {
dis = d[j.first];
c = j.second;
} else if (d[j.first] == dis) {
c = min(c, j.second);
}
}
}
for (int i : q) {
for (auto j : e[i]) {
if (d[j.first] == dis && j.second == c && !vis[j.first]) {
nxt.emplace_back(j.first);
p[j.first] = {i, j.second};
vis[j.first] = 1;
}
}
}
for (int i : nxt) {
vis[i] = 0;
}
if (p[n].first) {
break;
}
swap(q, nxt);
}
for (int x = n; x != 1; x = p[x].first) {
ans += p[x].second;
}
reverse(ans.begin(), ans.end());
cout << ans << ' ';
ans.clear();
q.clear();
fill(p + 1, p + cnt + 1, pair<int, char>(0, 0));
for (q.emplace_back(1); q.size(); ) {
nxt.clear();
char c = 'z';
for (int i : q) {
for (auto j : e[i]) {
if (f[j.first] && j.second < c) {
c = j.second;
}
}
}
for (int i : q) {
for (auto j : e[i]) {
if (f[j.first] && j.second == c && !vis[j.first]) {
nxt.emplace_back(j.first);
p[j.first] = {i, j.second};
vis[j.first] = 1;
}
}
}
for (int i : nxt) {
vis[i] = 0;
}
if (p[n].first) {
break;
}
swap(q, nxt);
}
for (int x = n; x != 1; x = p[x].first) {
ans += p[x].second;
}
reverse(ans.begin(), ans.end());
cout << ans << '\n';
return 0;
}
```
## 日记和欧拉函数
首先我一眼就看见了 $\le 10^6$,因此我先使用线性筛 $\mathcal O(n)$ 求出所有欧拉函数的值。然后就是求 $\varphi^x(i)$。这个有点类似于 LCA 的倍增上跳,因此我们直接 $\mathcal O(n\log_2 n)$ 预处理倍增加 $\mathcal O(\log_2 n)$ 单次查询即可。(不过赛后发现暴力跳最多跳 $2\left\lceil\log_2 x\right\rceil$)
于是我们就可以预处理出 $S(n)=\sum\limits_{i=1}^n \varphi^{\left(\max\limits_{j=1}^i \varphi(j)-B\right)}(i)$,最后 $S(r)-S(l)$ 就是答案了。
- [p] 最后再加上 $B=10^9$ 的 $f(i)=i$ 和 $B=0$ 的 $f(i)=1$,成功获得 $80$ 分。
现在就是进阶操作了。首先这个求和式子及其的不规则,所以是很难搞出来的。所以需要进行找规律。对于 $i\le B$,由于 $\varphi(i)\le i$,所以有 $\varphi(i)\le B$,也就是 $\max\limits_{j=1}^i \varphi(j)-B\le 0$,所以对于 $i\le B$ 有 $f(i)=i$。
然后打标观察,在 $B$ 往后大约 $400$ 个数都是无规律的,但是 $400$ 个之后就都变成 $1$ 了。证明不太会,大概就是大于一个值了之后 $\max\limits_{j=1}^i \varphi(j)-B$ 就会大于等于 $60$,此时就一定会跳成 $1$。
可以找一个大于 ${} B+60 {}$ 的质数 $p$,那么有 $\varphi(p)=p-1$ 也就有 $\varphi(p)-B\ge 60$ 即 $\max\limits_{j=1}^p \varphi(j)-B\ge 60$,往后这个数一定会更大所以最终都会变成 $1$。而设置 $400$ 的原因是这个数大小刚刚好,其实也可以直接暴力找大于 $B+60$ 的第一个质数作为分界点。
而对于 $B\le i\le B+400$,直接暴力求解欧拉函数即可。
```cpp
#include <iostream>
using namespace std;
using LL = long long;
LL sum[401], t, b;
bool isPrime(LL x) {
if (x <= 1) {
return 0;
}
for (LL i = 2; i * i <= x; i++) {
if (x % i == 0) {
return 0;
}
}
return 1;
}
LL max(LL x) {
for (LL i = x; i; i--) {
if (isPrime(i)) {
return i;
}
}
return 2;
}
LL phi(LL x) {
if (x == 1) {
return 1;
}
LL ans = x;
for (LL i = 2; i * i <= x; i++) {
if (x % i == 0) {
ans = ans / i * (i - 1);
for (; x % i == 0; x /= i) { }
}
}
if (x > 1) {
ans = ans / x * (x - 1);
}
return ans;
}
LL calc(LL x, LL y) {
if (y <= 0) {
return x;
}
while (y--) {
if (x == 1) {
break;
}
x = phi(x);
}
return x;
}
LL solve(LL x) {
if (x <= b) {
return x * (x + 1) / 2;
} else {
return sum[min(400LL, x - b)] + (x > b + 400) * (x - b - 400);
}
}
int main() {
cin.tie(0)->sync_with_stdio(0);
cin >> t >> b;
sum[0] = b * (b + 1) / 2;
for (LL i = 1; i <= 400; i++) {
sum[i] = sum[i - 1] + calc(b + i, max(b + i) - b - 1);
}
for (LL l, r; t; t--) {
cin >> l >> r;
cout << solve(r) - solve(l - 1) << '\n';
}
return 0;
}
```
## 日记和二叉搜索树
首先枚举任意一个数作为 LCA,那么所有的 $(u,v)$ 一定满足 $u$ 和 $v$ 在 LCA 的不同儿子的子树里。然后稍微推一下式子就可以得到 LCA 的值一定是介于两个子树的值域之间的,因为在子树值域中间不忧(可以推式子,最后会变成一个一次函数,不要像我一样考场上推错了)。
而子树大小之和是给定的,把子树大小分成两部分最大化乘积,可以做背包,找到离和 $/2$ 最近的方案。具体实现需要卡常。
```cpp
#include <algorithm>
#include <bitset>
#include <iostream>
#include <vector>
using namespace std;
const int kMaxN = 1e6 + 5;
int siz[kMaxN], fa[kMaxN], n;
long long ans;
vector<int> e[kMaxN], v, t;
void dfs(int x, int f) {
siz[x] = 1;
fa[x] = f;
for (int i : e[x]) {
if (i != f) {
dfs(i, x);
siz[x] += siz[i];
}
}
}
template <size_t mx>
void solve(const vector<int> &v, int sum) {
if (mx < size_t(sum / 2 + 1)) {
solve<min(2 * mx, (size_t)5e5)>(v, sum);
return;
}
bitset<mx + 5> dp;
dp.set(mx);
for (int i : v) {
dp |= dp >> i;
}
int p = dp._Find_next(mx - sum / 2 - 1);
ans += 1LL * (mx - p) * (sum - (mx - p));
}
int main() {
cin.tie(0)->sync_with_stdio(0);
cin >> n;
for (int i = 1, u, v; i < n; i++) {
cin >> u >> v;
e[u].emplace_back(v);
e[v].emplace_back(u);
}
dfs(1, 0);
for (int i = 1; i <= n; i++) {
int sum = 0, mx = 0;
v.clear(), t.clear();
for (int j : e[i]) {
if (j != fa[i]) {
v.emplace_back(siz[j]);
mx = max(mx, siz[j]);
sum += siz[j];
}
}
if (v.size() <= 1) {
continue;
}
if (v.size() == 2) {
ans += 1LL * v[0] * v[1];
continue;
}
if (v.size() == 3) {
ans += max(1LL * v[0] * (v[1] + v[2]), max(1LL * v[1] * (v[0] + v[2]), 1LL * v[2] * (v[0] + v[1])));
continue;
}
if (mx >= sum / 2) {
ans += 1LL * mx * (sum - mx);
continue;
}
sort(v.begin(), v.end());
v.emplace_back(-1);
int cnt = 1;
for (int i = 1; i < v.size(); i++) {
if (v[i] != v[i - 1]) {
int x = v[i - 1], j = 1;
for (; j < cnt; j *= 2) {
t.emplace_back(x * j);
cnt -= j;
}
t.emplace_back(x * cnt);
cnt = 1;
} else {
cnt++;
}
}
solve<8>(t, sum);
}
cout << ans << '\n';
return 0;
}
```
## 日记和编辑器
直接写的 STL 暴力。成功获得 $64$ 分。