QOJ8557 Goldberg Machine 题解
QOJ8557 Goldberg Machine 题解
QOJ #. 8557, Petrozavodsk Summer 2019. Day 6. MIPT Contest C
分块,代码实现 [黑]
联考搬了这题。首先思路就不是很好想。(至少我一开始完全没有任何思路,不知道怎么刻画问题)模拟赛的题解非常意识流,我破译了很久才逐渐搞懂,途中得感谢搬题人 oyzr 对我问题的解答。代码也很难写,细节非常多,我断断续续写了三天,写了 generator 拍出了各种错误才过掉,总共写下的代码可能有 15k,最终的 AC 代码也有 5.7k。(为了方便调试,我把 generator 放在了文末。)
静态问题
先想想如何刻画移动的规律。规定树的根节点为 \(1\),考虑一种特殊情况:除了根节点以为每个节点连向父亲的边都最后被激活。称从根节点出发又回到根节点,且根节点出边激活状态和初始相同的过程为“一轮”,那么每一轮都会访问全部节点,且访问节点的顺序就是欧拉序。
对于一般的情况,如果节点 \(u\) 连向父节点的边不是最后被激活的,那么在第一次访问到 \(u\) 的这一轮中,只有一部分 \(u\) 的子节点(可能是 \(0\) 个)会被访问,然后就会回到父节点。在这一轮之后又访问到 \(u\) 时,由于前一轮结束后 \(u\) 连向父节点的边被激活,所以在本轮中,一定可以访问 \(u\) 的所有子节点,最后再回到父节点。也就是说,经过有限轮之后,每个节点都会满足:在一轮中父节点最后被访问,这就变成了上文所说的特殊情况,称为终态。
继续考虑在这种情况发生之前的规律。可以发现:每一轮访问的节点是包含根节点的一个连通块,并且在到达终态前,每一轮访问的连通块大小都会增加。记 \(f_u\) 表示节点 \(u\) 在第几轮第一次被访问。根据上文,对于节点 \(u\),一部分子节点和 \(u\) 在同一轮被第一次访问,另一部分子节点在下一轮被首次访问,因此 \(f_{u}\) 可以看作:设一些边的边权为 \(1\),另一些为 \(0\),\(u\) 到根节点的距离。
进一步,每一轮被访问的节点序列是欧拉序的子序列,且后一轮访问的子序列一定包含前一轮。考虑求出欧拉序中每个点在第几轮首次被访问。记 \(g_{i}\)(\(0 \le i < 2n - 1\)) 表示欧拉序中第 \(i\) 个点在第几轮被首次访问,这容易根据 \(f\) 数组求出。
然后考虑怎么得到答案。容易想到先二分求出轮数,再二分求出是轮内的哪个点。记 \(cnt(x)\) 表示 \(g\) 中 \(\le x\) 的值个数,为了快速求出前 \(x\) 轮会操作多少次,需要在 \(cnt\) 数组的前缀和数组上二分。设已经求出待求的操作在第 \(p\) 轮中,为了求出是在第 \(p\) 轮中的哪个点,需要快速求出 \(g\) 中某一段区间内 \(\le x\) 的数的个数。可以离线以后在线段树上二分。我为了方便写了树状数组+二分,多了一个 \(\log\) 但可以过。这个问题也可以用分块解决,见下文。
Code
// QOJ8557. Goldberg Machine
// works for the static problem
#include<bits/stdc++.h>
#define debug(...) fprintf(stderr, __VA_ARGS__)
using namespace std;
typedef long long i64;
struct BIT {
vector<int> c;
int n;
int lb(int x) { return x & -x; }
void init(int _n) {
n = _n; c.resize(n + 1);
}
void change(int pos, int x) {
for(; pos <= n; pos += lb(pos)) {
c[pos] += x;
}
}
int query(int pos) {
int res = 0;
for(; pos; pos -= lb(pos)) {
res += c[pos];
}
return res;
}
int query(int L, int R) {
return (L > R) ? 0 : (query(R) - query(L - 1));
}
}tr;
struct Query {
i64 x; int id;
bool operator < (const Query &rhs) const {
return x < rhs.x;
}
};
int n;
vector<vector<int>> G;
vector<int> deg, ptr, fa;
vector<int> ord, f, g;
// ord: 欧拉序
// f[i]: 第i个点第一次被访问时的轮数
// g[i]: 欧拉序中第i个点从第几轮开始被访问
vector<i64> sum;
vector<vector<int>> h;
void dfs(int u) {
for(int v: G[u]) {
if(v != fa[u]) {
fa[v] = u;
dfs(v);
}
}
}
int euler = 0;
void dfs2(int u, int dep) {
ord.push_back(u), f[u] = dep;
g[euler++] = f[u], sum[f[u]]++;
for(int i = 0; i < deg[u] - (u != 1); i++) {
int v = G[u][i];
dfs2(v, dep + (u != 1 && i < ptr[u]));
ord.push_back(u);
g[euler++] = f[v], sum[f[v]]++;
}
}
int main() {
cin.tie(nullptr) -> sync_with_stdio(false);
cin >> n;
G.resize(n + 1), ptr.resize(n + 1), deg.resize(n + 1);
for(int i = 1; i <= n; i++) {
cin >> deg[i] >> ptr[i];
for(int j = 1, x; j <= deg[i]; j++) {
cin >> x;
G[i].push_back(x);
}
}
fa.resize(n + 1);
dfs(1);
rotate(G[1].begin(), G[1].begin() + ptr[1], G[1].end());
for(int i = 2; i <= n; i++) {
auto it = find(G[i].begin(), G[i].end(), fa[i]);
int old = G[i][ptr[i]];
rotate(G[i].begin(), next(it), G[i].end());
ptr[i] = (int)(find(G[i].begin(), G[i].end(), old) - G[i].begin());
for(int j = 0; j < deg[i]; j++) {
if(j == ptr[i]) {
} else {
}
}
}
f.resize(n + 1), g.resize(2 * n - 1), sum.resize(n + 1), dfs2(1, 1);
int w = *max_element(g.begin(), g.end());
sum[1]--, sum.resize(w + 1);
partial_sum(sum.begin(), sum.end(), sum.begin());
partial_sum(sum.begin(), sum.end(), sum.begin());
h.resize(w + 1);
for(int i = 1; i < 2 * n - 1; i++) {
h[g[i]].push_back(i);
}
int q;
cin >> q;
vector<Query> qry(q + 1);
for(int i = 1; i <= q; i++) {
char op;
cin >> op >> qry[i].x;
qry[i].id = i;
}
sort(qry.begin() + 1, qry.end());
tr.init(2 * n - 1);
int now = 0;
vector<int> ans(q + 1);
for(int i = 1; i <= q; i++) {
i64 x = qry[i].x;
// 二分在第几轮
auto it = lower_bound(sum.begin() + 1, sum.end(), x);
if(it == sum.end()) {
x -= sum[w];
// cerr << "x = " << x << '\n';
int res = (int)ord[x % (2 * n - 2)];
ans[qry[i].id] = res;
continue;
}
// 在这一轮内二分
int y = (int)(it - sum.begin());
while(now < y) {
now++;
for(int u: h[now]) {
tr.change(u, 1);
}
}
int z = (int)(x - sum[y - 1]);
int lo = 1, hi = 2 * n - 2, p = -1;
while(lo <= hi) {
int mid = (lo + hi) >> 1;
int cnt = tr.query(mid);
if(cnt <= z) {
p = mid, lo = mid + 1;
} else {
hi = mid - 1;
}
}
int res = ord[p];
ans[qry[i].id] = res;
}
for(int i = 1; i <= q; i++) {
cout << ans[i] << '\n';
}
return 0;
}
带修
考虑修改操作,也就是说我们要刻画修改某个节点 \(u\) 的 \(t_{u}\) 对 \(g\) 数组的影响。修改 \(t_{u}\) 相当于把 \(u\) 连向某些子节点的边的边权从 \(0\) 改成 \(1\),或从 \(1\) 改成 \(0\),修改的子节点在按输入顺序形成一段区间。不妨假设是把 \(0\) 改成 \(1\),另一种情况类似。考虑对 \(g\) 的影响,这相当于把这些子节点的子树中所有点首次被访问的轮数后移了一轮,体现到 \(g\) 上是一段区间 \(+1\)。预处理每个节点 \(u\) 的所有子节点在欧拉序中第一次出现的位置,以及从每个子节点返回到 \(u\) 时,\(u\) 在欧拉序中的位置,就可以确定区间修改的范围。
对于询问,由于 \(g\) 会改变,难以维护 \(cnt\) 数组,不能像静态问题那样在 \(cnt\) 数组上二分。解决方法并不难:换一个角度,前 \(x\) 轮操作的次数是
只要指导 \(g\) 中 \(\le x\) 的数的个数以及和就能求出上式。
现在我们只需考虑这样的数据结构问题:给定数组 \(g\),支持区间 \(+1/-1\),查询全局 \(\le x\) 的数的个数以及 \(\le x\) 的数之和。
这个问题的修改操作基于位置,而查询操作基于值域,用线段树不太好维护,考虑分块。
对每一块,维护 \(\le\) 某个数的个数以及和。显然 \(f\) 数组相邻元素的差值不超过 \(1\),由此可以推出 \(g\) 中相邻元素的差值不超过 \(1\),所以每块不同元素的个数不会超过块长 \(B\)。因此可以记录每块中 \(\le\) 某个数的个数及和,整块修改打 \(\operatorname{tag}\),散块暴力重构。那么修改操作的时间复杂度为 \(O(B + \frac{n}{B})\),查询操作中,求出在哪一轮是 \(O(B\log n)\),求出是轮内哪个点是 \(O(B + \frac{n}{B})\)(注意只要枚举每个块找就行了)。视 \(n\) 和 \(q\) 同阶,则总时间复杂度为 \(O(n(\frac{n}{B}\log n + B))\),根据基本不等式,当 \(B = \sqrt{n \log n}\) 时时间复杂度有最小值 \(O(n\sqrt{n \log n})\)。实际上取 \(B = \frac{1}{2}\sqrt{n \log n}\) 时更快。(借鉴了 oyzr 的代码)
AC Code
// QOJ8557. Goldberg Machine
#include<bits/stdc++.h>
using namespace std;
typedef long long i64;
int n;
vector<vector<int>> G;
vector<int> deg, ptr, fa;
vector<int> ord, f, g;
// ord: 欧拉序
// f[i]: 第i个点第一次被访问时的轮数
// g[i]: 欧拉序中第i个点从第几轮开始被访问
vector<vector<int>> rk1, rk2;
void dfs(int u) {
for(int v: G[u]) {
if(v != fa[u]) {
fa[v] = u;
dfs(v);
}
}
}
int euler = 0;
void dfs2(int u, int dep) {
ord.push_back(u), f[u] = dep;
g[euler++] = f[u];
for(int i = 0; i < deg[u] - (u != 1); i++) {
int v = G[u][i];
rk1[u][i] = euler;
dfs2(v, dep + (u != 1 && i < ptr[u]));
rk2[u][i] = euler;
ord.push_back(u);
g[euler++] = f[v];
}
}
namespace DS {
// 分块
// 维护 <= x 的数的个数以及 <= x 的数之和
int N, bsize, bcnt;
vector<int> tag, mn, mx;
vector<vector<int>> cnt;
vector<vector<i64>> sum;
#define bl(x) max(1, bsize * (x))
#define br(x) min(N, bsize * ((x) + 1) - 1)
void build(int id) {
for(int i = bl(id); i <= br(id); i++) {
g[i] += tag[id];
}
tag[id] = 0;
mn[id] = *min_element(g.begin() + bl(id), g.begin() + br(id) + 1);
mx[id] = *max_element(g.begin() + bl(id), g.begin() + br(id) + 1);
int w = mx[id] - mn[id] + 1;
cnt[id].assign(w, 0), sum[id].assign(w, 0);
for(int i = bl(id); i <= br(id); i++) {
cnt[id][g[i] - mn[id]]++;
sum[id][g[i] - mn[id]] += g[i];
}
partial_sum(cnt[id].begin(), cnt[id].end(), cnt[id].begin());
partial_sum(sum[id].begin(), sum[id].end(), sum[id].begin());
}
void init() {
N = 2 * n - 2;
bsize = max(1, (int)sqrt((double)(N * __lg(N))) / 2);
bcnt = N / bsize + 1;
tag.resize(bcnt), mn = mx = tag;
cnt.resize(bcnt), sum.resize(bcnt);
for(int i = 0; i < bcnt; i++) {
build(i);
}
}
void change(int l, int r, int x) {
int lid = l / bsize, rid = r / bsize;
for(int i = lid + 1; i < rid; i++) {
tag[i] += x;
}
for(int i = l; i <= min(r, br(lid)); i++) {
g[i] += x;
}
build(lid);
if(lid != rid) {
for(int i = bl(rid); i <= r; i++) {
g[i] += x;
}
build(rid);
}
}
pair<int, i64> query(int x) {
// 返回 <= x 的数的个数以及 <= x 的数之和
int res_cnt = 0;
i64 res_sum = 0;
for(int i = 0; i < bcnt; i++) {
if(mn[i] + tag[i] <= x) {
int y = min(x - tag[i] - mn[i], mx[i] - mn[i]);
res_cnt += cnt[i][y];
res_sum += sum[i][y] + tag[i] * cnt[i][y];
}
}
return make_pair(res_cnt, res_sum);
}
int find(int x, int k) {
// 返回第 x 轮中进行 k 次操作以后到达的点
for(int i = 0; i < bcnt; i++) {
if(mn[i] + tag[i] > x) continue;
int y = min(x - tag[i] - mn[i], mx[i] - mn[i]);
if(cnt[i][y] < k) {
k -= cnt[i][y];
} else {
for(int j = bl(i); j <= br(i); j++) {
if(g[j] + tag[i] <= x) k--;
if(k == 0) return ord[j];
}
}
}
throw logic_error("Error in DS::find: k-th not found.\n");
}
}
i64 query(int x) {
// 返回前 x 轮中总共操作了多少次
auto [cnt, sum] = DS::query(x);
return 1LL * cnt * (x + 1) - sum;
}
int main() {
cin.tie(nullptr) -> sync_with_stdio(false);
cin >> n;
G.resize(n + 1), ptr.resize(n + 1), deg.resize(n + 1);
for(int i = 1; i <= n; i++) {
cin >> deg[i] >> ptr[i];
for(int j = 0, x; j < deg[i]; j++) {
cin >> x;
G[i].push_back(x);
}
}
fa.resize(n + 1), dfs(1);
vector<int> offset(n + 1);
rotate(G[1].begin(), G[1].begin() + ptr[1], G[1].end()), offset[1] = -ptr[1], ptr[1] = 0;
for(int i = 2; i <= n; i++) {
auto it = find(G[i].begin(), G[i].end(), fa[i]);
int old = G[i][ptr[i]];
rotate(G[i].begin(), next(it), G[i].end());
int pos = (int)(find(G[i].begin(), G[i].end(), old) - G[i].begin());
offset[i] = pos - ptr[i], ptr[i] = pos;
}
rk1.resize(n + 1), rk2.resize(n + 1);
for(int i = 1; i <= n; i++) {
rk1[i].resize(deg[i]), rk2[i].resize(deg[i]);
}
f.resize(n + 1), g.resize(2 * n - 1), dfs2(1, 1);
DS::init();
int q;
cin >> q;
for(int i = 1; i <= q; i++) {
char op;
cin >> op;
if(op == 'C') {
int u, x;
cin >> u >> x;
x = (x + offset[u] + deg[u]) % deg[u];
if(x < ptr[u]) {
DS::change(rk1[u][x], rk2[u][ptr[u] - 1], -1);
} else if(x > ptr[u]) {
DS::change(rk1[u][ptr[u]], rk2[u][x - 1], 1);
}
ptr[u] = x;
} else {
i64 x;
cin >> x;
int w = 0;
for(int j = 0; j < DS::bcnt; j++) {
w = max(DS::mx[j] + DS::tag[j], w);
}
// 查询在哪一轮中
int L = 1, R = w, p = - 1;
while(L <= R) {
int mid = (L + R) >> 1;
i64 cnt = query(mid);
if(x <= cnt) {
p = mid, R = mid - 1;
} else {
L = mid + 1;
}
}
if(p == -1) {
x -= query(w);
cout << ord[x % (2 * n - 2)] << '\n';
continue;
}
// 在这一轮内二分
x -= query(p - 1);
int ans = DS::find(p, (int)x);
cout << ans << '\n';
}
}
return 0;
}
generator
#include<bits/stdc++.h>
using namespace std;
constexpr int n = 10, q = 5;
// mt19937 rng;
mt19937 rng((int)chrono::system_clock::now().time_since_epoch().count());
uniform_int_distribution<int> dis(1, n), disx(1, 20), dis_op(0, 1);
int main() {
cin.tie(nullptr) -> sync_with_stdio(false);
cout << n << '\n';
vector<vector<int>> G(n + 1);
for(int i = 2; i <= n; i++) {
uniform_int_distribution<> dis_fa(1, i - 1);
int fa = dis_fa(rng);
G[fa].push_back(i), G[i].push_back(fa);
}
vector<int> deg(n + 1);
for(int i = 1; i <= n; i++) {
deg[i] = (int)G[i].size();
uniform_int_distribution<> dis_p(0, deg[i] - 1);
int p = dis_p(rng);
cout << deg[i] << ' ' << p;
shuffle(G[i].begin(), G[i].end(), rng);
for(int j: G[i]) {
cout << ' ' << j;
}
cout << '\n';
}
cout << q << '\n';
for(int i = 1; i <= q; i++) {
int t = dis_op(rng);
if(t == 1) {
int x = disx(rng);
cout << "Q " << x << '\n';
} else {
int u = dis(rng);
uniform_int_distribution<> dis_deg(0, deg[u] - 1);
int x = dis_deg(rng);
cout << "C " << u << ' ' << x << '\n';
}
}
return 0;
}