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\),不方便之和推式子可以得到:
前两项好求,最后那一项难求。
考虑树剖,对每个节点 \(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\) 的二叉树的数量。
楼梯切割

这样的楼梯被红框切割成 \(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 水の三角(修改版)
首先压成这样的图:

没有斜线就是卡特兰数板子。
有斜线的话,发现 \(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 矩阵乘法、矩阵快速幂总结
矩阵
就是一个矩阵,嗯。
发明出来据说是为了偷懒。
矩阵乘法
不是传统意义上的相乘,只要矩阵 \(A\) 的列数等于矩阵 \(B\) 的行数,就可以做矩阵乘法。
举一个简单的例子:
上面三个矩阵 \(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;
}
嗯呐。
矩阵快速幂优化递推
看经典的斐波那契数列:
我们用一个矩阵 \(\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 乘法原理
容斥原理
例题:质数倍数计数

懒得打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定理
除法向下取整。
特殊排列组合
怎么想出来的?看课件。
圆排列
从 \(n\) 个元素的序列 \(A\) 中,取出 \(k\) 个元素排列成一个圆,求使得不同的圆排列的排列数。
排列数 = \(\frac{A^{r}_{n}}{r}\)
有重复元素的全排列
例子:\([1,1,3,2,3,3,3,2]\) 的全排列。
设 \(i\) 出现了 \(P_i\) 次。
排列数:
不相邻组合
要求相差为 \(1\) 的元素不能相邻。
不相邻组合就是从一个有 \(n\) 个元素的序列中、取 \(r\) 个不相邻的数进行组合。
组合数:
有重复元素的组合
从有 \(n\) 个不同的元素的序列中取 \(r\) 个元素的组合数,允许序列中有重复元素,允许有空的组合。
例:有三个球,放进两个相同的盒子,盒子可以为空,问方案数。
组合数:
错位排列
摆在 \(chen\_zhe\) 面前有编号是 \(1\)、\(2\)、...、\(n\) 的 \(n\) 封寄给 \(kkksc03\) 的信,把它们装入编号为 \(1\)、\(2\)、...、\(n\) 的 \(n\) 个信封,要求每封信和信封的编号不同,问有多少种装法?
设 \(D(x)\) 为 \(x\) 个不同元素的错排方案。
球盒问题
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]\)

浙公网安备 33010602011771号