T2 - 玩具质检
题目描述
小 Z 现在成为了玩具厂的厂长,为了保证出厂玩具的质量,他决定对这些玩具进行抽样检查。
具体来说,玩具厂生产的这批玩具个数为 \(n\),编号为 \(1,2,\cdots,n\)。小 Z 进行了若干次抽查,其中有 \(m\) 次抽查,第 \(i\) 次检查了所有编号在集合 \(S_i\) 中的玩具,并且小 Z 发现这些玩具中有至少一个是劣质的。
检查生产线之后小 Z 发现每个玩具在生产的时候都有 \(\dfrac{1}{2}\) 的概率质量过关,\(\dfrac{1}{2}\) 的概率质量不过关,且各个玩具互相独立。
现在小 Z 设 \(P_i\) 为在已知这 \(m\) 次抽查有问题的条件下,第 \(i\) 个玩具是劣质的概率。为了安排具体的复查流程,他希望你能将所有玩具按 \(P_i\) 从小到大排列,如果有两个玩具 \(i,j\) 的 \(P_i,P_j\) 相同,则按照编号从小到大排列,请你输出这个排列。
Solution
等价于求在钦定 \(i\) 是坏的前提下,有多少种可能的局面(玩具好坏情况)使得 \(m\) 个条件都成立的方案数 \(ans_i\)。按 \((ans_i, i)\) 从小到大排序。
除了至少/刚好的转化,快来复习一下最最最基本的容斥吧!
给定一些条件和对象,满足所有条件的对象数量 = 所有对象(至少零个不满足的对象) - 至少一个不满足的对象数量 + 至少两个不满足的对象数量 + \(\cdots\)
那么,当我们钦定 \(i\) 坏掉的情况下,包含 \(i\) 的条件 \(P_i\) 一定全部满足了。所以不用管他们。那么我们也就是需要全集 \(U\) 减去 \(P_i\) 的部分全部满足。套用刚刚的容斥,这里我们的对象就是一个表示玩具好坏情况的 01 串。至少集合 \(T\) 中的条件全部不满足的情况数是,\(2^{n - 1 - |\cup_{x \in T} S_x|}\),也就是集合内的所有玩具和 \(i\) 都得坏掉,其余随意。那么答案就是
再从另一个角度理解下,对于一个不满足 \(Q\) 中条件的 01 串。他会贡献 \(\sum_{W \subset Q} (-1)^{|W|} = [|Q| = 0]\) 次。
然后这个贡献的数都是二的幂次。我们二进制高精度,把 -1/1 的分开加,此时只用先单点加,最后在统一进位再相减。这样复杂度就是 \(O(n(2^m + n))\),最后还有排序是 \(O(n^2 \log n)\)。
Code
#include <bits/stdc++.h>
using namespace std;
const int N = 2e3 + 5, M = 16;
bitset<N> s[1 << M];
int lb(int x){ return x & (-x); }
int n, m, t[N], pop[1 << M], num[1 << M];
struct BI{int a[2 * N];BI(){ memset(a, 0, sizeof(a)); }void add(int x){ a[x]++; }void cal(){for(int i = 0; i <= 2 * n; ++i){a[i + 1] += a[i] / 2;a[i] %= 2;}}void operator -= (const BI &b){for(int i = 0; i <= 2 * n; ++i){a[i] -= b.a[i];if(a[i] < 0) a[i] += 2, a[i + 1]--;}}bool operator < (const BI &b) const {for(int i = n; i >= 0; --i){if(a[i] != b.a[i]) return a[i] < b.a[i];}return 0;}
}ans[N];
pair<BI, int> a[N];
int main(){cin.tie(nullptr)->sync_with_stdio(0);cin >> n >> m;for(int i = 0; i < m; ++i){int k; cin >> k;for(int j = 1; j <= k; ++j){int p; cin >> p;t[p] |= (1 << i);s[1 << i][p] = 1;}}for(int i = 1; i < (1 << m); ++i){int x = lb(i);pop[i] = pop[i ^ x] + 1;if(pop[i] == 1){num[i] = s[i].count();continue;}s[i] = s[i ^ x] | s[x];num[i] = s[i].count();}int D = (1 << m) - 1;for(int i = 1; i <= n; ++i){int S = D ^ t[i];BI sub;for(int T = S; ; T = (T - 1) & S){if(pop[T] & 1) sub.add(n - 1 - num[T]);else ans[i].add(n - 1 - num[T]);if(!T) break;}ans[i].cal(), sub.cal();ans[i] -= sub;}for(int i = 1; i <= n; ++i) a[i] = {ans[i], i};sort(a + 1, a + 1 + n);for(int i = 1; i <= n; ++i) cout << a[i].second << ' ';return 0;
}
T3 - 简单树上求和问题
题目描述
小 Z 写不出优美的题目背景了 /fad。
给出一棵 \(n\) 个点的有根树,点有点权 \(a_i\),根为 \(1\) 号节点。
定义 \(U(u,d)\) 为集合 \(\{v|\) \(v\) 在 \(u\) 的子树内且 \(u\) 与 \(v\) 的距离 \(\le d\) \(\}\),其中两个节点之间的距离定义为两节点间最短路径中边的个数。
现在小 Z 给了你 \(q\) 次询问,每次给出 \(u,d\),请你求出:
其中 \(\oplus\) 为按位异或运算。
Solution
考虑 \(x^2\) 的本质是什么?我们拆位,用 \(x_{(2)p}\) 表示 \(x\) 的第 \(p\) 个二进制位。那么就是 \(x ^ 2 = \sum_{x_{(2)p} = 1 \land x_{(2)q} = 1} 2^{p + q}\)。
那么先把询问离线。我们 \(O(\log^2 V)\) 枚举两个二进制位 \(p, q\)。那么也就是要统计 \(U(u, d)\) 内有几个 \({(a_i \oplus a_j)}_{(2)p} = 1 \land {(a_i \oplus a_j)}_{(2)q} = 1\)。这样看起来就好做一些了。我们把 \(a_i,a_j\) 的 \(p, q\) 两位取出来称为 \(c_i, c_j(0 \le c_i, c_j < 4)\),那么要让 \(a_i \oplus a_j\) 的 \(p,q\) 两位都是 1,只需要 \(c_i + c_j = 4\)。
问题变成了统计某个点 \(U(u, d)\) 中有多少个 \(c_i = 0/1/2/3\),统计好后就是 \(c_0c_3 + c_1c_2\) 以 \(2^{p + q}\) 贡献到答案。
接下来让我们先来学习一下处理深度问题的利器长剖吧。
我们定义 \(mxdep_u\) 为 \(u\) 到它子树内最深的叶子的经过的点的数量。也就是子树内“最长的”一个链。那么这里的长儿子就是 \(mxdep_v\) 最大的一个儿子 \(son_u\),不难发现 \(mxdep_u = mxdep_{son_u} + 1\)。
现在我们按照优先长儿子的顺序划分 dfs 序。此时,对于一个点 \(u\),\(dfn_u + 1\) 处的点恰好就是他所在长链上的下一个点,当然它不能是叶子。进一步地,只要 \(d\) 小于 \(mxdep_u\),那么 \(dfn_u + d\) 处恰好就是它所在长链上的 \(u\) 往下第 \(d\) 个点(认为 \(u\) 是第 0 个点)。
现在我们想统计 \(u\) 子树内距离 \(u\) 距离为 \(j\) 的点的一些信息 \(f_{u, j}\),比如是距离 \(u\) 为 \(j\) 的点的数量。如果深度相同的点的信息可以合并,长剖可以做到 \(O(n)\) 的统计。具体来说,我们将 \(f_{u, d}\) 的信息挂到 \(dfn_u + d\) 上。这样我们可以直接继承长儿子 \(v\) 的信息,因为 \(dfn_v\) 正好就是 \(dfn_u + 1\)。而对于别的儿子 \(w\) 的信息,我们把它暴力合并到 \([dfn_u, dfn_u + mxdep_u - 1]\) 上。在这里就是 \(dfn_u + i + 1\) 处加等于 \(dfn_w + i\) 的数量。这个复杂度是 \(O(n)\) 的,因为一条长链只会在它长链的顶端被合并一次,然后所有长链的链长之和显然是 n。
注意我们的这个做法是边做边查的,也就是说,如果 \(u\) 是长儿子,\(u\) 的父亲可能会将 \(u\) 子树外的一些信息合并到 \([dfn_u, dfn_u + mxdep_u - 1]\) 上,此时再查询得到的就不是 \(u\) 的信息了。所以必须加可持久化才能强制在线。
然后我们发现,我们实际上是把信息挂到 dfs 序上做的。所以比方说查询 \(\le d\) 的一些信息,我们可以随便什么数据结构维护一下 \([dfn_u, dfn_u + d]\) 的一些信息。
实际上这个很像 dsu on tree 的功能,姑且就叫长剖 dsu on tree。
在这道题中,我们就是开四颗树状数组分别维护子树内 \(c_i = 0/1/2/3\) 的数量。用上面的长剖 dsu on tree 维护即可。
复杂度 \(O(\log^2V \times (n + q) \log n)\)。
Code
#include <bits/stdc++.h>
using namespace std;
#define int long long
typedef pair<int, int> pii;
const int N = 1.5e5 + 5;
vector<int> e[N];
vector<pii> query[N];
int a[N], ans[N];
int n, q, mxdep[N], son[N], dfn[N], tsp, s, t, coef;
struct BIT{int tr[N], a[N];void clear(){ memset(tr, 0, sizeof(tr)); memset(a, 0, sizeof(a)); }int lb(int x){ return x & (-x); }void add(int x, int k){a[x] += k;for(int i = x; i <= n; i += lb(i))tr[i] += k;}int operator [](int x){ return a[x]; }int qry(int x){int res = 0;for(int i = x; i; i -= lb(i))res += tr[i];return res;}int qry(int l, int r){ return qry(r) - qry(l - 1); }
}tr[4];
void dfs(int u){for(int v : e[u]){dfs(v);if(mxdep[son[u]] < mxdep[v]) son[u] = v;}mxdep[u] = mxdep[son[u]] + 1;
}
void devide(int u){dfn[u] = ++tsp;if(son[u]) devide(son[u]);for(int v : e[u]){if(v != son[u]) devide(v);}
}
void solve(int u){for(int v : e[u]) solve(v);for(int v : e[u]){if(v != son[u]){for(int j : {0, 1, 2, 3}){for(int i = 0; i < mxdep[v]; ++i){tr[j].add(dfn[u] + i + 1, tr[j][dfn[v] + i]);}}}}int mask = bool(a[u] & s) * 2 + bool(a[u] & t);tr[mask].add(dfn[u], 1);for(auto [d, idx] : query[u]){d = min(d, mxdep[u] - 1);for(int j : {0, 1}){ans[idx] += coef * (tr[j].qry(dfn[u], dfn[u] + d) * tr[3 - j].qry(dfn[u], dfn[u] + d));}}
}
signed main(){cin.tie(0)->sync_with_stdio(0);cin >> n >> q;for(int i = 1; i <= n; ++i) cin >> a[i];for(int i = 2; i <= n; ++i){int p; cin >> p;e[p].push_back(i);}dfs(1);devide(1);for(int i = 1; i <= q; ++i){int u, d; cin >> u >> d;query[u].push_back({d, i});}for(t = 1; t < 1024; t *= 2){for(s = t; s < 1024; s *= 2){for(int j : {0, 1, 2, 3}) tr[j].clear();coef = (s == t ? s * t : s * t * 2);solve(1);}}for(int i = 1; i <= q; ++i) cout << ans[i] << '\n';return 0;
}
T4 - 逆序对查询
题目描述
给出一个长为 \(n\) 的序列。
一开始所有子区间都是解锁的。接下来 \(n\) 次每次删除一个位置 \(p_i\),那么所有 \(l \le p_i \le r\) 的区间都会被封印。每次删除前,问解锁的子区间中逆序对个数最大的那个区间有多少个逆序对。
强制在线。
Solution
先考虑加点。我们每次加入一个点 \(p\) 时,假设其将左边的一个区间 \(L\) 和右边的一个区间 \(R\) 合并起来了。那么此时新区间 \(L \cup {p} \cup R\) 的逆序对数由三部分构成:
- \(p\) 的贡献。即 \(L\) 中有几个数 \(>a_p\) + \(R\) 中有几个数 \(<a_p\)。
- \(L,R\) 内部的贡献,已经计算出来了,直接加。
- \(L,R\) 之间的贡献。考虑启发式合并。遍历较短的那个区间的每个数进行求解。
貌似差不多就做完了。因为把启发式合并变成启发式分裂就可以满足强制在线了。具体来说,当我们把大区间分裂为 \(L, p, R\) 时。
- \(p\) 的贡献,直接算。
- 较短的那个区间内部的贡献,遍历一遍算。
- \(L, R\) 之间的贡献,遍历较短的那边暴力算。
而对于较长的区间的贡献,直接从总的减掉上面这三部分就好。
启发式分裂完全就是启发式合并的逆过程,因此复杂度都是 \(O(n \log^2n)\)。查贡献的部分主席树即可,用一个 ODT 维护分裂的线段,然后在来一个 multiset 维护答案即可。
Code
#include <bits/stdc++.h>
using namespace std;
#define int long long
const int N = 3e5 + 5;
int n, o, a[N];
struct node{int ls, rs, siz;
}tr[N << 5];
#define ls(p) tr[p].ls
#define rs(p) tr[p].rs
#define siz(p) tr[p].siz
int rt[N], tot;
void pushup(int p){ siz(p) = siz(ls(p)) + siz(rs(p)); }
void upd(int x, int &p, int pre, int pl = 1, int pr = n + 1){p = ++tot;tr[p] = tr[pre];if(pl == pr) return siz(p)++, void();int mid = (pl + pr) >> 1;if(x <= mid) upd(x, ls(p), ls(pre), pl, mid);else upd(x, rs(p), rs(pre), mid + 1, pr);pushup(p);
}
int qry(int x, int y, int k, int pl = 1, int pr = n + 1){ // num < kif(pl == pr) return 0;int mid = (pl + pr) >> 1;if(k <= mid) return qry(ls(x), ls(y), k, pl, mid);return siz(ls(y)) - siz(ls(x)) + qry(rs(x), rs(y), k, mid + 1, pr);
}
int ask(int L, int R, int k){ if(L > R) return 0;return qry(rt[L - 1], rt[R], k);
}
struct rge{int l, r, v;bool operator < (const rge &b) const {return l < b.l;}
};
set<rge> s;
multiset<int, greater<int> > ans;
void split(int p){auto it = s.upper_bound({p, 0, 0});if(it == s.begin()) return;--it;auto [l, r, v] = *it;if(p < l || p > r) return;auto o = ans.find(v); ans.erase(o);v -= p - l - ask(l, p - 1, a[p] + 1) + ask(p + 1, r, a[p]);int vl = 0, vr = 0;if(p - l <= r - p){for(int i = l; i < p; ++i){vl += ask(i + 1, p - 1, a[i]);v -= ask(p + 1, r, a[i]);}vr = v - vl;}else{for(int i = p + 1; i <= r; ++i){vr += ask(i + 1, r, a[i]);v -= p - l - ask(l, p - 1, a[i] + 1);}vl = v - vr;}s.erase(it);if(l < p) s.insert({l, p - 1, vl}), ans.insert(vl);if(p < r) s.insert({p + 1, r, vr}), ans.insert(vr);
}
signed main(){cin.tie(nullptr)->sync_with_stdio(0);cin >> n >> o;for(int i = 1; i <= n; ++i){cin >> a[i];upd(a[i], rt[i], rt[i - 1]);}int lst = 0, v = 0;for(int i = 1; i < n; ++i){v += ask(i + 1, n, a[i]);}s.insert({1, n, v}); ans.insert(v);for(int i = 1; i <= n; ++i){cout << (lst = *ans.begin()) << ' ';int p; cin >> p;p = (p + o * lst) % n + 1;split(p);}return 0;
}
Summary
打了那么多场了,状态好或者不好的时候都有,谁也不能保证这周六的时候自己状态很好。
开题策略的话,这几场下来先开 T1 总是没错的。但是去年的惨痛教训告诉我,自己不一定能做出 T1 的。
对于后面的题,一档一档往上想。正解想不出来时关注部分分。
Think Twice, Code Once!