普及组集训

Trie

T1

字典树板子题,先按照串长从大到小排序,保证一个串插入时,如果他是另一个串的前缀,那么另一个串已经在字典树里面,所以每次插入时判断当前节点是否在之前已经建立过就行了。

T2

01 trie板子题,插入前从高到低位在trie上走,优先走当前位的反边,然后再插入当前数就行了

T3

区间查询想到前缀异或和,问题变为问 \(a_i \oplus a_j + a_k \oplus a_l,0<=i<j<=k<l<=n\) ,使用T2的做法,前缀后缀跑一遍然后合并就行了。

T4

同T1

T5

弱化版做法

对于每个询问,进行dfs,找到从每个位置开始能走最远是多少,转移的话就在trie上走就好了,复杂度 \(O(n\omega|T|)\)

强化版做法

发现字符串长度很短,考虑状压trie上每个节点能往左扩展的长度,你发现每个节点能扩展的长度还要继承fail树上的祖先,所以在建立ac自动机的过程中进行转移,在询问的时候将当前在trie上匹配的节点长度集合与可达到当前节点的位置集合并一下就行。

T6

问题等价于问一个字符串在trie上的祖先和子树大小,我们在建立trie的时候,就提前将贡献计算到trie上的节点,询问的时候在trie上一边跑一边计算贡献就行了。

T7

首先发现后缀是假的,对反串考虑就行了,然后发现第一个情况是没有用的,因为代价太大了,如果是后缀的话交换两个肯定更优,第二种情况相当于后缀最大下标是0,可以归到后面的情况,第三种情况,就是找到trie树上下标最大的祖先。我们考虑该如何分配最优,首先我们想到,如果选择了一个节点,然后立刻选他的儿子,会不会比选别的更优,考虑调整法,假设中间夹了很多其他节点,我们发现,把选儿子这个过程提前,显然不会更劣,所以说最后序列肯定是trie树的dfs序,然后那么还需要在兄弟节点之间选择,我们发现,这就是排队喝水问题,由于每次都会填满一个子树,然后再填其他兄弟,所以说先填子树大小最小的肯定更优。

T8

树上异或差分经典结论,两个节点路径上的异或等于两个节点到根的异或,因为到根的路径会抵消,所以同T2

ACAM

调试记录

跳fail写成走到儿子。trie上标记end的节点编号写错。dp的时候忘记特判,导致使用了非法的状态进行转移。

T1

板子,建立AC自动机之后,在匹配串上跳fail就行。

T2

建立AC自动机之后,用匹配串匹配AC自动机上的节点,然后对于每个模式串一直走trie就行了

T3

这一题如果按照正常ac自动机板子跳fail然后用链表维护删除可以拿到93 pts,但是为什么会被卡呢,因为ac自动机撤销之后的均摊的复杂度就不对了,比如说一堆a+一堆b,然后模式串只有一个b,这种情况对于后面消除的每一个b,都要跳这一堆a的fail,直接被卡到 \(O(n^2)\),所以说就要使用oi-wiki上面的优化,就是bfs里面加上else后面的部分,这样子后面询问的时候就不需要跳fail了,复杂度自然就对了。

T4

首先在插入trie的时候对于每个节点求出作为前缀被包含的次数,然后所有区间的包含次数可以转化位作为前缀的后缀被包含的次数就是fail树上的子树之和

T5

dp做法

发现如果忽略被包含的串,那么向后添加串的贡献只跟你串末是什么字符串有关,所以说类似于P3502 [POI 2010] CHO-Hamsters的做法,设 \(f_{s,i}\) 表示包含s集合里面的字符串,串末是i的串长最小的字典序最小的字符串,提前预处理一个字符串后面加上另一个字符串的代价即可。

ACAM做法

发现问题相当于找到ac自动机上的路径能走到所有的字符串,那么我们可以对每个节点状压,表示这个节点代表哪些字符串,然后在求fail的过程中转移就行了,然后用bfs在ac自动机上走就行了。

T6

跟T5相反,我们不能走到有标记的点,然后走到环就能无限走了。

T7

首先容斥,考虑不能被表示的数量,跟T6一样,但是这题需要加一个dp就做完了。

组合数学

调试记录

高精度输出的时候,忘记处理压位之后的前导0

Lucas定理

\(\binom{n}{m}\equiv\binom{\lfloor n/p \rfloor}{\lfloor m/p\rfloor}\binom{n\bmod p}{m\bmod p}\pmod p\)
证明的话可以把组合数拆开就行了。

应用

求模数为质数而且很小的组合数,递归求解,然后小于p的直接预处理就行了,时间复杂度 \(O(n\log_p n+p)\)

代码

#include<bits/stdc++.h>
#define int long long
using namespace std;
const int N=1e5+5;
int t,n,m,p,inv[N],fac[N],ifac[N];
int C(int x,int y){return x>=y?fac[x]*ifac[x-y]%p*ifac[y]%p:0;}
int L(int x,int y){return y?L(x/p,y/p)*C(x%p,y%p)%p:1;}
signed main(){
	ios::sync_with_stdio(0),cin.tie(0);
	cin>>t;
	while(t--){
		cin>>n>>m>>p;
		inv[0]=inv[1]=fac[0]=ifac[0]=1;
		for(int i=2;i<p;i++)inv[i]=inv[p%i]*(p-p/i)%p;
		for(int i=1;i<p;i++)fac[i]=fac[i-1]*i%p;
		for(int i=1;i<p;i++)ifac[i]=ifac[i-1]*inv[i]%p;
		cout<<L(n+m,n)<<'\n';
	}
	return 0;
}

容斥原理

调试记录

字符串新建的时候是没有大小的,所以需要先给字符串复制为另一个字符串,才能每一位赋值。

T1

显然,第一个随便选,后面的少选一种,快速幂做完了

T2

盒子不区分,就是第二类斯特林数,区分的话乘上阶乘就行了。

T3

新奇多重背包做法,先当作完全背包做,然后容斥掉超出次数的方案,然后就做完了,复杂度 \(O(nm+T2^nn)\)

T4

反着做有一个好的性质,最多只有1种食材的数量会超过一半,总方案数是好求的,不合法的方案数可以考虑dp,设状态 \(F_{i,j,k,l}\) 表示第i种食材为主,目前考虑到前j种烹饪方法,选了k个第i种,选了l个其他种,复杂度直接爆掉。考虑经典trick,由于我们其实完全不关心k和l的具体大小,只关心他们的大小关系,所以只需要维护这两个的差就行了

T5

首先经典容斥,恰好转钦定,由于n非常小,所以可以直接dfs就做完了

T6

首先,有朴素dp,\(F_{u,s,i}\)表示子树u对应s里面的点,u对应i,显然,这需要一个子集枚举,复杂度不对。考虑一个神仙容斥,分析一下我们什么状态可以简略,显然,s这个代价太大了,用2^n的代价限制不能对应重复的点,我们考虑允许重复,那么这样就是复杂度很低的dp,然后把重复的容斥掉。怎么容斥呢,我们发现,没有重复使用的点等价于所有的点都被对应上了,那我们可以考虑枚举这n个点能对应的点的编号集合,强行规定树上的点只能对应这些编号的点,然后容斥系数是1和-1交错,复杂度\(O(2^nn^3)\)

T7

首先,题意可以转化为你有两个值互不相同的数组,你要重排这两个数组,求使得a_i>b_i的数量=k的方案数,首先恰好转钦定,然后就可以dp了,设\(F_{i,j}\)表示前i组钦定其中j个满足条件的方案数,然后发现如果数组是乱的,不太好转移,我们发现重排数组不会影响答案,所以直接把两个数组排序,这样编号大的能匹配的点的集合严格包含大于编号小的,就可以很容易转移了,转移方程为 \(F_{i,j}=F_{i-1,j}+F_{i-1,j-1}\times (p-j+1)\),p为i能匹配的数量,因为前面已经有j-1个已经被匹配了,所以第i个匹配的方案就是p-j+1。

posted @ 2026-04-29 15:56  Lqs314  阅读(16)  评论(0)    收藏  举报