## 好三角形 #数学 #树状数组 #排序 首先看到曼哈顿距离就知道不好处理,所以把整个坐标轴旋转 $45\degree$ 变成切比雪夫距离[^1],更方便操作。题目要求存在 $T$ 满足 $d(P,T)=d(Q,T)=d(R,T)$,而在切比雪夫距离中,与 $T$ 点距离为 $d$ 的所有点会连成一个以 $T$ 为中心,边长为 $2d$ 的正方形。 因此,问题就变成了选择一个三元组,是否存在一个正方形使得三元组中每一个点都能在正方形的边上。但是这个问题非常的复杂,并且通过样例我们可以发现不能满足的三元组数量较少,因此可以选择容斥,求不满足条件的三元组的数量。 经过长达两个多小时的开庭,我找到了三元组不满足条件的充分条件,就是其中一个点在另外两个点以对角组成的长方形内部,这样子是肯定无法满足条件的,因为内部的那个点无法搞到正方形的边上,强制搞上另外两个点就不行了。 转换成坐标来表述就是如果三元组以 $x$ 递增/减排序的话,$y$ 坐标仍然也是递增/减的(二位偏序,严格),那么三元组不满足条件。所以我们能够简单地写出一个 $\mathcal O(n^3)$ 的暴力。 ```cpp #include <iostream> #include <map> using namespace std; using LL = long long; const LL kMaxN = 5e5 + 5; struct Node { LL x, y, u, v; } a[kMaxN]; LL n, ans; int main() { cin.tie(0)->sync_with_stdio(0); cin >> n; for (LL i = 1; i <= n; i++) { cin >> a[i].x >> a[i].y; a[i].u = a[i].x + a[i].y; a[i].v = a[i].x - a[i].y; } // 如果 j 在 i 和 k 作为对角组成的长方形内部,则无法构成好三角形 for (LL i = 1; i <= n; i++) { for (LL j = 1; j <= n; j++) { if (i != j) { for (LL k = 1; k <= n; k++) { if (k != i && k != j) { ans += a[i].u < a[j].u && a[j].u < a[k].u && a[i].v < a[j].v && a[j].v < a[k].v; ans += a[i].u < a[j].u && a[j].u < a[k].u && a[i].v > a[j].v && a[j].v > a[k].v; } } } } } cout << n * (n - 1) * (n - 2) / 6 - ans << '\n'; return 0; } ``` 可以发现这份代码能过大样例,只是复杂度不对。这证明我们猜测的结论正好就是三元组不满足条件的充分必要条件,那么就可以尝试优化了。 首先按照 $x$ 递增排序,这样子后面的 $x$ 一定大于前面的 $x$。$y$ 的话就用树状数组维护,此时就可以很简单地写出求解二位偏序二元组的代码了。但是这里是三元的,所以我们可以先跑一遍二维的,然后在二维的答案的基础上再跑一遍得到二维偏序三元组的数量,即不满足条件的三元组数量,然后用总数减去它就行了。 注意是严格递增/减,所以 $x$ 排序之后仍然要用双指针维护。时间复杂度 $\mathcal O(n\log_2 n)$。 ```cpp #include <iostream> #include <algorithm> using namespace std; using LL = long long; const LL kMaxN = 1e6 + 5; struct Node { LL x, y; } a[kMaxN]; LL val[kMaxN], tr[kMaxN], dp[kMaxN], n, m, ans; void modify(LL x, LL y) { for (; x <= m; x += x & -x) { tr[x] += y; } } LL query(LL x) { LL res = 0; for (; x; x -= x & -x) { res += tr[x]; } return res; } int main() { cin.tie(0)->sync_with_stdio(0); cin >> n; for (LL i = 1, x, y; i <= n; i++) { cin >> x >> y; a[i].x = x + y; a[i].y = x - y; val[++m] = a[i].y; } sort(val + 1, val + m + 1); m = unique(val + 1, val + m + 1) - val - 1; sort(a + 1, a + n + 1, [](const Node &a, const Node &b) { return a.x < b.x; }); // x 递增且 y 递增 for (LL i = 1, j = 1; i <= n; i++) { a[i].y = lower_bound(val + 1, val + m + 1, a[i].y) - val; for (; j < n && a[j].x < a[i].x; j++) { modify(a[j].y, 1); } dp[i] = query(a[i].y - 1); } fill(tr + 1, tr + m + 1, 0); for (LL i = 1, j = 1; i <= n; i++) { for (; j < n && a[j].x < a[i].x; j++) { modify(a[j].y, dp[j]); } ans += query(a[i].y - 1); } // x 递增且 y 递减 fill(tr + 1, tr + m + 1, 0); for (LL i = 1, j = 1; i <= n; i++) { for (; j < n && a[j].x < a[i].x; j++) { modify(a[j].y, 1); } dp[i] = query(m) - query(a[i].y); } fill(tr + 1, tr + m + 1, 0); for (LL i = 1, j = 1; i <= n; i++) { for (; j < n && a[j].x < a[i].x; j++) { modify(a[j].y, dp[j]); } ans += query(m) - query(a[i].y); } cout << n * (n - 1) * (n - 2) / 6 - ans << '\n'; return 0; } ``` [^1]: $A(x_1,y_1)$ 和 $B(x_2,y_2)$ 之间的切比雪夫距离为 $\max(|x_1-x_2|,|y_1-y_2|)$,即斜着走需要的最小步数。 ## 树上探测 #树形dp 首先预处理倍增 LCA,这样子就可以方便地计算树上距离。对于 $d_a+d_b=dis(a,b)$ 的数据,选择 $a$ 往上跳 $d_a$ 步或者选择 $b$ 往上跳 $d_b$ 步就行了,用那个不会跳到 LCA 以上的。其余数据全部写暴力。成功获得 $25$ 分。 ```cpp #include <iostream> #include <vector> using namespace std; const int kMaxN = 4e5 + 5, kLog = 21; int dp[kMaxN][kLog], d[kMaxN], n, q; vector<int> e[kMaxN]; void dfs(int x, int f) { d[x] = d[f] + 1; dp[x][0] = f; for (int i = 1; i < kLog; i++) { dp[x][i] = dp[dp[x][i - 1]][i - 1]; } for (int i : e[x]) { if (i != f) { dfs(i, x); } } } int jump(int x, int k) { for (int i = 0; i < kLog; i++) { if (k & (1 << i)) { x = dp[x][i]; } } return x; } int lca(int x, int y) { if (d[x] < d[y]) { swap(x, y); } x = jump(x, d[x] - d[y]); if (x == y) { return x; } for (int i = kLog - 1; i >= 0; i--) { if (dp[x][i] != dp[y][i]) { x = dp[x][i]; y = dp[y][i]; } } return dp[x][0]; } int dis(int x, int y) { return d[x] + d[y] - 2 * d[lca(x, y)]; } int main() { // freopen("main.in", "r", stdin); // freopen("main.out", "w", stdout); cin.tie(0)->sync_with_stdio(0); cin >> n >> q; for (int i = 1, u, v; i < n; i++) { cin >> u >> v; e[u].push_back(v); e[v].push_back(u); } dfs(1, 0); int x, dx, y, dy; for (; q; q--) { cin >> x >> dx >> y >> dy; int l = lca(x, y); if (d[x] + d[y] - 2 * d[l] == dx + dy) { if (d[x] < d[y]) { swap(x, y); swap(dx, dy); } if (dx <= d[x] - d[l]) { cout << jump(x, dx) << '\n'; } else { cout << jump(y, dy) << '\n'; } } else { bool b = 0; for (int i = 1; i <= n; i++) { if (dis(i, x) == dx && dis(i, y) == dy) { cout << i << '\n'; b = 1; break; } } if (!b) { cout << "-1\n"; } } } return 0; } ``` ## 按位覆盖 #二分 #前缀和 暴力都不会,$0$ 分。 ## 最小权值 #树形dp #背包dp 采取的纯暴力,DFS 套 DFS,成功获得 $15$ 分。 ## 总结 总分 $100+25+15=140$。 全场精力基本上全部都在想 T1,虽然最后想出来了但是另外几题啥都没过脑,代码源该降低难度了。不过感谢 PTY 老铁之前 ABC 教我们曼哈顿转切比雪夫,让我成功守住了 T1 的 AC。T2 应该也是我可以做的,等会想一想。后面两题太超标了。