11.18

AT_abc204_f

水题,看到数据范围想到状压和矩快,然后想想转移,发现这玩意一定可以线性递推,所以推出来状压的转移就好了。我们可以得到一下方程,其中 \(C\) 表示了一个状态到另一个状态的系数,发现只和并集有关,所以可以预处理出来:

\[f_{i,S}=\sum_{T|S=2^n-1}(f_{i-1,T}\times C_{T\&S}) \]

最终答案即为 \(f_{m,2^n-1}\)

时间复杂度 \(O(8^n\log m)\)

AT_abc204_e

最短路套上数学,考虑计算怎么经过这个点时间最短,我们把式子拿下来,发现:

\[ans=t+c+\lfloor\frac{D_i}{t+1}\rfloor \]

这是一个有极值的函数,取到 \(t=\sqrt n-1\) 时最优,所以价值就可以分段给贡献计算了。

P14509

赛时 \(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;
}

P4587

复习下这个题,再次遇到了这个 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\) 的。

CF888G

运用了 \(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;
}

P5768

难度在于读题,考虑询问是可以差分的,然后考虑维护 \(trie\),发现第一个操作就是插入。第二个操作也很简单,就是往下走的时候维护一个单调栈保证时间递增,答案就是栈的大小。

P5460

对于 \(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\) 不变的两种可能就好了。

posted @ 2025-11-18 21:59  NeeDna  阅读(13)  评论(0)    收藏  举报