OI 学习总结 part1

7.4 测试

爆炸

T1 至今不知道平衡树哪里打挂了,打了个暴力忘开 long long 50->0tps

T2 一眼状压 DP,至今不知道哪里挂了,80tps

呃呃呃原来不是状压 DP 吗

呃呃呃原来是我题目理解错了呃呃呃

原来是搜索题。

T3 想了个会炸空间的线段树做法,暴力没打完,30->0tps,应该少分配点 T1 的时间的。

其实我感觉最近的知识点掌握的挺好的,但就是老打挂。看来是模拟刷少了。

呜呜呜

7.13 线段树分治

解锁新人物:DP

例题:动态图连通性离线版

我们把每个边出现的时间段挂在一颗时间轴线段树上。

然后 dfs 一遍,遇到一个节点,就把边挂上并查集,用可撤销并查集维护是否联通,回溯时撤销。

并查集用来维护是否联通。遇到叶子结点判一下就可以了。

CF601E A Museum Robbery

就是有删元素、加元素的 01 背包。

把每个元素出现的时间挂到线段树上,遍历时直接 dp,回溯时因为 dp 的无后效性可以直接回溯。

P5631 最小mex生成树

如果最小 mex 是 \(w\),那么 \(w\) 就不能加进图里。我们对 mex 可能的值开一颗线段树,那么该边有效的区间就是 \([0,x-1]\cup[x+1,maxv+1]\)。我们直接挂上线段树然后 dfs 一遍,如果是叶子并且联通就输出然后 exit(0)。

场景/套路

如果一个元素只生效于某个范围,并且常规算法难做,可以离线时,我们可以用线段树分治。

线段树分治的题无非就是找每个元素生效的范围、如何维护(撤销)就没了。

7.10 平衡树 - 无旋 FHQ Treap

Treap

普通的 BST 会在连续插入递增数列时树高渝复杂度会退化为 \(\mathcal{O}(n)\),此时就要用上平衡树,把 BST 的树高和复杂度稳定在 \(\mathcal{O}(\log n)\)。

FHQ Treap 就是让 BST 按随机出来的优先级排序。就是给每个节点新建一个优先级,按优先级形成一个小根堆,同时满足 BST 的性质。

FHQ Treap 使用分裂与合并实现插入、删除、第 k 小等操作。

分裂:把一颗 FHQ Treap 权值小于 \(x\) 的节点保留,大于 \(x\) 的节点给另外一颗树。显然,分出来的两颗树都是 FHQ Treap。

合并:合并一般是把分裂后的两颗 Treap 合起来,此时左边的 Treap 一定小于右边的 Treap,所以直接按堆的优先级规则合并即可。

这两个代码比较重要我还是贴一下。

void split(int p,int v,int &L,int &R){
	if(!p){
		L=R=0;
		return;
	}
	if(t[p].val<=v){
		L=p;
		split(t[p].rs,v,t[p].rs,R);
	}
	else{
		R=p;
		split(t[p].ls,v,L,t[p].ls);
	}
	pushup(p);
}
int merge(int L,int R){
	if(!L||!R) return L|R;
	if(t[L].pri<t[R].pri){
		t[L].rs=merge(t[L].rs,R);
		pushup(L);
		return L;
	}
	else{
		t[R].ls=merge(L,t[R].ls);
		pushup(R);
		return R;
	}
}

往 Treap 里插入一个数 \(v\):分裂出 Treap 权值小于 \(v\) 和大于 \(v\) 的两个部分,然后合并小于 \(v\) 的部分与新节点 \(v\),再把剩下两部分合并即可。

在 Treap 里删除一个数 \(v\):分裂出 Treap 权值大于 \(v\) 的,然后再分裂出小于 \(v\) 的,剩下的就是 \(v\) 那一个节点,舍去即可。然后把小于 \(v\) 和大于 \(v\) 的重新合并即可。

寻找第 \(k\) 小/大的数:在 Treap 上二分,跟线段树上二分类似。

寻找 \(v\) 是第几大:分裂出小于 \(v\) 的 Treap,\(v\) 的排名就是这棵树的大小加一。

翻转序列:和线段树一样打懒标记,pushdown 时直接交换做儿子和右儿子,最后中序遍历一遍就是反转后的序列。

pb_ds

pb_ds 库中有大量预制好的数据结构,CCF 赛事可用,使用方法:https://www.luogu.com.cn/article/tk8rh0c9

7.9 线段树练习

P3740 [HAOI2014] 贴海报

ODT 板子。

线段树做法也是板子。

P3224 [HNOI2012] 永无乡

并查集判联通,不联通就合并。

P3241 [HNOI2015] 开店

请输入文本。

首先考虑没有 \([L,R]\) 限制的求法。对于每个节点 \(u\),不方便之和推式子可以得到:

\[n\times d_u + \sum_v{d_v}-2\times\sum_{v}d_{lca(u,v)} \]

前两项好求,最后那一项难求。

考虑树剖,对每个节点 \(v\),用树剖让 \(1\sim v\) 的路径打一个标记。然后 \(1\sim u\) 路径上的标记数 \(\times\) 边权就是最后一项。

然后加上 \([L,R]\) 的限制。先给 \(x\) 按权值从小到大排序,然后对于每一个权值,开一颗线段树,对线段树求一个前缀和。然后询问就是可持久化线段树板子,这题就做完了。

注意边权转点权、离散化。

请输入文本。

7.8 线段树分裂+珂朵莉树ODT

线段树分裂

就是从一棵大的线段树里伶出一颗小的,这个小的必须是连续的一段。

常见分裂:

1.分裂出值大于 \(k\) 的树。

递归左右儿子,如果当前节点 \(l\ge k\) 直接把右子树给新的树,否则继续递归。

2.分裂出前 \(k\) 小/大的树。

跟 1 差不多。

3.分裂 \([l,r]\) 范围内的树。

跟区间查询的递归方式一样,但是边递归边分裂。

珂朵莉树

就是把一个序列分段,存进 set 里。然后如果要把 \([l,r]\) 统一赋值成 \(c\),就在 set 上二分 \(l,r\),把 \(l,r\) 间的都删除,插入一个大的 \(l,r\) 段就行了。因为 set 是红黑树可以 \(\log n\) 级别删除所以很快。然后如果需要拆开 \(l,r\) 的那两段直接拆就行了不会有影响。

7.7 可持久化线段树

可以高效查询历史版本的线段树。

同样地,还有可持久化并查集、可持久化平衡树等等。

可持久化全职线段树被称为主席树。

例题:区间第 k 小

建树

对于每个 \(a_i\),建一颗线段树,然后把这些线段树做前缀和,需要动态开点,否则时空双爆。。

具体地,对于每个 \(a_i\),从 \(root_{i-1}\) 开始沿着 \(a_i\) 在线段树上的路径搜索。每搜到一个节点 \(p\) 就新开一个节点 \(np\),使 \(sum_{np}=sum_{p}+1\)。如果要往左递归,就把 \(np\) 的左儿子连接到 \(p\) 的左儿子上,往右同理。边界就是如果遇到叶子结点就返回叶子结点编号。然后返回这个子树的根节点。递归完了之后就记录一下 \(root_i\)

时空复杂度 \(\mathcal{O}(n\log V)\),\(V\) 表示 \(a_i\) 的值域,因为要离散化所以在此题等于 \(n\)。

查询

同时从 \(root_{l-1}\) 和 \(root_r\) 开始递归求出第 \(k\) 小就行了。看一下 \(r-l\) 的左区间的元素个数 \(lsum\) 是否小于等于 \(k\),是就递归求左区间内第 \(k\) 小,不是就求右区间内第 \(k-lsum\) 小。

7.6 线段树合并

顾名思义,就是两个线段树合并成一个线段树的算法。

例题:子树众数

启发式暴力合并

dfs 时,把子节点的线段树合并到父节点的线段树里面。

暴力合并:直接把儿子的线段树的每个节点插入父节点的线段树。时间复杂度显然 \(\mathcal{O}(n^2)\)。

启发式暴力:把大小小的线段树合并到大小大的线段树里。看上去没快多少,实际上,每个节点最多被搬运 \(\log_2n\) 次,因为每次树的大小都会变成两倍以上。时间复杂度 \(\mathcal{O}(n \log^2n)\)。

结构化合并

直接把相同值域的节点都相加。

加上动态开店,合并两棵树 \(A,B\) 时,同时遍历两颗线段树。如果当前节点 \(B\) 有左儿子 \(A\) 没有,直接把 \(A\) 当前节点的左儿子连到 \(B\) 当前节点的左儿子上。右儿子或者反过来同理。

因为最多合并总线段树节点数,所以时间复杂度为 \(\mathcal{O}(n\log V)\)。

均摊时间复杂度计算

有计算总数、设置权值、势能分析(设置权值形式化表示)。

5.3 前缀函数 & KMP 学习总结

这个算法下标从一开始比较方便,下文统一从一开始。

前缀函数的定义:字符串 \(s\) 中,最长的长度相等的真前缀与真后缀的长度。

对于一个字符串 \(s\),它的前 \(i\) 个字符构成的子串的前缀函数记作 \(\pi_i\)。

暴力求 \(\pi_{1\dots n}\):\(\mathcal O(n^3)\),一次字符串比较 \(\mathcal O(n)\)。

优化一:

注意到 \(\pi_i\) 相比 \(\pi_{i-1}\) 最多加了 \(1\)。

所以在求 \(\pi_i\) 时,可以从 \(\pi_{i-1}\) 枚举到 \(1\)。虽然看着没啥变化,但时间复杂度从 \(\mathcal O(n^3)\) 降至 \(\mathcal O(n^2)\)!

\(\Large\text{WHY?}\)

首先对于加了一的情况,只需要字符串比较一次就可以了(比较 \(s[1\dots \pi_{i-1}+1]\) 与 \(s[i-\pi_{i-1}\dots i]\))。

然后对于减少的情况,最坏情况是所有这种情况都减到了 \(0\),也就是比较 \(\pi_{i-1}\) 次字符串,这个 \(\pi_i-1\) 是若干次加一的情况来的。

显然加一的情况最多一共加 \(n-1\) 次,那么减少的情况一共也只会比较 \(n-1\) 次,所以一共最多比较 \(\mathcal O(n)\) 次,故复杂度为 \(\mathcal O(n^2)\)。

优化二:

这个画图比较好理解。

图片名称

如上图,如果 \(s[\pi_{i-1}+1]=s[i]\),那么 \(\pi_i=\pi_{i-1}+1\)。

否则,根据 \(\pi_{\pi_{i-1}}\) 的定义,图中两个蓝色部分相等,根据 \(\pi_{i-1}\) 的定义,两个橙色部分相等,那么左蓝就等于右橙部分。所以只要 \(s[\pi_{\pi_{i-1}}+1] = s[i]\),\(s[1\dots\pi_{\pi_{i-1}}+1]\) 就等于右边橙色部分加上 \(s[i]\),就是 \(\pi_i\) 了!如果不相等,就继续套 \(\pi_{\pi_{\pi_{i-1}}}\dots\),直到等于 \(0\) 或者找到可以匹配的为止。

这个玩意不需要任何字符串比较,而且根据优化一中时间复杂度的求法(加上优化二甚至达不到 \(2n-3\) 次比较),这个算法的时间复杂度是 \(\mathcal O(n)\) 的。

for(int i=2;i<=n;i++){
	int p=pi[i-1];
	while(p>0&&s[i]!=s[p+1]) p=pi[p];
	if(s[i]==s[p+1]) p++;
	pi[i]=p;
}

下面是前缀函数的应用。

KMP 算法

在字符串中查找所有子串位置可以使用 KMP 算法。

做法就是对 占位符 + 要找的子串 + 分隔符 + 字符串 跑一遍前缀函数,然后看字符串内哪个位置的前缀函数是子串的长度,那个位置就是这个子串结尾的位置,注意可能有多个。

#include<bits/stdc++.h>
#define wef(i,l,r) for(int i=l;i<=r;i++)
inline int read(){int x=0,f=1;char ch=getchar();while(ch<'0'||ch>'9'){if(ch=='-') f=-1;ch=getchar();}while(ch>='0'&&ch<='9') x=x*10+ch-'0',ch=getchar();return x*f;}
void write(auto x,char fg=0){bool neg=0;if(x<0){neg=1;putchar('-');}static int sta[40];int top=0;do{sta[top++]=x%10;x/=10;}while(x);if(neg) while(top) putchar('0'-sta[--top]);	else while(top) putchar('0'+sta[--top]);if(fg) putchar(fg);}
int pi[2000005];
int main(){
	ios::sync_with_stdio(0);
	cin.tie(0),cout.tie(0);
	string s1,s2;
	cin>>s1>>s2;
	int n=s1.size(),m=s2.size();
	string s=" "+s2+"#"+s1;
	int c=s.size()-1;
	wef(i,2,c){
		int p=pi[i-1];
		while(p>0&&s[i]!=s[p+1]) p=pi[p];
		if(s[i]==s[p+1]) p++;
		pi[i]=p;
	}
	wef(i,m+2,c) if(pi[i]==m) write(i-2*m,'\n');
	return 0;
}

求字符串最长周期大小

注意这里的周期可以像这样:

aabaabaa 的循环节可以是 aab、aabaab。

也就是说末尾的那一个节不用循环完,长度不一定整除整个串的长度。 怎么感觉有点像神秘分块

答案为 \(n-\pi_n\),\(n\) 为字符串长度。

有一个变式,就是长度一定要整除。

答案依旧为 \(n-\pi_n\),如果这个答案不能整除,那就没有答案(只能是 \(n\))。

还有一个变式,求出所有可能的循环节的长度。

答案为 \(n-\pi_n\)、\(n-\pi_{\pi_{n}}\)、\(n-\pi_{\pi_{\pi_n}}\dots\)

这些题我都有一个绝妙的证明过程,可惜晚自习的时间太短我写不下(在课件上)。

例题 P10475 [USACO03FALL] Milking Grid(数据加强版)

就是求字符串最长周期大小,每行挨着求一遍可能的循环节长度,然后看那种长度每行都可以,取最小值,列同理。

例题 CF1200E Compress Words

首先想到每次合并时,把后一个字符串放到前一个串的前面,然后跑一遍前缀函数。比如 \(miaomiao\color{red}asas\) 和 \({\color{red}asas}qwqwqwq\) 就合并成 \(miaomiao{\color{red}asas} + qwqwqwq\)。

但是每次合并都是 \(\mathcal O(n)\),时间复杂度 \(\mathcal O(n^2)\) 咋办?

发现 \(OOOOOOOOasdf\color{red}TLE\) 和 \({\color{red}TLE}qwq\),这个 \(OOOOOOOOa\) 其实是多余的,因为公共的长度最多也就 \(min(|a|,|b|)\),所以直接合并 \(a\) 的后 \(min(|a|,|b|)\) 个字符和 \(b\) 的前 \(min(|a|,|b|)\) 个字符就可以了,现在总共最多合并 \(n\) 个字符,复杂度 \(\mathcal O(n)\)。

\(\large\text{但 WA 了!}\)

为什么?发现 101 和 010 合并时,存在跨界出现的公共前后缀(0101),所以我们在他们中间加上一个分隔符,这样跑前缀函数的时候就是 010#101,不会跨界啦!

#include<bits/stdc++.h>
#define wef(i,l,r) for(int i=l;i<=r;i++)
#define few(i,r,l) for(int i=r;i>=l;i--)
using ll=long long;
using namespace std;
inline int read(){int x=0,f=1;char ch=getchar();while(ch<'0'||ch>'9'){if(ch=='-') f=-1;ch=getchar();}while(ch>='0'&&ch<='9') x=x*10+ch-'0',ch=getchar();return x*f;}
void write(auto x,char fg=0){bool neg=0;if(x<0){neg=1;putchar('-');}static int sta[40];int top=0;do{sta[top++]=x%10;x/=10;}while(x);if(neg) while(top) putchar('0'-sta[--top]);	else while(top) putchar('0'+sta[--top]);if(fg) putchar(fg);}
void putstr(string a){int t=a.size();for(int i=0;i<t;i++) putchar(a[i]);}
int pi[1000005];
void kmp(string a){
	int n=a.size();
	a=' '+a;
	wef(i,2,n){
		int p=pi[i-1];
		while(p>0&&a[i]!=a[p+1]) p=pi[p];
		if(a[i]==a[p+1]) p++;
		pi[i]=p;
	}
}
int main(){
	ios::sync_with_stdio(0);
	cin.tie(0),cout.tie(0);
	int n;
	string res;
	cin>>n>>res;
	wef(i,2,n){
		string a;
		cin>>a;
		int n=a.size(),m=res.size(),c=min(n,m);
		string tmp=a+"#"+res.substr(m-c,c);
		kmp(tmp);
		wef(i,pi[tmp.size()],n-1) res+=a[i];
	}
	cout<<res;
	return 0;
}

5.4 前缀函数练习

非常抱歉我之前给你的总结太啰嗦!这一次我会用简练的语言总结前缀函数的应用,保证无废话,全是大白话,语言精准概括要点!

求任意前缀在字符串里的出现字数

对于 abcabcacababc:

a 出现 \(5\) 次;

ab 出现 \(4\) 次;

abc 出现 \(3\) 次;

abca 出现 \(2\) 次;

abcab 出现 \(1\) 次;

后面的都是 \(1\) 次;

求法:根据 \(\pi\) 的性质,右端点为 \(\pi_i\) 的前缀会在 右端点为 \(i\) 的前缀里面的最右边出现。

for(int i=n;i>=1;i--) ans[pi[i]]+=ans[i];

\(ans[i]\) 表示 \(1\) 到 \(i\) 的字符串的出现次数。

字符串中不同的子串数量

注意这里是内容不同而不是位置不同。

我们考虑每新增一个字符,就统计这个字符增加了多少种子串。

也就是说,我们只需统计,每次增加了多少个“新后缀”。

我们反转一下现在的串,问题变为求“新前缀”。

然后把他转化为“总长度-旧前缀”,发现“旧前缀”就是前缀函数板子。

对于每个翻转过来的前缀(长度为 \(len\)),跑一遍前缀函数,求出 \(\max\{pi_i\}\),将 \(len-\max\{pi_i\}\),这样比 \(\max\{pi_i\}\) 长度小的都会被减去,又根据 \(\pi\) 是最长的这个性质,比 \(\max\{pi_i\}\) 大的就都是新的。

时间复杂度 \(\mathcal O(n^2)\)

#include<bits/stdc++.h>
#define wef(i,l,r) for(int i=l;i<=r;i++)
using namespace std;
int P[5005];
int main(){
	ios::sync_with_stdio(0);cin.tie(0),cout.tie(0);
	int n,r=0;string s="",A;cin>>n>>A;
	A=' '+A; 
	wef(T,1,n){
		s=A[T]+s;
		int M=0;
		wef(i,2,T){
			int j=P[i-1];
			while(j>0&&s[i-1]!=s[j]) j=P[j];
			(j=s[i-1]==s[j]?j+1:j),P[i]=j,M=max(M,j);
		}
		r+=T-M;
	}
	cout<<r;
	return 0;
}

练习题:CF126B Password

题意:求最长的是字符串的前缀、后缀,并且在字符串中间出现过的子串。

公共前缀后缀很好搞(前缀函数),但如何判断中间也出现过?

跑一遍上面的 求任意前缀在字符串里的出现字数,如果这个公共前后缀在串中的出现次数 \(\le 3\),就说明除了开头结尾,中间肯定出现过。

跑出所有公共前后缀:\(\pi_n,\pi_{\pi_{n}},\pi_{\pi_{\pi_n}}\dots\),直到为 \(0\),这些都是串的公共前后缀。

#include<bits/stdc++.h>
#define wef(i,l,r) for(int i=l;i<=r;i++)
#define few(i,r,l) for(int i=r;i>=l;i--)
using ll=long long;
using namespace std;
inline int read(){int x=0,f=1;char ch=getchar();while(ch<'0'||ch>'9'){if(ch=='-') f=-1;ch=getchar();}while(ch>='0'&&ch<='9') x=x*10+ch-'0',ch=getchar();return x*f;}
void write(auto x,char fg=0){bool neg=0;if(x<0){neg=1;putchar('-');}static int sta[40];int top=0;do{sta[top++]=x%10;x/=10;}while(x);if(neg) while(top) putchar('0'-sta[--top]);	else while(top) putchar('0'+sta[--top]);if(fg) putchar(fg);}
int p[1000005],t[1000005];
int main(){
	ios::sync_with_stdio(0);
	cin.tie(0),cout.tie(0);
	string s;
	cin>>s;
	int n=s.size();
	s=' '+s;
	wef(i,2,n){
		int j=p[i-1];
		while(j>0&&s[i]!=s[j+1]) j=p[j];
		if(s[i]==s[j+1]) j++;
		p[i]=j;
		t[j]++;
	}
	few(i,n,1) if(t[i]>1){
		cout<<s.substr(1,i);
		return 0;
	}
	cout<<"Just a legend";
	return 0;
}

练习题 ARC060 F - Best Representation

紫题吓哭了

AC 难度绿,证明难度黑。

首先跑一遍前缀函数,然后根据循环节分类讨论。

  • 如果 \(n-\pi_n = n\) 或 \(n-\pi_n \nmid n\),意味着 \(w\) 的循环节只能是它自己,直接输出 \(1,1\)。
  • 如果 \(w\) 全是一个相同的字符,答案显然为 \(n,1\)。
  • 如果 \(n-\pi_n \mid n\),存在循环节,我们重点看这个。

首先根据瞪眼法得出,只分成两份也一定有解,所以第一行输出 \(2\)。

为什么?我也不会证自己去看题解吧 反正考试的时候不需要知道为什么,这个一眼就是对的。

然后我们枚举分开的这个断点,看左边是否有循环节,右边是否有循环节。所以为了判断右边我们还要反着跑一遍前缀函数。

然后就没了,时间空间都是 \(\mathcal O(n)\),模 \(10^9+7\) 就是个骗人的。Atcoder你阴完了害得我想那么久

#include<bits/stdc++.h>
#define wef(i,l,r) for(int i=l;i<=r;i++)
#define few(i,r,l) for(int i=r;i>=l;i--)
using namespace std;
using ll=long long;
const int N=500000;
int n;
int p1[N+1],p2[N+1];
void solve(int p[],string a){//这样反着跑很方便qwq
	wef(i,2,n){
		int j=p[i-1];
		while(j&&a[i]!=a[j+1]) j=p[j];
		if(a[i]==a[j+1]) j++;
		p[i]=j;
	}
//	wef(i,1,n) cout<<p[i]<<" ";
}
int main(){
	ios::sync_with_stdio(0);
	cin.tie(0),cout.tie(0);
	string a;
	cin>>a;
	n=a.size();
	a=' '+a;
	bool f=1;
	wef(i,1,n-1){
		if(a[i]!=a[i+1]){
			f=0;
			break;
		}
	}
	if(f){
		cout<<n<<"\n1";
		return 0;
	}
	solve(p1,a);
	reverse(a.begin(),a.end());
	a=' '+a;
	solve(p2,a);
	if(!p1[n]||n%(n-p1[n])){
		cout<<"1\n1";
		return 0;
	}
	cout<<"2\n";
	int res=0;
	for(int i=1;i<n;i++){
		if((!p1[i]||i%(i-p1[i]))&&(!p2[n-i]||(n-i)%(n-i-p2[n-i]))) res++;
	}
	cout<<res;
	return 0;
}

3.25 卡特兰数总结

卡特兰数是一种特殊的数列,适用于各种特殊问题。

Catalan数适用的问题

下面几个问题的答案都是卡特兰数的第 \(n\) 项。

对角线问题

从 \((0,0)\) 开始,每次可以向右或向上走一步,走到 \((n,n)\),走的时候不能超过左下到右上的对角线,问有多少种走法。可以任意次触碰对角线。

括号序列数量

问长度为 \(n\) 的合法括号匹配序列有多少个。

\(+1,-1\) 序列个数

长度为 \(n\) 的 \(+1,-1\) 组成的序列,任意一点的前缀和大于等于 \(0\),这样的序列有多少个。

二叉树数量

节点数为 \(n\) 的二叉树的数量。

楼梯切割

image

这样的楼梯被红框切割成 \(n\) 份的方案数。

出栈顺序

一个无穷大的栈进栈序列为 \(1,2,3,\dots,n-1,n\),问有多少种出栈顺序。

\(\vdots\)

Catalan 数的求法

递推公式:\(Cat_n=\sum^{n}_{i=0}Cat_i Cat_{n-i} , Cat_0=1\)。

用这个求复杂度 \(O(n^2)\)。

通项公式:\(Cat_n=C_{2n}^{n}\times C_{2n}^{n-1}\)。

时间复杂度 \(O(n)\),如果用预处理求排列数的话。

P5014 水の三角(修改版)

首先压成这样的图:
image

没有斜线就是卡特兰数板子。

有斜线的话,发现 \(n\) 足够小使得可以枚举(计算)斜着走了几次。然后这个问题就转化成 \((1,1)→(a',k')\) 每时每刻向下必须比向右多的方案数。

那就考虑魔改一下 Catalan 数,变成 \(n\) 个 \(0\) , \(m\) 个 \(1\) ,前缀 \(0≥\) 前缀 \(1\) 的方案数。

然后下面和推 Catalan 的方法几乎一样了。

推出来 \(Cat'(n,m)=C^{n}_{n+m}-C^{n+1}_{n+m}\)。

#include<bits/stdc++.h>
using namespace std;
#define MOD(x) x=(x>=mod?x-mod:x)
using ll=long long;
const int N=2e6,mod=998244353;
int qpow(int a,int b){
	int res=1;
	while(b){
		if(b&1) res=(ll)res*a%mod;
		a=(ll)a*a%mod,b>>=1;
	}
	return res;
}
#define Cat(n,m) ( C(n+m,n) - C(n+m,n+1) +mod )
#define C(n,m) (ll)fac[n]%mod*inv[m]%mod*inv[(n)-(m)]%mod
int fac[N+5],inv[N+5];
inline void init(){
	fac[0]=1;
	for(int i=1;i<=N;i++) fac[i]=(ll)fac[i-1]*i%mod;
	for(int i=N;i>=0;i--) inv[i]=qpow(fac[i],mod-2);
}
int main(){
	ios::sync_with_stdio(0);
	cin.tie(0),cout.tie(0);
	init();
	int T;
	cin>>T;
	while(T--){
		ll n;
		cin>>n;
		int a=sqrt(n)-1,k;
		while(n-(ll)a*(a-1)/2>a) a++;
		k=n-(ll)a*(a-1)/2;
		int ans=0;
		for(int i=0;i<k;i++) ans+=C(a+k-2-i,i)*Cat(a-1-i,k-1-i)%mod,MOD(ans);
		cout<<ans<<"\n";
	}
	return 0;
}

3.14 排列组合练习

P3197 [HNOI2008] 越狱

正难则反,用所有情况减去不可能发生越狱的情况。

所有情况:\(m^n\) 种

不可能发生越狱的情况:

从左到右,第一个人有m种选择,第二个人为了和前面一个人不一样,有 m-1 种选择。以此类推,共有 \(m\times (m-1)^n\) 种。

答案:\(m^n-m\times (m-1)^n\),用快速幂解决。

P8557 炼金术(Alchemy)

考虑 \(n\) 种金属每种金属可能的方案数。

对于任意一种金属,它可以被任意 \(1\sim k\) 个熔炉所造出来,方案总数为:\(\sum^{k}_{i=1} C^{i}_{k}=2^k-1\)

\(n\) 种金属就是 \((2^k-1)^n\) 种。

矩阵乘法、矩阵快速幂总结

3.7 矩阵乘法、矩阵快速幂总结

矩阵

就是一个矩阵,嗯。

\[\begin{bmatrix} 1 & 1\\ 4 & 5\\ 1 & 4 \end{bmatrix} \]

发明出来据说是为了偷懒。

矩阵乘法

不是传统意义上的相乘,只要矩阵 \(A\) 的列数等于矩阵 \(B\) 的行数,就可以做矩阵乘法。

举一个简单的例子:

\[A= \begin{bmatrix} 1&2&3\\ 3&1&4\\ 2&2&1 \end{bmatrix} \]

\[B= \begin{bmatrix} 5&6&3\\ 2&7&9\\ 8&4&5 \end{bmatrix} \]

\[A\times B= \begin{bmatrix} 1\times5+2\times2+3\times8 & 1\times6+2\times7+3\times4 & 1\times3+2\times9+3\times5\\ 3\times5+1\times2+4\times8 & 3\times6+1\times7+4\times4 & 3\times3+1\times9+4\times5\\ 2\times5+2\times2+1\times8 & 2\times6+2\times7+1\times4 & 2\times3+2\times9+1\times5 \end{bmatrix} \]

上面三个矩阵 \(L^AT_EX\) 打了12min。。。

发现了吗?矩阵的乘积的第 \(i\) 行 \(j\) 列上的数,就是第一个矩阵的第 \(i\) 行乘上第二个矩阵的第 \(j\) 列的和。

矩阵中的 "1":\(\begin{bmatrix} 1&0\\ 0&1 \end{bmatrix}\)

这种对角线全是 \(1\) 的矩阵,任意矩阵乘上它不会变。

矩阵乘法不满足交换律,满足结合律。

\(\begin{bmatrix} 0&1\\ 1&0 \end{bmatrix}\times\begin{bmatrix} 11&4\\ 5&14 \end{bmatrix}\neq\begin{bmatrix} 11&4\\ 5&14 \end{bmatrix}\times\begin{bmatrix} 0&1\\ 1&0 \end{bmatrix}\)

矩阵快速幂

由于矩阵乘法满足结合律,所以对于矩阵 \(M^k\),可以拆分成 \(M^{2^1}\times M^{2^2}\times...\times M^{2^l}\) 的形式,也就是快速幂。

代码用封装 + 重载乘法实现。

#include<bits/stdc++.h>
#define int long long
using namespace std;
const int mod=1e9+7;
struct TVO{
	int c[105][105];
}A,B;
int n,k;
TVO operator*(const TVO &x,const TVO &y) {
	TVO a;
	for(int i=1;i<=n;i++)
		for(int j=1;j<=n;j++)
			a.c[i][j]=0;
	for(int i=1;i<=n;i++){
		for(int j=1;j<=n;j++){
			for(int k=1;k<=n;k++){
				a.c[i][j]=(a.c[i][j]+x.c[i][k]*y.c[k][j]+mod)%mod;
			}
		}
	}
	return a;
}
signed main(){
	ios::sync_with_stdio(0);
	cin.tie(0),cout.tie(0);
	cin>>n>>k;
	for(int i=1;i<=n;i++)
		for(int j=1;j<=n;j++)
			cin>>A.c[i][j];
	for(int i=1;i<=n;i++) B.c[i][i]=1;
	while(k>0){
		if(k%2==1) B=B*A;
		A=A*A,k>>=1;
	}
	for(int i=1;i<=n;i++){
		for(int j=1;j<=n;j++)
			cout<<B.c[i][j]<<" ";
		cout<<"\n";
	}
	return 0;
}

嗯呐。

矩阵快速幂优化递推

看经典的斐波那契数列:

\[f_n=f_{n-1}+f_{n-2} \]

我们用一个矩阵 \(\begin{bmatrix}f_{n}&f_{n-1}\end{bmatrix}\) 记录斐波那契数列。

想要求出这个矩阵,我们需要知道 \(\begin{bmatrix}f_{n-1}&f_{n-2}\end{bmatrix}\)

假设我们知道 \(\begin{bmatrix}f_{n-1}&f_{n-2}\end{bmatrix}\) 时。

目标求出 \(f_n\),需要 \(f_{n-1}\)、\(f_{n-2}\),矩阵的第一列就是 \(1,1\)

目标求出 \(f_n-1\),需要 \(f_{n-1}\),矩阵的第二列就是 \(0,1\)

所以设矩阵 \(M=\begin{bmatrix}0&1\\1&1\end{bmatrix}\),答案矩阵 = \(\begin{bmatrix}1&1\end{bmatrix}\times M^n\)。

\(M^n\) 可以容矩阵快速幂解决。

2.28 组合数学入门总结

记了关键点,但好像只记了关键点。。。

加法原理 AND 乘法原理

容斥原理

\[A\ \cup\ B = A + B - A\ \cap\ B \]

例题:质数倍数计数

image

懒得打latex

可以用状压的思想枚举,然后根据奇数加偶数减的原理统计答案。

for(int i=0;i<m;i++) cin>>p[i];
int ans=0;
for(int s=1;s<(1<<m);s++){
	int cnt=1;
	for(int i=0;i<m;i++){
		if(s&(1<<i)) cnt*=p[i];
	}
	ans+=(__builtin_popcount(s)&1?1:-1)*(n/cnt);
}

排列数 AND 组合数

Lucas定理

\[C^{n}_{k}=C^{n/p}_{k/p}\times C^{n\%p}_{k\%p}\%p \]

除法向下取整。

特殊排列组合

怎么想出来的?看课件。

圆排列

从 \(n\) 个元素的序列 \(A\) 中,取出 \(k\) 个元素排列成一个圆,求使得不同的圆排列的排列数。

排列数 = \(\frac{A^{r}_{n}}{r}\)

有重复元素的全排列

例子:\([1,1,3,2,3,3,3,2]\) 的全排列。

设 \(i\) 出现了 \(P_i\) 次。

排列数:

\[\frac{(P_1+P_2+P_3+...+P_n)!}{P_1!\times P_2! \times P_3!\times...\times P_n!} \]

不相邻组合

要求相差为 \(1\) 的元素不能相邻。

不相邻组合就是从一个有 \(n\) 个元素的序列中、取 \(r\) 个不相邻的数进行组合。

组合数:

\[C^{r}_{n-r+1} \]

有重复元素的组合

从有 \(n\) 个不同的元素的序列中取 \(r\) 个元素的组合数,允许序列中有重复元素,允许有空的组合。

例:有三个球,放进两个相同的盒子,盒子可以为空,问方案数。

组合数:

\[C^{r}_{n-r+1} \]

错位排列

摆在 \(chen\_zhe\) 面前有编号是 \(1\)、\(2\)、...、\(n\) 的 \(n\) 封寄给 \(kkksc03\) 的信,把它们装入编号为 \(1\)、\(2\)、...、\(n\) 的 \(n\) 个信封,要求每封信和信封的编号不同,问有多少种装法?

设 \(D(x)\) 为 \(x\) 个不同元素的错排方案。

\[D(x)= \begin{cases} x-1 & x=1,2 \\ (x-1) \times [D(x-1)+D(x-2)] & x \in N^*,x\neq 1,2 \end{cases} \]

球盒问题

\[\begin{array}{|c|c|c|c|c|} \hline \text{问题编号} & \text{球(数量为n)} & \text{盒(数量为k)} & \text{盒是否可空} & \text{公式} \\ \hline 1 & \text{相同} & \text{不同} & \text{否} & C_{n-1}^{k-1} \\ \hline 2 & \text{相同} & \text{不同} & \text{是} & C_{n+k-1}^{k-1} \\ \hline 3 & \text{相同} & \text{相同} & \text{否} & P(n,k) \\ \hline 4 & \text{相同} & \text{相同} & \text{是} & P(n+k,k) \\ \hline 5 & \text{不同} & \text{相同} & \text{否} & S(n,k) \\ \hline 6 & \text{不同} & \text{相同} & \text{是} & \sum_{i=1}^{k} S(n,i) \\ \hline 7 & \text{不同} & \text{不同} & \text{否} & S(n,k) \times k! \\ \hline 8 & \text{不同} & \text{不同} & \text{是} & k^n \\ \hline \end{array} \]

1、2、4、6、7、8太简单不记。

3

递推公式为:\(P[n,k]=P[n-1,k-1]+P[n-k,k]\) (至少有1份是1,或者每一份数量都大于1)

边界:\(P[1,1]=1\),\(P[i,i]=i\),\(P[i,1]=0\)

5

递推公式为:\(S[n,k]=S[n-1,k-1]+k\times S[n-1,k]\)

posted @ 2026-07-20 20:53  vivid/stasis  阅读(41)  评论(0)    收藏  举报