做题记录 #3
A. QOJ2064 Bitset Master (5)
Date: 2025.10.20
不难发现,任意时刻对 \(p\) 的查询,其答案为:满足存在一个在当前时间以前的 操作子序列,按顺序操作了 \(p\) 到 \(x\) 的路径 的 \(x\) 个数。
那么此类路径问题不难想到点分治。设当前分治中心为 \(x\),考虑解决经过了 \(x\) 的所有路径贡献。可以倒着做一遍操作找到每个点第一次走到 \(x\) 的时间,再正着做一遍求出每个时刻 \(x\) 到达每个点的最晚出发时间。只有在最晚出发时间一个点第一次走到 \(x\) 之后才能到达。所以可以用树状数组动态维护一下后缀和就好了。
非常好想,但是不咋好写就是了。然后犯了个唐,点分治求 size 选择了在最后 ++sz 而不是最开头 sz=1,忘记了有初值。T 了挺久
Code
#pragma GCC optimize("Ofast,unroll-loops")
// STOOOOOOOOOOOOOOOOOOOOOOOOO hzt CCCCCCCCCCCCCCCCCCCCCCCORZ
#include <algorithm>
#include <cassert>
#include <iostream>
#include <numeric>
#include <vector>
using namespace std;
using LL = long long;
using PII = pair<int, int>;
constexpr int kN = 2e5 + 1, kM = 6e5 + 1;
int n, m, ans[kM];
vector<int> e[kN];
struct Operate { int o, x, y; };
int t[kM], lim;
vector<PII> bak;
inline void Add(int x, int w, bool o = 1) {
if (x == 0)
return;
o && (bak.emplace_back(x, w), 0);
for (; x <= lim; x += x & -x)
t[x] += w;
}
inline int Ask(int x) {
int ret = 0;
for (; x > 0; x -= x & -x)
ret += t[x];
return ret;
}
inline void Clear() {
for (auto [x, w] : bak)
Add(x, -w, 0);
bak.clear();
}
bool del[kN];
int sz[kN], dep[kN];
int Size(int x, int fa) {
sz[x] = 1;
for (auto v : e[x])
v != fa && !del[v] && (sz[x] += Size(v, x));
return sz[x];
}
int Root(int x, int fa, int w) {
for (auto v : e[x]) {
if (v != fa && !del[v] && sz[v] > w / 2)
return Root(v, x, w);
}
return x;
}
int mn[kN], lst[kN], bel[kN];
void Dfs(int x, int fa, int col) {
dep[x] = dep[fa] + 1, bel[x] = col;
mn[x] = lim + 1, lst[x] = 0;
for (auto v : e[x]) {
if (v != fa && !del[v])
Dfs(v, x, col);
}
}
void Divide(int x, int fa, auto &op) {
x = Root(x, fa, sz[x] >= sz[fa] ? Size(x, fa) : sz[x]);
del[x] = 1, dep[x] = 0;
vector<vector<Operate>> son, vec;
son.resize(e[x].size()), vec.resize(e[x].size());
lim = op.size();
for (int i = 0; i < e[x].size(); i++) {
if (int v = e[x][i]; !del[v])
Dfs(v, x, i);
}
for (int i = op.size() - 1; i >= 0; i--) { // Nodes to Root
auto [o, u, v] = op[i];
if (o == 1)
continue;
mn[x] = i + 1;
dep[u] < dep[v] && (swap(u, v), 0);
mn[u] = min(mn[u], mn[v]);
}
mn[x] = 1;
for (int i = 0; i < op.size(); i++) { // Root to Nodes
auto [o, u, v] = op[i];
if (o == 2) {
lst[x] = i + 1;
dep[u] > dep[v] && (swap(u, v), 0);
if (lst[u] > lst[v]) {
vec[bel[v]].push_back({2, lst[u], lst[v]});
Add(lst[v], -1), Add(lst[v] = lst[u], 1);
}
} else {
ans[v] += Ask(lim) - Ask(mn[u] - 1) + (mn[u] <= i + 1);
u != x && (vec[bel[u]].push_back({1, u, v}), 0);
}
u != x && (son[bel[u]].push_back(op[i]), 0);
}
Clear();
for (int i = 0; i < e[x].size(); i++) { // Same Blocks
if (del[e[x][i]])
continue;
for (auto [o, u, v] : vec[i]) {
if (o == 2)
Add(v, -1), Add(u, 1);
else
ans[v] -= Ask(lim) - Ask(mn[u] - 1);
}
vec[i].clear(), Clear();
vec[i].shrink_to_fit();
}
vec.clear();
for (int i = 0; i < e[x].size(); i++)
if (int v = e[x][i]; !del[v]) {
Divide(v, x, son[i]);
son[i].clear(), son[i].shrink_to_fit();
}
son.clear();
}
PII edge[kN];
int main() {
#ifndef ONLINE_JUDGE
freopen("input.in", "r", stdin);
freopen("output.out", "w", stdout);
#endif
cin.tie(0)->sync_with_stdio(0);
cin >> n >> m;
for (int i = 1, u, v; i < n; i++) {
cin >> u >> v, edge[i] = {u, v};
e[u].push_back(v), e[v].push_back(u);
}
vector<Operate> op;
int cnt = 0;
for (int o, x; m--;) {
cin >> o >> x;
if (o == 2) {
auto [u, v] = edge[x];
op.push_back({2, u, v});
} else
op.push_back({1, x, ++cnt});
}
Divide(1, 0, op);
for (int i = 1; i <= cnt; i++)
cout << ans[i] << '\n';
return 0;
}
B. QOJ2066 Data Structure Quiz (7)
Date: 2025.10.20
难评题。做的时候脑子很炸,不知道在想什么。
题目描述里有提到 KD-Tree,但是其实真去做横纵交替分治其实是不怎么可做的,因为 KD-Tree 原本是做点相关修改的,矩阵修改大概是没办法 polylog 划分的。但是矩阵修改可以考虑去差分,去做扫描线,这样首先只需要分治一维,其次修改变成了单行的,但是比较需要前缀和的信息维护。考虑具体怎么做。有两种想法:
-
考虑分治的时候解决掉跨过 mid 的所有区间。依旧可以把询问拆成 左半后缀 和 右半前缀。此时一端固定,如何做矩形 max???发现可以从 mid 开始,往前撤销 / 往后加入,然后维护从 mid 状态开始的 历史最值! 此时只需要维护 区间 max,历史 max,区间加以及删除历史记录这几个事情。可以考虑一棵简单的 segbeats,然后再加一个 reset tag,每次需要删除历史记录直接在根上打上这个 tag,下传形如每次见到这个点,把儿子的 tag 下传,删除儿子的历史最值,再给儿子打上 tag。也就是需要下传两层,比较奇怪但是的确需要如此。在进入 [l, r] 这一层分治之前,需要已经将 [1, l) 加入到线段树中。总的复杂度 \(\mathcal{O}(m\log^2 n + q\log n)\)。可以通过这道题。
-
考虑类似线段树,把一个询问区间拆分到每一个整的区间。此时每个询问是查询整个块,依然需要维护块内时刻出现过的最大值,依然类似历史最大值。考虑类似主席树的将这个点更新的东西(新加的点)拿出来,向上线段树合并,也把遇到的历史最大值取 max,即为区间出现过的最值。这个线段树合并是类似动态开点的,不过一个询问被拆分到了 \(\log\) 个地方去做,所以复杂度大概是 \(\mathcal{O}(m\log n+q\log^2 n)\)。这道题 q 大 m 小,不知道这样能不能过 8s,但是获得了一道新的题目。/qiang
反思时发现做法 1 是类似猫树分治的,但是在前面的练习中,左半后缀右半前缀的维护已经成为了更加具有包容性的思想。比较具有道理。
暂时没有写做法 2,有时间再补吧。
Code
// STOOOOOOOOOOOOOOOOOOOOOOOOO hzt CCCCCCCCCCCCCCCCCCCCCCCORZ
#include <algorithm>
#include <cassert>
#include <iostream>
#include <numeric>
#include <vector>
using namespace std;
using LL = long long;
using PII = pair<int, int>;
constexpr int kN = 5e4 + 2, kQ = 5e5 + 1;
#define int LL
int n, m, q;
struct Update { int l, r, w; };
vector<Update> vu[kN];
struct Query { int l1, r1, l2, r2, id; };
vector<Query> vq[4 * kN];
LL ans[kQ];
namespace Segment {
struct Node {
LL mx, hmx, add, had;
bool rst;
} t[4 * kN];
inline void PushUp(int x) {
t[x].mx = max(t[2 * x].mx, t[2 * x + 1].mx);
t[x].hmx = max(t[2 * x].hmx, t[2 * x + 1].hmx);
}
inline void PushTag(int x, LL add, LL had) {
if (x >= 4 * kN)
return;
t[x].hmx = max(t[x].hmx, t[x].mx + had);
t[x].had = max(t[x].had, t[x].add + had);
t[x].mx += add, t[x].add += add;
}
inline void Reset(int x) {
for (int v : {2 * x, 2 * x + 1})
PushTag(v, t[x].add, t[x].had);
t[x].add = t[x].had = 0, t[x].hmx = t[x].mx, t[x].rst = 1;
}
inline void PushDown(int x) {
for (int v : {2 * x, 2 * x + 1}) {
t[x].rst && (Reset(v), 0);
PushTag(v, t[x].add, t[x].had);
}
t[x].rst = t[x].add = t[x].had = 0;
}
void Add(int L, int R, LL w, int x = 1, int l = 1, int r = n) {
if (R < l || r < L)
return;
else if (L <= l && r <= R)
return PushTag(x, w, w);
int mid = (l + r) / 2;
PushDown(x);
Add(L, R, w, 2 * x, l, mid);
Add(L, R, w, 2 * x + 1, mid + 1, r);
PushUp(x);
}
LL Max(int L, int R, int x = 1, int l = 1, int r = n) {
if (R < l || r < L)
return -1e18;
else if (L <= l && r <= R)
return t[x].hmx;
int mid = (l + r) / 2;
PushDown(x);
return max(Max(L, R, 2 * x, l, mid), Max(L, R, 2 * x + 1, mid + 1, r));
}
}
using Segment::Reset, Segment::Add, Segment::Max;
inline void Insert(int x) {
for (auto [l, r, w] : vu[x])
Add(l, r, w);
}
inline void Remove(int x) {
for (int i = vu[x].size() - 1; i >= 0; i--) {
auto [l, r, w] = vu[x][i];
Add(l, r, -w);
}
}
void Divide(int x, int l, int r) {
int mid = (l + r) / 2;
for (int i = l; i <= mid; i++)
Insert(i);
sort(vq[x].begin(), vq[x].end(), [&](Query p, Query q) { return p.r1 < q.r1; });
int j = 0;
for (; j < vq[x].size() && vq[x][j].r1 == mid; j++);
for (int i = mid + 1; i <= r; i++) {
Insert(i);
i == mid + 1 && (Reset(1), 0);
for (; j < vq[x].size() && vq[x][j].r1 == i; j++) {
auto [_, __, L, R, id] = vq[x][j];
ans[id] = max(ans[id], Max(L, R));
}
}
for (int i = r; i >= mid + 1; i--)
Remove(i);
l != r && (Divide(2 * x + 1, mid + 1, r), 0);
sort(vq[x].begin(), vq[x].end(), [&](Query p, Query q) { return p.l1 > q.l1; });
for (j = 0; j < vq[x].size() && vq[x][j].l1 == mid + 1; j++);
Reset(1);
for (int i = mid; i >= l; i--) {
for (; j < vq[x].size() && vq[x][j].l1 == i; j++) {
auto [_, __, L, R, id] = vq[x][j];
ans[id] = max(ans[id], Max(L, R));
}
Remove(i);
}
l != r && (Divide(2 * x, l, mid), 0);
}
signed main() {
cin.tie(0)->sync_with_stdio(0);
cin >> n >> m >> q;
for (int l1, r1, l2, r2, x; m--;) {
cin >> l1 >> l2 >> r1 >> r2 >> x;
vu[l1].push_back({l2, r2, x});
r1 < n && (vu[r1 + 1].push_back({l2, r2, -x}), 0);
}
for (int l1, l2, r1, r2, i = 1; i <= q; i++) {
cin >> l1 >> l2 >> r1 >> r2;
int x = 1, l = 1, r = n, mid = (n + 1) / 2;
for (; l1 > mid + 1 || r1 < mid;) {
if (l1 <= mid)
x = 2 * x, r = mid;
else
x = 2 * x + 1, l = mid + 1;
mid = (l + r) / 2;
}
vq[x].push_back({l1, r1, l2, r2, i});
}
for (int i = 1; i <= n; i++)
sort(vu[i].begin(), vu[i].end(), [&](Update a, Update b) { return a.w < b.w; });
Divide(1, 1, n);
for (int i = 1; i <= q; i++)
cout << ans[i] << '\n';
return 0;
}
Day21A. 旅行商 (3)
赛时写了一个假完了的做法,因为我当时还没有对 n 和 点数 有区别这个事情有很深刻的体会,所以实际可能是 平方L 的我都测试了很久。
发现之后立马想到可以走一些对角线,按曼哈顿距离穿插。很明显严格的曼哈顿距离相等才穿插是不现实的,值域毕竟和点数有所差距。所以考虑了平均分为 \(n\) 块,块内按 (x,y) 排序走,块和块之间直接接上,轻松的通过了 pretest。然后不知道为什么,反正想到了很多次奇偶分组,但是并没有写。可能是因为我觉得现在这份代码过了大样例自有它的道理所在。所以并没有改。
赛时写了点比较弱智的错误,导致正式测试出现了一些 滚木 以及 duplicate 输出,但是并没有长度不对的情况。赛后改的时候,加上了奇偶分组,并把可能的小问题改掉了,依然过不了第四个大样例。调了很久认定为比较器的问题。当时我的比较器形如,令 t 为 (x, y) 大小关系,看 (x + y) / B 的奇偶性决定返回 t 还是 !t。我觉得很对,因为我将每个 (x + y) / B 的东西塞入了不同的 set 来分组,块里的组别一定相同。一个小时后我知道了比较器需要满足“自反性”。深刻的教训。
不过把奇偶分组删除也可以通过,体现出我赛时的想法是非常正确的。
Code
// STOOOOOOOOOOOOOOOOOOOOOOOOO hzt CCCCCCCCCCCCCCCCCCCCCCCORZ
#include <algorithm>
#include <cassert>
#include <cmath>
#include <iostream>
#include <numeric>
#include <set>
#include <vector>
using namespace std;
using LL = long long;
using PII = pair<int, int>;
constexpr int kN = 300 + 1, kN2 = kN * kN;
int T, n, L, q, B;
int x[kN2], y[kN2];
struct Node {
int x, y, id;
inline bool operator<(const Node& a) const {
return PII(x, y) != PII(a.x, a.y) ? PII(x, y) < PII(a.x, a.y) : id < a.id;
}
};
set<Node> s[kN];
int main() {
#ifndef ONLINE_JUDGE
freopen("input.in", "r", stdin);
freopen("output.out", "w", stdout);
#endif
cin.tie(0)->sync_with_stdio(0);
for (cin >> T; T--;) {
cin >> n >> L;
B = (2 * L + n - 1) / n;
for (int i = 1; i <= n * n; i++) {
cin >> x[i] >> y[i];
s[(x[i] + y[i]) / B].insert({x[i], y[i], i});
}
for (int i = 0; i <= n; i++) {
for (auto p : s[i]) cout << p.id << ' ';
}
cout << '\n';
cin >> q;
for (int c, a, b; q--;) {
cin >> c;
int bel = (x[c] + y[c]) / B;
auto t = s[bel].erase({x[c], y[c], c});
cin >> x[c] >> y[c];
bel = (x[c] + y[c]) / B;
auto it = s[bel].insert({x[c], y[c], c}).first;
if (it == s[bel].begin()) {
int i = bel - 1;
for (; i >= 0 && s[i].empty(); i--);
cout << (i >= 0 ? s[i].rbegin()->id : 0) << '\n';
} else
cout << prev(it)->id << '\n';
}
for (int i = 0; i <= n; i++) s[i].clear();
}
return 0;
}
C. CF566C Logistical Questions (7.5)
有趣题/se
这个重心的路径权值很诡异,但是可以尝试将朴素重心的性质代入到这道题中。求重心有一个性质非常好的做法,最初是在写点分治的时候在 哥哥 的代码里看见的:就是随意选择一个点,以它为根求 size,若存在 \(size_v>\frac n2\),则走进其中,否则重心就是该点。这个题不太可能有好的子树 size 的替代品,所以尝试用另一个重心的定义:到所有点的距离之和。考虑这个重心写法的本质,其实是如果存在 \(size>\frac n2\) 的子树,则走进其中可以让 \(\sum size\) 变小。所以不妨猜测若 x 到所有点的距离和 大于 y 到所有点的距离和,则 y 比 x 更靠近重心。
先不急着证明,考虑接下来怎么做。此时暴力做是 \(\mathcal{O}(ndeg_x)\) 才能知道 \(x\) 该往哪走。劣完了,但是很明显这道题是很可能必要这步的,所以尝试优化这个定向的过程。考虑走入 \(i\) 子树给距离和带来的变化量。令:
于是走过 \(w\) 边权进 \(i\) 子树的距离和变化量是:\(F(0)+G(w)-(F(w)+G(0))\)。我们希望这个值小于 \(0\),即 \(F(w)-F(0)>G(w)-G(0)\)。反思这个事情,不难发现边权可以拆分成一个包含着 边权全为 \(\Delta\)、点权全为 \(0\) 的链。因此可以考虑把走过的距离改一个 \(\Delta\)。即 \(F(w)-F(w-\Delta)>G(\Delta)-G(0)\),\(F'(w)>G'(0)\)。\(G\) 后续的求法应该是拿全集扣掉一棵子树,但是 \(w\) 毕竟之和子树相关,塞到全集里估计不太好维护,考虑改成 \(F'(0)>G'(w)\) 再校验。不难发现如果校验成功但是没直接进去,说明重心在这条生造的链中间,那么真正的重心应该在当前这条边的端点之中,取最优即可。于是对 \(F(x)\), \(G(x)\) 求导,得到 \(\sum \frac 32(w+x)^{0.5}\),讨论一下比较即可。由于求值从枚举全集改为了枚举子树,此时的单次定向复杂度降到了 \(\mathcal{O}(n)\)。
此时我有些卡住,我在思考怎么去快速维护这样的 \(F\), \(G\) 函数,去走一条路径,直到重心停下。但其实根本不需要维护,\(\mathcal{O}(n)\) 定向其实已经可以做掉:考虑进行 点分治!此时每次可能的重心将会被划分到连通块中,这样仅需 \(\mathcal{O}(\log n)\) 次定向即可找到重心。总复杂度 \(\mathcal{O}(n\log n)\)。非常美丽!
但是,原题面其实并没有提到 重心 二字,怎么证明这个重心的性质放在如此特殊的情况下也适用呢?我们需要证明以下两个性质:
- 走一条边使得距离和变大后,再也不会经过深入后变小。考虑每次深入都会让 \(F\) 里的 \(dis\) 越来越小,而 \(G\) 里的 \(dis\) 越来越大。这个变化量的式子本质上就是看 \(dis\) 变大变小给距离和带来的变化,由于 \((x+y)^{1.5}-x^{1.5}=(x+y)\sqrt y\),点数的交换不会对变化量产生影响,但是 \(G\) 的 \(x\) 越大增长越大,\(F\) 减小的也越来越慢,因此 \(\Delta F-\Delta G\) 是单调递减的。因此距离和的变化量只会越来越大(负的)。
- 对于一个点 \(x\),最多只有一条边能使距离和变小。考虑到 \(G(x) = All(x) - F(x), All(x)=\sum(dis_v+x)^{1.5}\),距离和变小即 \(F(x)\) 大过 \(All(x)\) 一半。很明显一个点的子树之间没有交集,\(F\) 也不会是负的,因此不可能存在两个使距离和变小的子树。
可能不太严谨,感觉差不多。但是性质还挺美丽的。应该可以扩展到其他指数。
Code
// STOOOOOOOOOOOOOOOOOOOOOOOOO hzt CCCCCCCCCCCCCCCCCCCCCCCORZ
#include <algorithm>
#include <cassert>
#include <cmath>
#include <iostream>
#include <numeric>
#include <vector>
#include <iomanip>
using namespace std;
using LL = long long;
using PII = pair<int, int>;
constexpr int kN = 2e5 + 1;
int n, a[kN];
vector<PII> e[kN];
int sz[kN];
bool del[kN];
int Size(int x, int fa) {
sz[x] = 1;
for (auto [v, _] : e[x])
v != fa && !del[v] && (sz[x] += Size(v, x));
return sz[x];
}
int Root(int x, int fa, int w) {
for (auto [v, _] : e[x]) {
if (v != fa && !del[v] && sz[v] > w / 2)
return Root(v, x, w);
}
return x;
}
vector<int> V[kN];
int dep[kN];
double f[kN];
void Dfs(int x, int fa, int bel) {
V[bel].push_back(x);
f[bel] += sqrt(dep[x]) * a[x];
for (auto [v, w] : e[x]) {
if (v != fa)
dep[v] = dep[x] + w, Dfs(v, x, bel);
}
}
int sp = 0;
int Divide(int x) {
double sum = 0;
del[x] = 1;
for (auto [v, w] : e[x])
dep[v] = w, Dfs(v, x, v), sum += f[v];
for (auto [v, w] : e[x]) {
if (del[v] || (sum - f[v]) - f[v] > 1e-9)
continue;
double f = 0, g = a[x] * sqrt(w);
for (auto i : V[v])
f += a[i] * sqrt(dep[i] - w);
for (auto [c, _] : e[x]) {
if (c == v)
continue;
for (auto i : V[c])
g += a[i] * sqrt(dep[i] + w);
}
if (g - f > 1e-9) {
sp = v;
break;
}
for (int i = 1; i <= n; i++)
::f[i] = dep[i] = 0, V[i].clear();
return Divide(Root(v, x, sz[v] > sz[x] ? Size(v, x) : sz[v]));
}
return x;
}
int main() {
#ifndef ONLINE_JUDGE
freopen("input.in", "r", stdin);
freopen("output.out", "w", stdout);
#endif
cin.tie(0)->sync_with_stdio(0);
cin >> n;
for (int i = 1; i <= n; i++)
cin >> a[i];
for (int i = 1, u, v, w; i < n; i++) {
cin >> u >> v >> w;
e[u].emplace_back(v, w), e[v].emplace_back(u, w);
}
int Pos[2] = {Divide(Root(1, 0, Size(1, 0))), sp};
long double Ans[2] = {0.0, 1e30};
for (int o : {0, 1}) {
int pos = Pos[o];
if (!pos)
continue;
double ans = 0.0;
dep[pos] = 0;
for (auto [v, w] : e[pos])
dep[v] = w, Dfs(v, pos, v);
for (int i = 1; i <= n; i++)
ans += a[i] * sqrt(dep[i]) * dep[i];
Ans[o] = ans;
}
cout << Pos[Ans[0] - Ans[1] > 1e-9] << ' ';
cout << fixed << setprecision(7) << min(Ans[0], Ans[1]) << '\n';
return 0;
}
2025.10.22:复习了一下虚树,简单做了一下虚树题单,挑战了一下 CSP-S2024 T3,暂时没有什么有意思的题,故不是很想记录/hsh
Day22A. Lis (3)
这是一个类似滑动窗口的东西,考虑一边加数一边删数。首先做 LIS 可以很轻松的维护当前时刻,长度为 \(i\) 的子序列的尾数最小为多少。删除可以对于每个可能的长度为 \(i\) 的尾数,记录新的数被更新到了哪里,删除的时候到时间 pop_back 即可。加数删数都是朴素的,此时问题在于怎么维护答案。
可以一开始窗口置于最右侧,维护左侧 能接上 \(w\) 的最大 LIS 长度。这个不难和 pop_back 同步处理,操作类似一个区间 -1;再维护右侧每个 LIS 长度对应的最大首个数字,直接将长度加到左侧维护的序列 最大数对应的位置上。每次修改一定是单点的,相当于序列单点加减。不难使用线段树维护,是好写的。
这题主要运用了 加入一个数 / 删除一个数 只会对一个长度的 LIS 尾数产生影响。做这个题的时候一开始思路并不明确,尝试去维护归属集合,pop_back 时划分,并没有归约到序列问题上。因此浪费大量时间,1.5h 才完成此题。(虽然有些去看其他题了/hsh)
Code
// STOOOOOOOOOOOOOOOOOOOOOOOOO hzt CCCCCCCCCCCCCCCCCCCCCCCORZ
#include <algorithm>
#include <cassert>
#include <iostream>
#include <numeric>
#include <vector>
using namespace std;
using LL = long long;
using PII = pair<int, int>;
constexpr int kN = 5e5 + 1;
int n, k, a[kN];
vector<int> pre;
vector<vector<int>> rec;
int p[kN], ans[kN];
struct Node {
int mx, tag;
} t[4 * kN];
inline void PushUp(int x) { t[x].mx = max(t[2 * x].mx, t[2 * x + 1].mx); }
int Pos = 0;
void Build(int x, int l, int r) {
if (l == r) {
Pos += (pre.size() > Pos && pre[Pos] == l);
return t[x].mx = Pos, void();
}
int mid = (l + r) / 2;
Build(2 * x, l, mid);
Build(2 * x + 1, mid + 1, r);
PushUp(x);
}
inline void PushTag(int x, int w) { t[x].mx += w, t[x].tag += w; }
inline void PushDown(int x) {
PushTag(2 * x, t[x].tag);
PushTag(2 * x + 1, t[x].tag), t[x].tag = 0;
}
void Point(int p, int w, int x = 1, int l = 1, int r = n) {
if (l == r)
return t[x].mx += w, void();
int mid = (l + r) / 2;
PushDown(x);
p <= mid ? Point(p, w, 2 * x, l, mid) : Point(p, w, 2 * x + 1, mid + 1, r);
PushUp(x);
}
void Range(int L, int R, int x = 1, int l = 1, int r = n) {
if (R < l || r < L)
return;
else if (L <= l && r <= R)
return PushTag(x, -1);
int mid = (l + r) / 2;
PushDown(x);
Range(L, R, 2 * x, l, mid);
Range(L, R, 2 * x + 1, mid + 1, r);
PushUp(x);
}
int main() {
#ifndef ONLINE_JUDGE
freopen("input.in", "r", stdin);
freopen("output.out", "w", stdout);
#endif
cin.tie(0)->sync_with_stdio(0);
cin >> n >> k;
for (int i = 1; i <= n; i++)
cin >> a[i];
for (int i = 1; i <= n - k; i++) {
if (pre.size() == 0 || a[i] > pre.back()) {
p[i] = pre.size(), pre.push_back(a[i]);
rec.push_back(vector<int>{a[i]});
} else {
int l = lower_bound(pre.begin(), pre.end(), a[i]) - pre.begin();
p[i] = l, pre[l] = a[i], rec[l].push_back(a[i]);
}
}
Build(1, 1, n);
vector<int> ans = {int(pre.size())}, cur, suf;
for (int i = n; i > k; i--) {
{
int pos = p[i - k], l = rec[pos].back(), r;
rec[pos].pop_back();
if (rec.back().empty())
rec.pop_back(), pre.pop_back(), r = n;
else
pre[pos] = rec[pos].back(), r = rec[pos].back() - 1;
Range(l, r);
}
if (suf.size() == 0 || a[i] < suf.back())
suf.push_back(a[i]), Point(a[i], suf.size());
else {
int l = lower_bound(suf.begin(), suf.end(), a[i], greater<int>()) - suf.begin();
Point(suf[l], -l - 1), Point(suf[l] = a[i], l + 1);
}
ans.push_back(t[1].mx);
}
reverse(ans.begin(), ans.end());
for (auto i : ans)
cout << i << ' ';
return 0;
}
Day22B. Set (3)
可以看作最小化问题,尝试二分答案。设 \(mx=\max l_i\),首先若有 \(mx>m\),则无解。于是不难得到一个下界 \(m+mx\) 和一个上界 \(m+2mx\)。设当前二分到 \(V\),那么问题可以转化为:通过一系列操作,在满足 \([1, n-1]\) 的所有条件下,尝试让 \(|S_{n-1}\cap S_0|\) 尽量小。若最小能小过 \(m-l_0\),则值域合法。
现在尝试做到这件事情,可以把当前的值域划分为 \(S_0\) 中的 \(A\) 以及 不在其中的 \(B\)。此时只在乎当前的 \(|S_i\cap A|\),不难发现这个集合大小其实是个区间。维护这个区间 \([l,r]\),现在到了第 \(i\) 个限制,需要转移。若想要新的集合大小最小,由于 \(B\) 剩下的空间不一定能够放下整个 \(S_i\),因此操作顺序应当形如 \(A\rightarrow B, A\rightarrow B, B\rightarrow A\),转移式形如令 \(x=V-2m+lst\),若 \(x\geq l_i\),则 \(mn=0\);若 \(x<l_i\),则 \(mn = \max(0, lst - x + \max(l_i - x - (lst - x), 0))\),其实化简后 \(lst\) 只剩里层的 \(-lst\),因此 使总体小需要 \(lst\) 大,取 \(lst=r\)。若想要新的集合大小最大,则形如 \(B\rightarrow A, A\rightarrow B\),转移式则为 \(mx = m - \max(0, l_i - m + x)\)。由于右侧需要减去,需要里层 \(+x\) 最小,则取 \(x=l\)。
算是个小分讨,补的时候也讨论了很久,有概率是脑子不好使。赛时一直在想一些构造方式,尝试每次转移的时候新增点,但忽略了最后一个限制会导致即使构造出来,与最后限制直接硬搬的正确性很难有保障。理应要想到二分答案的,还是欠思考欠联系。机房只有 chenyx 切掉了,或许这就是 CF IM 的二分意识/bx
Code
// STOOOOOOOOOOOOOOOOOOOOOOOOO hzt CCCCCCCCCCCCCCCCCCCCCCCORZ
#include <algorithm>
#include <cassert>
#include <iostream>
#include <numeric>
#include <vector>
using namespace std;
using LL = long long;
using PII = pair<int, int>;
constexpr int kN = 1e6 + 1;
int T, n;
LL m, a[kN];
bool Check(LL lim) {
LL l = m, r = m;
for (int i = 2; i <= n; i++) {
LL x = lim - m - m + r;
LL tl = (x >= a[i] ? 0 : max(0ll, r - x + max(a[i] - x - (r - x), 0ll)));
LL tr = m - max(0ll, a[i] - m + l);
l = tl, r = tr;
}
return l <= m - a[1];
}
inline void Solve() {
LL mx = 0;
for (int i = 1; i <= n; i++)
cin >> a[i], mx = max(mx, a[i]);
if (mx > m)
return cout << "-1\n", void();
LL l = m + mx, r = m + 2 * mx, ans = -1;
for (LL mid; l <= r;) {
mid = (l + r) / 2;
if (Check(mid))
r = mid - 1, ans = mid;
else
l = mid + 1;
}
cout << ans << '\n';
}
int main() {
#ifndef ONLINE_JUDGE
freopen("input.in", "r", stdin);
freopen("output.out", "w", stdout);
#endif
cin.tie(0)->sync_with_stdio(0);
for (cin >> T; T--;) {
cin >> n >> m;
Solve();
}
return 0;
}
Day22C. Qua (4)
感觉很难啊!考虑 DP,设 \(f_{i, j}\) 表示当前考虑点 \(i\),填了 \(j\) 的最小答案。首先要注意到这个 \(f_i\) 其实一定是个二次函数,才能进行这道题的有效思考/hsh 考场上没注意到就又当成大构造结论做了,于是纯浪费时间。
注意到是二次函数之后,可以发现向上转移形似 \(f(i) = ax ^ 2 + bx + c, ax ^ 2 + bx + c + (y - x) ^ 2 \Rightarrow f(i / 2)\)。
考虑拆掉,变成 \((a+1)x^2+(b-2y)x+c+y^2\),需要对其进行最小化。由于这些二次函数均向上开口,取 \(x=\frac{b-2y}{2a+2}\) 带入即可。再更换主元,得到 \(\frac{a}{a+1}y^2+\frac{b}{a+1}+c-\frac{b^2}{4a+4}\)。贡献到父亲即可。此时只需要初始暴力做一下,单点修改直接暴力改一条祖先链即可。
不难发现所有的求逆元都可以避开瓶颈,因此复杂度可以做到 \(\mathcal{O}(qn)\)。
Code
// STOOOOOOOOOOOOOOOOOOOOOOOOO hzt CCCCCCCCCCCCCCCCCCCCCCCORZ
#include <algorithm>
#include <cassert>
#include <iostream>
#include <numeric>
#include <vector>
using namespace std;
using LL = long long;
using PII = pair<int, int>;
constexpr int kN = 1 << 18, kP = 998244353;
inline int Inv(int a) {
int ret = 1, b = kP - 2;
for (; b > 0; b /= 2) {
if (b % 2 == 1)
ret = 1ll * ret * a % kP;
a = 1ll * a * a % kP;
}
return ret;
}
int n, m, w[kN], a[kN], inv[kN], INV;
struct Node { int b, c; } f[kN];
inline void PushUp(int x) {
int d = __lg(x) + 1;
auto [lb, lc] = f[2 * x];
auto [rb, rc] = f[2 * x + 1];
f[x].b = 1ll * (lb + rb) * inv[d] % kP;
int i = (kP + 1) / 2;
i = 1ll * i * i % kP;
f[x].c = (lc + rc - (1ll * lb * lb + 1ll * rb * rb) % kP * i % kP * inv[d]) % kP;
f[x].c = (f[x].c + kP) % kP;
}
inline void Output() {
int x = ((kP + 1ll) / 2 * -f[1].b % kP * INV % kP + kP) % kP;
cout << (1ll * a[1] * x % kP * x + 1ll * f[1].b * x + f[1].c) % kP << '\n';
}
int main() {
#ifndef ONLINE_JUDGE
freopen("input.in", "r", stdin);
freopen("output.out", "w", stdout);
#endif
cin.tie(0)->sync_with_stdio(0);
cin >> n >> m;
for (int i = (1 << n - 1); i < 1 << n; i++) {
cin >> w[i];
f[i / 2].b = (f[i / 2].b - 2 * w[i] + kP) % kP;
f[i / 2].c = (f[i / 2].c + 1ll * w[i] * w[i]) % kP;
}
a[n - 1] = 2;
for (int d = n - 2; d >= 1; d--) {
inv[d] = Inv(a[d + 1] + 1);
a[d] = 2ll * a[d + 1] * inv[d] % kP;
}
INV = Inv(a[1]);
for (int i = (1 << n - 2) - 1; i >= 1; i--)
PushUp(i);
Output();
for (int x, val; m--;) {
cin >> x >> val;
f[x / 2].b = (f[x / 2].b + 2 * w[x] - 2 * val + kP) % kP;
f[x / 2].c = (f[x / 2].c - 1ll * w[x] * w[x] % kP + 1ll * val * val + kP) % kP;
w[x] = val;
for (x /= 4; x >= 1; x /= 2)
PushUp(x);
Output();
}
return 0;
}
Day22D. Car (7.5)
极为巧妙的博弈 DP 优化,令人惊叹。
首先考虑这个博弈过程。分析操作性质,发现第 \(i\) 次操作后就再也无法对第 \(i\) 个卡牌进行翻转了,于是可以设计一个博弈 DP:\(f_{i,j}\) 表示第 \(i\) 个操作决策前,第 \(i\) 张卡牌当前是 翻转 / 未翻转 对应的后续答案。至于具体记录什么,由于有着先后手的转化,如果记录同一个人的得分,那么可以想象到会多出来很多边界的系数,而且分奇偶 取 max 取 min,极为丑陋。不难想到状态记录为当前的 先手得分减去后手得分。
考虑转移,如 \(f_{i, 0}\)。很明显,可以在做到拿下 \(a_i\) 的情况下到达 \(f_{i+1,0/1}\) 两种状态,即 \(f_{i,0}=\max(a_i-f_{i+1,0},a_i-f_{i+1,1})\)。对于 \(f_{i, 1}\),首先肯定可以拿到 \(a_i\) 达到 \(f_{i+1,0}\) 状态;第 \(i\) 张牌被翻转过,由于前面的决策一定是对于某个人最优的,因此这张牌当前一定是先手面朝下的,翻 \(a_{i+1}\) 会使自己拿不到这个 \(a_i\)。因此转移为 \(f_{i,1}=\max(a_i-f_{i+1,0},-a_i-f_{i+1,1})\)。达成了一个 \(\mathcal{O}(qn)\) 的 暴力。
继续转化:发现 \(0/1\) 状态转移极为相似,但 \(1\) 状态多一个负,因此 \(f_{i,0}\geq f_{i,1}\)。于是有 \(f_{i,0}=a_i-f_{i+1,1}\),但是 \(1\) 状态并不好简化。发现现在的 \(0\) 状态转移和 \(1\) 转移右边很像,于是考虑作差(wtf???):\(f_{i,0}-f_{i,1}=\min(f_{i+1,0}-f_{i+1,1},2a_i)\)。发现了一样的东西,于是设 \(g_i=f_{i,0}-f_{i,1}\),就有 \(g_i=\min(g_{i+1},2a_i)\),即 \(2a_i\) 的后缀 min。
此时再看 \(0\) 转移式,\(f_{i,0}=a_i-f_{i+1,0}+g_{i+1}\)。嵌套可以层层剥开,直到边界的 \(f_{n,0}=a_n,f_{n,1}=-a_n,g_n=2a_n\) 处。可得 \(f_{1, 0}=\sum\limits_{i=1}^n(-1)^{i-1}a_i+\sum\limits_{i=2}^n(-1)^ig_i\)。此时左式非常好处理,右式是个带系数后缀 min 和,可以单边递归解决,复杂度 \(\mathcal{O}(n\log^2n)\)。
想这道题的时候想到了需要记录先手减去后手,但是对博弈 DP 的思想理解完全没有,所以的确做不了这个题。现在来看,DP 算是我的一个极大知识缺陷,有待填坑。
Code
// STOOOOOOOOOOOOOOOOOOOOOOOOO hzt CCCCCCCCCCCCCCCCCCCCCCCORZ
#include <algorithm>
#include <cassert>
#include <iostream>
#include <numeric>
#include <vector>
using namespace std;
using LL = long long;
using PII = pair<int, int>;
constexpr int kN = 2e5 + 1;
int n, q, a[kN];
struct Node {
int mn;
LL sum;
} t[4 * kN];
inline int K(int l, int r) { return (r - l + 1) % 2 * (r % 2 ? -1 : 1); }
LL Get(int w, int x, int l, int r) {
if (2 * a[r] <= w)
return t[x].sum;
else if (t[x].mn > w)
return K(l, r) * w;
int mid = (l + r) / 2;
if (t[2 * x + 1].mn > w)
return Get(w, 2 * x, l, mid) + K(mid + 1, r) * w;
return t[x].sum - t[2 * x + 1].sum + Get(w, 2 * x + 1, mid + 1, r);
}
void Build(int x = 1, int l = 2, int r = n) {
if (l == r)
return t[x] = {2 * a[l], K(l, l) * 2 * a[l]}, void();
int mid = (l + r) / 2;
Build(2 * x, l, mid);
Build(2 * x + 1, mid + 1, r);
t[x].mn = min(t[2 * x].mn, t[2 * x + 1].mn);
t[x].sum = t[2 * x + 1].sum + Get(t[2 * x + 1].mn, 2 * x, l, mid);
}
void Update(int p, int w, int x = 1, int l = 2, int r = n) {
if (l == r)
return t[x] = {2 * w, K(p, p) * 2 * w}, void();
int mid = (l + r) / 2;
if (p <= mid)
Update(p, w, 2 * x, l, mid);
else
Update(p, w, 2 * x + 1, mid + 1, r);
t[x].mn = min(t[2 * x].mn, t[2 * x + 1].mn);
t[x].sum = t[2 * x + 1].sum + Get(t[2 * x + 1].mn, 2 * x, l, mid);
}
LL cur = 0, sum = 0;
inline void Output() {
cout << (sum + cur + t[1].sum) / 2 << '\n';
}
int main() {
#ifndef ONLINE_JUDGE
freopen("input.in", "r", stdin);
freopen("output.out", "w", stdout);
#endif
cin.tie(0)->sync_with_stdio(0);
cin >> n >> q;
for (int i = 1; i <= n; i++) {
cin >> a[i], sum += a[i];
cur += (i % 2 ? 1 : -1) * a[i];
i > 1 && (Update(i, a[i]), 0);
}
Output();
for (int p, x; q--;) {
cin >> p >> x, sum += x - a[p];
cur += (p % 2 ? 1 : -1) * (x - a[p]), a[p] = x;
p > 1 && (Update(p, x), 0), Output();
}
return 0;
}
D. P3323 世界树 (5)
早期退坑之题/hsh 不过那时候不会做估计是纯贺调不懂导致的。
这题就怕把虚树贴脸上了。建出来之后想后续怎么做。首先考虑如果当前虚树没有虚点怎么做,即虚树上每个点都对应着一个关键点,只需要对每条边计算对应的贡献即可。很容易想到就是一个路径中点,但是路径两侧还会有其他的子树。不过发现对于重链上的一条边,不会存在其他关键点在路径内部子树中。因此对应的点数可以直接拿 sz[top] - sz[x] 计算。一些 corner 是,首先因为有一些点直接挂在了某个关键点上,但是被孤立的,需要被囊括在这个关键点的答案中。这个事情可以直接对一个关键点维护:初值是其 siz;在算边贡献时,对边对应路径的 top 的 siz 从上面的关键点扣掉。
还有一个 corner,是虚树的根上面还有东西,这样也统计不到。但是如果能存在虚点的话,直接把 \(1\) 加进虚树里即可,所以直接考虑带虚点。但还是可以记录下来每个虚点对应的最近的关键点编号及其距离,不过这个时候虚树边的贡献就不一定是路径中点了,需要带权进行一些计算。上述 corner 可以同样处理。
其实这题写起来确实有点麻烦,但是给人的感觉是其实主要难在 DP,虚树本身还好。这道题也体现出来虚树一些性质的,比如按边考虑,按边计算贡献,还是比较有手法的。
Code
// STOOOOOOOOOOOOOOOOOOOOOOOOO hzt CCCCCCCCCCCCCCCCCCCCCCCORZ
#include <algorithm>
#include <cassert>
#include <iostream>
#include <numeric>
#include <vector>
using namespace std;
using LL = long long;
using PII = pair<int, int>;
constexpr int kN = 3e5 + 1;
int n, q;
vector<int> e[kN];
int f[kN][20], sz[kN], dep[kN], dfn[kN], dfc;
void Init(int x, int fa) {
dfn[x] = ++dfc;
f[x][0] = fa;
for (int d = 1; d < 20; d++)
f[x][d] = f[f[x][d - 1]][d - 1];
sz[x] = 1, dep[x] = dep[fa] + 1;
for (auto v : e[x])
v != fa && (Init(v, x), sz[x] += sz[v]);
}
inline int LCA(int x, int y) {
dep[x] < dep[y] && (swap(x, y), 0);
for (int d = 0; d < 20; d++)
(dep[x] - dep[y] >> d & 1) && (x = f[x][d]);
if (x == y)
return x;
for (int d = 19; d >= 0; d--) {
if (f[x][d] != f[y][d])
x = f[x][d], y = f[y][d];
}
return f[x][0];
}
inline int Jump(int x, int h) {
for (int d = 0; d < 20; d++)
(h >> d & 1) && (x = f[x][d]);
return x;
}
vector<int> V;
int imp[kN], ans[kN], fa[kN];
inline bool Comp(int i, int j) { return dfn[i] < dfn[j]; }
PII mn[kN];
#define l first
#define x second
inline PII Add(PII &x, int w) { return {x.l + w, x.x}; }
int siz[kN];
inline void Solve() {
for (int i = V.size() - 1, x; i >= 0; i--) {
if (x = V[i]; imp[x] != 0)
mn[x] = {0, x};
mn[fa[x]] = min(mn[fa[x]], Add(mn[x], dep[x] - dep[fa[x]]));
siz[x] = sz[x];
}
mn[0] = {1e9, 1e9};
for (auto x : V) {
if (x == 1)
continue;
int len = dep[x] - dep[fa[x]];
int fa = ::fa[x], top = Jump(x, len - 1);
siz[fa] -= sz[top];
mn[x] = min(mn[x], Add(mn[fa], len));
if (mn[x].x != mn[fa].x) {
int d = (len + mn[fa].l - mn[x].l) / 2;
d -= !((mn[fa].l + mn[x].l & 1) ^ (len & 1)) && (mn[fa].x < mn[x].x);
int mid = Jump(x, d);
ans[imp[mn[x].x]] += sz[mid] - sz[x];
ans[imp[mn[fa].x]] += sz[top] - sz[mid];
} else
ans[imp[mn[x].x]] += sz[top] - sz[x];
}
for (auto x : V)
ans[imp[mn[x].x]] += siz[x];
}
int main() {
#ifndef ONLINE_JUDGE
freopen("input.in", "r", stdin);
freopen("output.out", "w", stdout);
#endif
cin.tie(0)->sync_with_stdio(0);
cin >> n;
for (int i = 1, u, v; i < n; i++) {
cin >> u >> v;
e[u].push_back(v), e[v].push_back(u);
}
Init(1, 0);
for (int i = 1; i <= n; i++)
e[i].clear();
cin >> q;
fill_n(mn, n + 1, PII{1e9, 1e9});
for (int m; q--;) {
cin >> m, V.push_back(1);
for (int i = 1, x; i <= m; i++)
cin >> x, imp[x] = i, V.push_back(x);
sort(V.begin(), V.end(), Comp);
for (int i = 1; i < m; i++)
V.push_back(LCA(V[i], V[i + 1]));
sort(V.begin(), V.end(), Comp);
V.erase(unique(V.begin(), V.end()), V.end());
for (int i = 0; i < V.size() - 1; i++)
fa[V[i + 1]] = LCA(V[i], V[i + 1]);
Solve();
for (int i = 1; i <= m; i++)
cout << ans[i] << ' ', ans[i] = 0;
cout << '\n';
for (auto i : V)
fa[i] = imp[i] = siz[i] = 0, mn[i] = {1e9, 1e9};
V.clear();
}
return 0;
}
E. P4426 毒瘤 (6.5~7.5)
正如其名,确实比较具有实现难度,不过居然是 HNOI 2018 的早期题目,不得不好好做做。
思路是比较简单的,因为注意到数据范围 m-n+1 很小,即非树边数量极小,最多 11 条,所以考虑一些暴力枚举向的东西。于是就不难想到一个暴力:首先枚举这些边的端点状态 (01, {10, 00}) 两种,\(\mathcal{O}(2^k)\) 暴力枚举后,此时有一些点的状态是会被删除一半的。暴力做就重做一次整棵树 DP,\(\mathcal{O}(n)\),总的下来其实只有 2e8,还可以拉 dfs 序下来免 dfs 做 DP。
但是考虑到时代局限性,尊重这个题目。再想,发现不难 DDP,重链剖分然后每次暴力修改。可能是 \(k\log n\) 的。此时已经能做了,不过稍微想一下,发现可以剖分的时候遇到关键点就新开链顶,这样可以提前把所有重链的矩阵全部提前算出来,跑的时候根本不会产生任何修改。再想到,因为虚树上边的美好性质,我其实根本没必要重剖去 DDP,完全可以直接算出一条边对应的路径内部子树转移 对应的矩阵去转移。如果能快速预处理的话后续做是个裸的 \(\mathcal{O}(k)\)。那么这个事情其实有一定细节,也会带来一定实现难度。
第一次写写崩了,由于一些地方欠考虑,越写越多顾忌,这不想动那也不想改,心里好纠结。然后直接全删了重构,写起来真得很顺,把所有独立的东西分开,封装 namespace 并直接缩起来,眼睛看不到是真的会感觉舒服的。写代码眼前简洁又能使身心愉悦,以后要写一大坨东西就得这么干。
Code
// STOOOOOOOOOOOOOOOOOOOOOOOOO hzt CCCCCCCCCCCCCCCCCCCCCCCORZ
#include <algorithm>
#include <cassert>
#include <iostream>
#include <numeric>
#include <vector>
using namespace std;
using LL = long long;
using PII = pair<int, int>;
constexpr int kN = 1e5 + 15, kP = 998244353;
inline int Inv(int a) {
int ret = 1, b = kP - 2;
for (; b > 0; b /= 2) {
if (b % 2 == 1)
ret = 1ll * ret * a % kP;
a = 1ll * a * a % kP;
}
return ret;
}
int n, m, M;
struct Node {
int fa;
vector<int> e;
} t[kN];
int sub[kN][2]; // Answer for Full SubTree i (0/1)
vector<PII> nt; // Not on Tree
namespace Init {
vector<PII> ve[kN];
bool ont[kN];
int dep[kN], hson[kN], sz[kN];
void Generate(int x, int fa) {
sz[x] = 1, t[x].fa = fa;
dep[x] = dep[fa] + 1;
sub[x][0] = sub[x][1] = 1;
for (auto [v, id] : ve[x]) {
if (sz[v] != 0)
continue;
ont[id] = 1;
Generate(v, x), sz[x] += sz[v];
sz[v] > sz[hson[x]] && (hson[x] = v);
sub[x][0] = 1ll * sub[x][0] * (sub[v][0] + sub[v][1]) % kP;
sub[x][1] = 1ll * sub[x][1] * sub[v][0] % kP;
}
}
int dfn[kN], dfc, top[kN];
void Dissect(int x, int tp) {
dfn[x] = ++dfc, top[x] = tp;
hson[x] != 0 && (Dissect(hson[x], tp), 0);
for (auto v : t[x].e) {
if (v != t[x].fa && v != hson[x])
Dissect(v, v);
}
}
inline int LCA(int x, int y) {
for (; top[x] != top[y]; x = t[top[x]].fa) {
if (dep[top[x]] < dep[top[y]])
swap(x, y);
}
return dep[x] < dep[y] ? x : y;
}
PII edge[kN];
inline void main() {
for (int i = 1, u, v; i <= m; i++) {
cin >> u >> v, edge[i] = {u, v};
ve[u].emplace_back(v, i), ve[v].emplace_back(u, i);
}
Generate(1, 0);
for (int i = 1; i <= m; i++) {
if (!ont[i]) {
nt.push_back(edge[i]);
continue;
}
auto [u, v] = edge[i];
t[u].e.push_back(v), t[v].e.push_back(u);
}
Dissect(1, 1);
}
inline bool Comp(int x, int y) { return dfn[x] < dfn[y]; }
}
using Init::LCA, Init::Comp;
vector<int> V;
int fa[kN];
struct Pair {
int v0, v1;
inline Pair operator+(const Pair &x) const { return {(v0 + x.v0) % kP, (v1 + x.v1) % kP}; }
inline Pair operator*(int w) { return {1ll * v0 * w % kP, 1ll * v1 * w % kP}; }
} g[kN][2];
inline void Link(int x, int Fa) {
fa[x] = Fa;
Pair cur[2] = {{1, 0}, {0, 1}};
int i = x, p = t[i].fa;
for (; p != Fa; i = p, p = t[p].fa) {
swap(cur[0], cur[1]), cur[0] = cur[0] + cur[1];
for (auto v : t[p].e)
if (v != i && v != t[p].fa) {
cur[0] = cur[0] * (sub[v][0] + sub[v][1]);
cur[1] = cur[1] * sub[v][0];
}
}
sub[Fa][0] = 1ll * sub[Fa][0] * Inv(sub[i][0] + sub[i][1]) % kP;
sub[Fa][1] = 1ll * sub[Fa][1] * Inv(sub[i][0]) % kP;
for (int o : {0, 1})
g[x][o] = cur[o];
// cerr << '\n';
}
inline void Build() { // Build Virtual Tree
V.push_back(1);
sort(V.begin(), V.end(), Comp);
for (int i = 0, sz = V.size(); i < sz - 1; i++)
V.push_back(LCA(V[i], V[i + 1]));
sort(V.begin(), V.end(), Comp);
V.erase(unique(V.begin(), V.end()), V.end());
for (int i = 0; i < V.size() - 1; i++)
Link(V[i + 1], LCA(V[i], V[i + 1]));
}
int vis[kN];
inline bool Check(int s) {
for (int x : V)
vis[x] = 0;
bool fail = 0;
for (int i = 0; i < M; i++) {
auto [u, v] = nt[i];
if (s >> i & 1) {
fail |= vis[u] == 2 || vis[v] == 1;
vis[u] = 1, vis[v] = 2;
} else
fail |= vis[u] == 1, vis[u] = 2;
}
return !fail;
}
int f[kN][2];
int main() {
cin.tie(0)->sync_with_stdio(0);
cin >> n >> m;
Init::main();
M = nt.size();
for (auto [u, v] : nt)
V.push_back(u), V.push_back(v);
Build();
int ans = 0, s = 0;
for (int s = 0; s < 1 << M; s++) {
if (!Check(s))
continue;
for (auto x : V) {
f[x][0] = sub[x][0], f[x][1] = sub[x][1];
vis[x] == 1 && (f[x][0] = 0);
vis[x] == 2 && (f[x][1] = 0);
}
for (int i = V.size() - 1; i > 0; i--) {
int x = V[i];
int f0 = (1ll * f[x][0] * g[x][0].v0 + 1ll * f[x][1] * g[x][0].v1) % kP;
int f1 = (1ll * f[x][0] * g[x][1].v0 + 1ll * f[x][1] * g[x][1].v1) % kP;
f[fa[x]][0] = 1ll * f[fa[x]][0] * (f0 + f1) % kP;
f[fa[x]][1] = 1ll * f[fa[x]][1] * f0 % kP;
}
ans = (0ll + ans + f[1][0] + f[1][1]) % kP;
}
cout << ans << '\n';
return 0;
}
F. P5327 语言 (5.5)
首先不难有一个 \(\mathcal{O}(n^3)\) 的做法。考虑重剖然后对于重链区间两两拼成矩形,直接做矩形面积并扫描线即可。比较不带脑子,但是太劣了,寻找更加优秀的做法。
首先发现对于一个点 \(x\),其可达的点一定在 经过 \(x\) 的路径的端点形成的虚树 的边路径或点上。点数即用这些路径端点组成的虚树边长和。至于维护这些路径端点,可以像 Day20C 一样用 set启发式/线段树合并 维护;虚树边长和是和 P3320 寻宝游戏类似的维护 dfn 序列。若使用 multiset 可以简单 \(\log^2\),但直接动态开点权值线段树合并可以做到单 \(\log\)。非常简单好想。实现难度也比不上 毒瘤/hsh 不知道为什么评的难度这么高。
Code
// STOOOOOOOOOOOOOOOOOOOOOOOOO hzt CCCCCCCCCCCCCCCCCCCCCCCORZ
#include <algorithm>
#include <cassert>
#include <iostream>
#include <numeric>
#include <vector>
using namespace std;
using LL = long long;
using PII = pair<int, int>;
constexpr int kN = 1e5 + 1;
int n, m;
vector<int> e[kN];
int id[kN], dfn[kN], dfc, dep[kN], st[20][kN], fa[kN];
inline int High(int x, int y) { return dep[x] < dep[y] ? x : y; }
void Init(int x, int fa) {
dep[x] = dep[fa] + 1;
st[0][dfn[x] = ++dfc] = ::fa[x] = fa;
id[dfn[x]] = x;
for (auto v : e[x])
(v != fa) && (Init(v, x), 0);
}
inline int LCA(int x, int y) {
if (x == y)
return x;
x = dfn[x], y = dfn[y];
x > y && (swap(x, y), 0);
int k = __lg(y - x);
return High(st[k][x + 1], st[k][y - (1 << k) + 1]);
}
inline int Dis(int i, int j) {
i = id[i], j = id[j];
return dep[i] + dep[j] - 2 * dep[LCA(i, j)];
}
struct Path { int x, y; } path[kN];
vector<PII> vp[kN];
LL ans = 0;
int rt[kN], tot;
struct Info {
int cnt, mn, mx;
LL ans;
inline Info operator+(const Info &x) const {
return {cnt + x.cnt, mn, x.mx, ans + x.ans + Dis(mx, x.mn)};
}
inline LL Get() { return ans + Dis(mn, mx); }
} Z = {0, n + 1, -1, 0};
struct Node {
int l, r;
Info f;
} t[200 * kN];
void PushUp(int x) {
if (!t[x].l || t[t[x].l].f.cnt == 0)
return t[x].f = t[t[x].r].f, void();
if (!t[x].r || t[t[x].r].f.cnt == 0)
return t[x].f = t[t[x].l].f, void();
t[x].f = t[t[x].l].f + t[t[x].r].f;
}
int Update(int x, int p, int o, int l = 1, int r = n) {
int ret = ++tot;
if (t[ret] = t[x]; l == r) {
t[ret].f.cnt += o, t[ret].f.ans = 0;
t[ret].f.mn = t[ret].f.mx = l;
(t[ret].f.cnt == 0) && (t[ret].f = Z, 0);
return ret;
}
int mid = (l + r) / 2;
if (p <= mid)
t[ret].l = Update(t[x].l, p, o, l, mid);
else
t[ret].r = Update(t[x].r, p, o, mid + 1, r);
PushUp(ret);
return ret;
}
void Merge(int &x, int y, int l = 1, int r = n) {
if (!x || !y)
return x += y, void();
else if (l == r)
return t[x].f = t[x].f + t[y].f, void();
int mid = (l + r) / 2;
Merge(t[x].l, t[y].l, l, mid);
Merge(t[x].r, t[y].r, mid + 1, r);
PushUp(x);
}
void DP(int x, int fa) {
rt[x] = ++tot, t[tot].f = Z;
for (auto v : e[x]) {
if (v != fa)
DP(v, x), Merge(rt[x], rt[v]);
}
for (auto [id, o] : vp[x]) {
auto [u, v] = path[id];
u = dfn[u], v = dfn[v];
rt[x] = Update(rt[x], u, o);
rt[x] = Update(rt[x], v, o);
}
ans += t[rt[x]].f.Get() / 2;
}
int main() {
#ifndef ONLINE_JUDGE
freopen("input.in", "r", stdin);
freopen("output.out", "w", stdout);
#endif
cin.tie(0)->sync_with_stdio(0);
cin >> n >> m;
for (int i = 1, u, v; i < n; i++) {
cin >> u >> v;
e[u].push_back(v), e[v].push_back(u);
}
Init(1, 0);
for (int d = 1; d < 20; d++) {
for (int i = 1; i + (1 << d) - 1 <= n; i++)
st[d][i] = High(st[d - 1][i], st[d - 1][i + (1 << d - 1)]);
}
for (int i = 1, x, y, lca; i <= m; i++) {
cin >> x >> y, lca = LCA(x, y);
path[i] = {x, y};
vp[x].emplace_back(i, 1), vp[y].emplace_back(i, 1);
vp[lca].emplace_back(i, -1), vp[fa[lca]].emplace_back(i, -1);
}
DP(1, 0);
cout << ans / 2 << '\n';
return 0;
}
Day23D. 铁人三项 (3)
纯宝宝题。考虑到分数和区间 max 强相关,这种最优化一般是两种思路,一种考虑笛卡尔树 极长特殊性质区间;另一种考虑单调栈动态维护后缀最值。两种都可以做,后者更加宝宝。因为要这样单调栈维护,所以首先把询问离线下来。枚举当前询问的右端点,很好维护当前的 \(\max a -\max b+k*len\)。但是要求所有子区间,不过不难发现没有顶到询问右端点的答案其实以前在相同位置计算过,可以直接查询历史最值。
非常简单的题目,没听过任何做法但是下午一过来听到 fzx 说 D 也是宝宝题就会了。考场上不知道在想什么,可能是在对着题目心乱飞,估计放 CSP 这题我就秒掉了。
有笛卡尔树的做法感觉意义不大,复杂度上没法更优,代码难度也高一些,要在线单调栈做法也可以直接主席树维护。话说这个主席树它也是典型的修改多有用时刻少,之前在想 UOJ 693 地铁规划 的时候也有想过类似的东西。我想了一个可能更加节省的方法,就是给每个点打个时间戳,如果和当前一样就不新开了直接修改。感觉看上去会美丽一些。
并没有写在线,离线是真好写。
Code
// STOOOOOOOOOOOOOOOOOOOOOOOOO hzt CCCCCCCCCCCCCCCCCCCCCCCORZ
#include <algorithm>
#include <cassert>
#include <iostream>
#include <numeric>
#include <vector>
using namespace std;
using LL = long long;
using PII = pair<int, int>;
constexpr int kN = 1e5 + 1;
constexpr LL kI = 1e18;
int n, k, q;
LL a[kN], b[kN];
vector<PII> vq[kN];
LL ans[kN];
struct Node {
LL mx, hmx, ad, had;
} t[4 * kN];
inline void PushUp(int x) {
t[x].mx = max(t[2 * x].mx, t[2 * x + 1].mx);
t[x].hmx = max(t[2 * x].hmx, t[2 * x + 1].hmx);
}
inline void PushTag(int x, LL add, LL hadd) {
t[x].had = max(t[x].had, t[x].ad + hadd);
t[x].hmx = max(t[x].hmx, t[x].mx + hadd);
t[x].ad += add, t[x].mx += add;
}
inline void PushDown(int x) {
PushTag(2 * x, t[x].ad, t[x].had);
PushTag(2 * x + 1, t[x].ad, t[x].had);
t[x].ad = t[x].had = 0;
}
void Add(int L, int R, LL w, int x = 1, int l = 1, int r = n) {
if (R < l || r < L)
return;
else if (L <= l && r <= R)
return PushTag(x, w, w);
int mid = (l + r) / 2;
PushDown(x);
Add(L, R, w, 2 * x, l, mid);
Add(L, R, w, 2 * x + 1, mid + 1, r);
PushUp(x);
}
LL Max(int L, int R, int x = 1, int l = 1, int r = n) {
if (R < l || r < L)
return -kI;
else if (L <= l && r <= R)
return t[x].hmx;
int mid = (l + r) / 2;
PushDown(x);
return max(Max(L, R, 2 * x, l, mid), Max(L, R, 2 * x + 1, mid + 1, r));
}
int main() {
cin.tie(0)->sync_with_stdio(0);
cin >> n >> k;
fill_n(t + 1, 4 * n, Node{-kI, -kI, 0, 0});
for (int i = 1; i <= n; i++)
cin >> a[i];
for (int i = 1; i <= n; i++)
cin >> b[i];
cin >> q;
for (int l, r, i = 1; i <= q; i++) {
cin >> l >> r;
vq[r].emplace_back(l, i);
}
vector<int> va = {0}, vb = {0};
for (int i = 1; i <= n; i++) {
for (int sz = vb.size(); sz > 1 && b[vb.back()] < b[i]; vb.pop_back(), sz--)
Add(vb[sz - 2] + 1, vb.back(), -(b[i] - b[vb.back()]));
vb.push_back(i);
for (int sz = va.size(); sz > 1 && a[va.back()] < a[i]; va.pop_back(), sz--)
Add(va[sz - 2] + 1, va.back(), a[i] - a[va.back()]);
va.push_back(i);
Add(i, i, a[i] - b[i] + kI);
Add(1, i, k);
for (auto [l, id] : vq[i])
ans[id] = Max(l, i);
}
for (int i = 1; i <= q; i++)
cout << ans[i] << '\n';
return 0;
}
G. ABC429G Sum of Pow of Mod of Linear (7)
赛场上第一眼觉得大概率是类欧万欧,但是没有互质没有逆元没有各种东西会很难做。但是发现过了一个小时没有一个人过,说明肯定不是这种暴力的题,不然 AI 早薄纱了,也肯定会有人类干飞,说明是有思考价值的。发现一个等比数列求和还是可以倍增,类似快速幂的实现,因此会去尝试找等差数列。首先可以解决掉 \((a,m)\neq 1\),\(n=m\) 是简单的所以也可以让 \(n<m\)。然后观察这个指数变化,一直加 \(a\) 且不超过 \(m\) 可以得到一个等差数列,每次超过 \(m\) 得到一个新的等差数列。然后这些等差数列的首项同样构成一个等差数列……依此类推,但是如果 \(a=m-1\) 会发现这个套一层一层其实根本没有用,缩减速度极慢。但是不难发现可以把 \(a\) 变成 \(-a\),提前加好倒着做,这样可以保证 \(a\leq m/2\)。然后赛场上就不会了,观看 standing.jpg
赛后发现是惊人结论:集合 \((ai+b)\mod m, i\in[0, n)\) 可以被划分为 \(\mathcal{O}(\sqrt m)\) 个等差数列。
因为你发现不同的余数最多只有 \(m\) 个,所以拉出 \(x\) 个数,很明显最小差可以小到 \(m/x\)。因此可以把这个最小差的数对拿出来对 \(i\) 作差,这样一定可以得到一个 \(d=ak\mod m \leq m/x\) 且 \(0\leq k<x\)(\(k<0\) 可以取负)。此时我得到了一个 位置上公差为 \(k\),值域上公差为 \(d\) 的美丽等差数列。由于 \(n,m\) 同阶,令 \(x=\sqrt m\),就可以每次枚举当前位置余数 \(pd+r\) 的 \(r\),每次拉出一个长度为 \(m/d\) 的等差数列直到 当前位置超过 \(n\) 即可。由于除了每个余数最后一次可能会浪费,其他的等差数列一定长度至少为 \(m/d\),所以数量应该是不超过 \(2\sqrt m\) 的。
太厉害了,再也不用担心不记得类欧万欧怎么做了,至少有一个 根号log 做法能用/qiang ABC 干的就是推广!
Code
// STOOOOOOOOOOOOOOOOOOOOOOOOO hzt CCCCCCCCCCCCCCCCCCCCCCCORZ
#include <algorithm>
#include <cmath>
#include <cassert>
#include <iostream>
#include <numeric>
#include <vector>
using namespace std;
using LL = long long;
using PII = pair<int, int>;
int T, n, m, a, b, x, mod;
inline int Pow(int a, int b) {
int ret = 1;
for (; b > 0; b /= 2) {
if (b % 2 == 1)
ret = 1ll * ret * a % mod;
a = 1ll * a * a % mod;
}
return ret;
}
// \sum\limits_{i=0}^{n-1} x^{ai+b}
inline int Calc(int x, int a, int b, int n) {
int pw = Pow(x, a), cur = 1, ret = 0;
for (; n > 0; n /= 2) {
if (n % 2 == 1)
ret = (1ll * ret * pw + cur) % mod;
cur = cur * (pw + 1ll) % mod;
pw = 1ll * pw * pw % mod;
}
return 1ll * ret * Pow(x, b) % mod;
}
int ans = 0, bl;
PII Get() {
vector<PII> vec;
for (int i = 0; i < bl; i++)
vec.emplace_back((1ll * a * i + b) % m, i);
sort(vec.begin(), vec.end());
for (int i = 1; i < bl; i++) {
int dw = vec[i].first - vec[i - 1].first;
if (dw > bl)
continue;
int di = vec[i].second - vec[i - 1].second;
if (di < 0) {
b = (a * (n - 1ll) + b) % m;
a = m - a, di *= -1;
}
return {di, dw};
}
assert(0);
}
int main() {
#ifndef ONLINE_JUDGE
freopen("input.in", "r", stdin);
freopen("output.out", "w", stdout);
#endif
cin.tie(0)->sync_with_stdio(0);
for (cin >> T; T--;) {
cin >> n >> m >> a >> b >> x >> mod, ans = 0;
if (n >= m) {
int g = __gcd(a, m);
ans = 1ll * Calc(x, g, b % g, m / g) * g % mod * (n / m) % mod;
n %= m;
}
bl = sqrt(m) + 1.01;
if (n <= bl) {
for (int i = 0; i < n; i++)
ans = (ans + Pow(x, (1ll * a * i + b) % m)) % mod;
cout << ans << '\n';
continue;
}
auto [di, dw] = Get();
for (int r = 0; r < di; r++)
for (int i = r; i < n;) {
int first = (1ll * a * i + b) % m;
int len = min(dw ? (m - 1 - first) / dw + 1 : int(1e9), (n - 1 - i) / di + 1);
ans = (ans + Calc(x, dw, first, len)) % mod;
i += len * di;
}
cout << ans << '\n';
}
return 0;
}

浙公网安备 33010602011771号