11.18
水题,看到数据范围想到状压和矩快,然后想想转移,发现这玩意一定可以线性递推,所以推出来状压的转移就好了。我们可以得到一下方程,其中 \(C\) 表示了一个状态到另一个状态的系数,发现只和并集有关,所以可以预处理出来:
最终答案即为 \(f_{m,2^n-1}\)。
时间复杂度 \(O(8^n\log m)\)。
最短路套上数学,考虑计算怎么经过这个点时间最短,我们把式子拿下来,发现:
这是一个有极值的函数,取到 \(t=\sqrt n-1\) 时最优,所以价值就可以分段给贡献计算了。
赛时 \(0\) 思路,要注意怎么维护答案/把答案转换为可做形式(推式子)。
考虑怎么计算答案,我们可以想到的就是固定 \(lca\) 来计算,接下来我们想办法怎么把答案转化成 \(f(u,d_{lca})\) 的形式。发现我们容斥一下变成
不难发现 \(\sum f(i_1+d_{\operatorname{LCA*}(p_x,i_1)})+\sum f(u+d_{p_x})=s_{p_x}\)。于是 \(s_x=s_{p_x}+\sum f(v+d_x)-\sum f(v+d_x-1)\)。
考虑怎么求出后面的两个东西。本质上,相当于我们每找到一个节点就将 \(v+d_v\) 加入一个可重集,求出可重集内所有数经过 \(f\) 映射后的和,求得即为前面的东西;然后将可重集内的所有数减去 \(1\),求出可重集内所有数经过 \(f\) 映射后的和,求得即为后面的东西。
发现这个 \(f\) 映射和二进制密切相关,尝试使用 01-Trie 解决,首先因为是在树上,所以可以很方便的合并 01-Trie,并更新当前可重集中所有数经过 \(f\) 映射后的和,同节点权值直接相加即可。
关键是如何处理所有数减去 \(1\),联想小学时的减法,我们如果在末尾遇到连续的一串 \(0\),那么需要把它们全部变成 \(1\),直到出现第一个 \(1\),将其变成 \(0\)。把这个过程放到 Trie 上,首先我们需要从低位到高位建 Trie,然后每次相当于交换左右儿子,并且递归进交换后的右儿子。
时间复杂度 \(O(Tn\log n)\)。
#include<bits/stdc++.h>
using namespace std;
int n,q,m,x,y,a[21][2],d[200001],p,t[5000001][2],r[200001];
long long w[5000001],s[200001],c;
vector<int>g[200001];
inline long long insert(int i,int v,int h){
long long k=a[h][v&1];
if(h!=20){
if(t[i][v&1]==0){
p++;
t[i][v&1]=p;
t[p][0]=t[p][1]=w[p]=0;
}
k=k*insert(t[i][v&1],v>>1,h+1)%m;
}
w[i]=(w[i]+k)%m;
return k;
}
inline int merge(int i,int j){
if(i==0||j==0){
return i+j;
}
w[i]=(w[i]+w[j])%m;
t[i][0]=merge(t[i][0],t[j][0]);
t[i][1]=merge(t[i][1],t[j][1]);
return i;
}
inline void update(int i,int h){
swap(t[i][0],t[i][1]);
if(t[i][1]!=0){
update(t[i][1],h+1);
}
w[i]=(w[t[i][0]]*a[h][0]+w[t[i][1]]*a[h][1])%m;
}
inline void dfs(int i,int j){
d[i]=d[j]+1;
p++;
r[i]=p;
t[p][0]=t[p][1]=w[p]=0;
for(int k:g[i]){
if(k!=j){
dfs(k,i);
r[i]=merge(r[i],r[k]);
}
}
insert(r[i],i+d[i],0);
s[i]=w[r[i]];
if(i!=x){
update(r[i],0);
s[i]=(s[i]-w[r[i]]+m)%m;
}
}
inline void calc(int i,int j){
s[i]=(s[i]+s[j])%m;
for(int k:g[i]){
if(k!=j){
calc(k,i);
}
}
}
signed main(){
cin.tie(nullptr)->sync_with_stdio(0);
cin>>n;
for(int i=1;i<=n-1;i++){
cin>>x>>y;
g[x].push_back(y);
g[y].push_back(x);
}
cin>>q;
while(q--){
p=c=0;
cin>>x>>m;
for(int i=0;i<=20;i++){
cin>>a[i][0];
}
for(int i=0;i<=20;i++){
cin>>a[i][1];
}
dfs(x,0);
calc(x,0);
for(int i=1;i<=n;i++){
c^=s[i]*i;
}
cout<<c<<'\n';
}
return 0;
}
复习下这个题,再次遇到了这个 trick。
对于一个集合能不能用加法组合成一个数这个问题,是这样的。
我们考虑暴力的话,可以用背包做到 \(O(\frac{nV}{w})\) 没啥前途,我们可以做到 \(O(n)\)。
考虑直接维护答案,先把所有数从小到大排序,然后对于每个数的增加来维护答案的改变。
比如现在可以表示出 \([1,x]\) 中的所有数,那如果加进来的数 \(res \le x+1\) 那么答案也 \(+res\)。否则一定其中有数字 \(x+1\) 表达不出来,那么因为加入数字递增,同样可以推出来 \(x+1\) 无论如何都表达不出来。
考虑怎么放到区间里,考虑一个性质:
- 如果区间中的数字可以表达 \([1,x]\),但是 \(\le x\) 的数字的和 \(>x\) 那么代标我们可以把这些数字增加进来,答案变为 \(sum\),否则弹出。
我们可以用主席树维护这个东西,但是直接做复杂度不直观,我们来证明一下:
如果能进行上面的答案变为 \(sum\) 的操作,考虑一个数字$ w$ 被加的过程有三个,分别是 \(x<w\)、\(x\ge w\)、\(w\) 被加入三种状态。现在我们设这三种状态叫做 \(f_{i-1},f_i,f_{i+1}\) 我们实际上可以发现 \(f_{i+1}\ge f_i+f_{i-1}\),所以这个数列的增长速度不慢于斐波那契数列,所以复杂度是 \(2\log\) 的。
运用了 \(trie\) 的很多性质:
- 一棵加入了 \(n\) 个每个点权不同的 \(trie\) 其有 \(n-1\) 个节点恰好有两个儿子。
- 异或相差最小的两个点就是有最深相同 \(lca\) 的特点。
考虑直接把这 \(n-1\) 个点对拿出来就好了。可以证明每个点度数至少为 \(1\),所以是一棵树。
一个小 \(trick\):因为我们要枚举 \(lca\) 所含有的左端点儿子,我们可以把所有元素排好序,因为 \(Trie\) 上的点从左往右看是递增的,于是 \(Trie\) 的每一个节点就会对应排好序的数列中的一段区间,这样就不需要启发式合并之类的复杂操作了
code:
#include<bits/stdc++.h>
using namespace std;
#define int long long
#define inf (1 << 30)
#define rep(i, s, t) for(int i = s; i <= t; ++ i)
#define maxn 200005
int n, m, a[maxn], L[maxn * 32], R[maxn * 32], ch[2][maxn * 32], rt, cnt;
void insert(int&k, int id, int dep) {
if(!k) k = ++ cnt;
if(!L[k]) L[k] = id; R[k] = id;
if(dep == -1) return;
insert(ch[(a[id] >> dep) & 1][k], id, dep - 1);
}
int query(int k, int x, int dep) {
if(dep == -1) return 0;
int v = (x >> dep) & 1;
if(ch[v][k]) return query(ch[v][k], x, dep - 1);
return query(ch[v ^ 1][k], x, dep - 1) + (1 << dep);
}
int dfs(int k, int dep) {
if(dep == -1) return 0;
if(ch[0][k] && ch[1][k]) {
int ans = inf;
rep(i, L[ch[0][k]], R[ch[0][k]]) {
ans = min(ans, query(ch[1][k], a[i], dep - 1) + (1 << dep));
}
return dfs(ch[0][k], dep - 1) + dfs(ch[1][k], dep - 1) + ans;
}
else if(ch[0][k]) return dfs(ch[0][k], dep - 1);
else if(ch[1][k]) return dfs(ch[1][k], dep - 1);
return 0;
}
signed main() {
scanf("%lld", &n);
rep(i, 1, n) scanf("%lld", &a[i]);
sort(a + 1, a + n + 1);
rep(i, 1, n) insert(rt, i, 30);
printf("%lld", dfs(rt, 30));
return 0;
}
难度在于读题,考虑询问是可以差分的,然后考虑维护 \(trie\),发现第一个操作就是插入。第二个操作也很简单,就是往下走的时候维护一个单调栈保证时间递增,答案就是栈的大小。
对于 \(lazytag\) 在 \(trie\) 上的应用。考虑一个增加什么时候会对哪些点有贡献,具体来说就是会对这个点结尾的子树 \(+1\),但是前提是这个串是最长的,所以我们需要下放节点的时候判断这个点加入是不是最长的,也可以在 \(pushdown\) 内部解决。具体来说就是要维护一个 \(vis\) 代表这个节点为多少个节点的末尾。部分代码如下:
void pushdown(int x){
if (!t[x].s[0]) t[x].s[0] = ++stamp;
if (!t[x].s[1]) t[x].s[1] = ++stamp;
if (!t[t[x].s[0]].vis) t[t[x].s[0]].tag += t[x].tag, t[t[x].s[0]].ans += t[x].tag;
if (!t[t[x].s[1]].vis) t[t[x].s[1]].tag += t[x].tag, t[t[x].s[1]].ans += t[x].tag;
t[x].tag = 0;
}
void modify(string s, int op){
int x = 0;
for (auto i : s){
pushdown(x);
x = t[x].s[i - '0'];
}
t[x].vis += op;
t[x].tag++;
t[x].ans++;
}
P5283
可持久化 \(trie\),主要看看可持久化 \(trie\) 怎么构建,就是想主席树一样加点的时候新的那一条链用新节点。具体我们还要维护一个值 \(late\) 表示这个节点最晚被哪个串访问,这样我们就可以找到区间有没有这个点的更新。
code:
void ins(int i, int k, int p, int o) {
if (k < 0) return late[o] = i, void();
int c = (a[i] >> k) & 1;
if (p) trie[o][c^1] = trie[p][c^1];
trie[o][c] = ++t;
ins(i, k - 1, trie[p][c], trie[o][c]);
late[o] = max(late[trie[o][0]], late[trie[o][1]]);
}
int ask(ui x, int k, int o, int p) {
if (k < 0) return late[o];
int c = (x >> k) & 1;
return ask(x, k - 1, trie[o][c^(late[trie[o][c^1]]>=p)], p);
}
这个题就是我们想下第一大怎么转换到第 \(k\) 大,我们可以找到全局第一大,第二大……然后慢慢找出来。
所以我们可以对每个 \(r\) 维护一个当前最大左端点。放到堆里面弹出来的时候更新 \(l\) 在之前的左边或者右边,\(r\) 不变的两种可能就好了。

浙公网安备 33010602011771号