7.15复习笔记
一、组合数
组合数一共有三种处理方式
1.杨辉三角
当n和m都比较小的时候我们可以用这种办法预处理复杂度是\(O(nm)\)的
void init(){
c[0][0] = C[0][1] = 1;
for (int i = 1;i <= n;i++){
C[i][0] = 1;
for (int j = 1;j <= min(i,m);j++) C[i][j] = C[i-1][j]+C[i-1][j-1];
}
}
2.组合数公式
当n和m比较大,但是没有超过P的时候,我们可以预处理出阶乘和逆元用公式来求
int qpow(int a,int k){
int res = 1;a = a % mod;
while (k){
if (k&1) res = res*a % mod;
a = a*a % mod;
k >>= 1;
}
return res % mod;
}
int C(int n,int m){
return power[n]*inv[power[m]] % mod*inv[power[n-m]] % mod;
}
void init(){
power[0] = 1,inv[0] = 1;
for (int i = 1;i <= n;i++) power[i] = power[i-1]*i % mod;
inv[n] = qpow(power[n],mod-2);
for (int i = n-1;i >= 1;i--) inv[i] = inv[i+1]*(i+1) % mod;
}
3.Lucas定理
当n和m比较大,且超过了P的时候,我们就可以用Lucas定理了
至于为什么我们不可以直接公式求呢?因为如果n和m大于P,那么阶乘里就会出现P这一项,取模之后全为0
int qpow(int a,int k){
int res = 1;a = a % mod;
while (k){
if (k&1) res = res*a % mod;
a = a*a % mod;
k >>= 1;
}
return res % mod;
}
int C(int n,int m){
return power[n]*inv[power[m]] % mod*inv[power[n-m]] % mod;
}
void init(){
power[0] = 1,inv[0] = 1;
for (int i = 1;i <= n;i++) power[i] = power[i-1]*i % mod;
inv[n] = qpow(power[n],mod-2);
for (int i = n-1;i >= 1;i--) inv[i] = inv[i+1]*(i+1) % mod;
}
int Lucas(int n,int m){
if (m == 0) return 1;
return C(n % mod,m % mod)*Lucas(n/mod,m/mod) % mod;
}
二、kruskal重构树
简单来讲,就是在Kruskal算法进行的过程中,我们把最小生成树的边权改为点权,加虚点连边,原树的节点个数变成2n-1个
Kruskal重构树的性质
1.根据我们构造的过程,这是一个二叉堆
2.原树两点之间的边权最大值是重构树上两点Lca的权值
3.重构树中代表原树中的点的节点全是叶子节点,其余节点都代表了一条边的边权。
Kruskal重构树的构造
-
首先对边排序
-
使用并查集辅助加边,每新建一条边时:新建节点index(编号从n+1开始)
-
将原有两节点所在集合改为index
-
将原有节点与index连边新建节点的权值为当前边的边权
例题:货车运输
求两点间的最小路径最大
我们想可以贪心的每次加入较大的边,那么两个点第一次连通的时候,此时的边一定是最大的
这个思路就和最小生成树一模一样嘛
这样我们就可以把边从大到小排序,然后建一颗Kruskal重构树,那么两点间的lca就是我对当前货车的限制
给我们了启示,什么情况可以用kruskal重构树,求最小的边最大,最大的边最小,或者第一个大于的边,第一个小于的边
cnt = n;init();
for (int i = 1;i <= m;i++){
int u = edge[i].u,v = edge[i].v,w = edge[i].w;
int fu = find(u),fv = find(v);
if (fu != fv){
fa[fu] = fa[fv] = ++cnt;
val[cnt] = w;
add(cnt,fu),add(cnt,fv);
}
if (cnt == 2*n-1) break;
}
三、状压dp,数位dp
今天恰好讲了一道数位dp套状压dp的题,把这道题写个题解就当做是复习完这两个dp了
HDU 4352 XHXJ's LIS
思路:
1.看到两个限制l和r,大小为1e18,我们很容易就想到数位dp
2.根据nlog求LIS的做法在数位dp的同时维护LIS
前置知识:nlogn求LIS的一种做法
设f[i]表示长度为i的LIS的结束位置的最小的数(显然这样对我后面的转移不会更劣),不难想到f[i]是单调递增的
每一次插入一个新的数,为了维护f[i]的单调性,我们需要找到第一个大于当前数的数,并把它替换掉,否则直接加入末尾
那么在这道题里一样,因为我最多只会有长度为10的LIS,所以我们可以状压每一位表示当前的最长上升子序列中出现了哪几个数
每次加入一个新的数时,把第一个比他大的数替换掉,否则直接加入末尾
那么如何数位dp套状压dp呢?
状态定义:
正常来说dp[i][s]表示当前到第i位,当前最长上升子序列的状态为s的方案数就可以了
但是注意这道题我们要多测且t是1e4的,但是k又只有1~10的取值,数位dp的灵魂又就在于记忆化,所以我们可以多加一维
dp[i][s][k]表示当前到第i位,最长上升子序列的状态为s,所求最长上升子序列的长度为k的方案数
转移的时候注意前导零和最高位的限制,但是我写T了,不过思路和方法懂了,我就没再D
ll update(int x,int state){
for (int i = x;i < 10;i++){
if (state&(1 << i)) return (state^(1 << i)) | (1 << x);
}
return state | (1 << x);
}
bool check(int state){
int cnt = 0;
while (state) cnt += (state & 1),state >>= 1;
return cnt == K;
}
int num[20];
ll dfs(int pos,int state,bool flag,bool lim){
if (pos == 0) return check(state);
if (!lim&&dp[pos][state][K] != -1) return dp[pos][state][K];
int maxx = lim ? num[pos] : 9;ll ans = 0;
for (int i = 0;i <= maxx;i++){
if (flag){
ans += dfs(pos-1,(flag&&(i == 0)) ? 0 : update(i,state),flag&&(i == 0),lim&&(i == maxx));
}
else{
ans += dfs(pos-1,update(i,state),flag&&(i == 0),lim&&(i == maxx));
}
}
if (!lim) dp[pos][state][K] = ans;
return ans;
}
ll solve(ll x){
int cnt = 0;
while (x) num[++cnt] = x % 10,x /= 10;
return dfs(cnt,0,1,1);
}
四、SAM
看看能不能在回酒店之前把SAM整理完,板子:
struct SAM{
int len[maxn],fa[maxn],ch[maxn][27],lastNode,tot;
SAM(){tot = lastNode = 1;}
void add(int c){
int preNode = lastNode;
int nowNode = lastNode = ++tot;
len[nowNode] = len[preNode]+1;
siz[nowNode] = 1;
for (;preNode&&!ch[preNode][c];preNode = fa[preNode]) ch[preNode][c] = nowNode;
if (!preNode) fa[nowNode] = 1;
else{
int tmpNode = ch[preNode][c];
if (len[tmpNode] == len[preNode]+1) fa[nowNode] = tmpNode;
else{
int tmpNode1 = ++tot;
len[tmpNode1] = len[tmpNode],fa[tmpNode1] = fa[tmpNode];
for (int i = 0;i < 26;i++) ch[tmpNode1][i] = ch[tmpNode][i];
len[tmpNode1] = len[preNode]+1;
fa[tmpNode] = fa[nowNode] = tmpNode1;
for (;preNode&&ch[preNode][c] == tmpNode;preNode = fa[preNode]) ch[preNode][c] = tmpNode1;
}
}
}
}sam;
应用:
1.检查一个串是否出现过
对待检查的串一个一个位置匹配,如果有位置不能匹配上证明没出现过
2.不同子串个数
方法1: 利用上述后缀自动机的树形结构。每个节点对应的子串数量是len[i]-len[fa[i]],对自动机所有节点求和即可。
方法2: 每次新增加一个节点的时候说明出现了一个前缀,说明他会比他的父亲少一个endpose,初始化为1,在fail树上dp
3.所有不同子串的总长度
同样可以利用上述后缀自动机的树形结构。每个节点对应的所有后缀长度是(len[i]+1)*len[i]/2,减去其fa节点的对应值就是该节点的净贡献,对自动机所有节点求和即可。
4.字典序第 k 大子串
这个可以参考[TJOI2015]弦论(emmm有些自闭,我再思考思考这个问题吧)
如果本质相同的子串算一个:这个可以参考问题2,即可求出对于每一个节点,以他开始后面随便填的本质不同的子串的个数
5.多个串的最长公共子串
6.查询多个串出现的次数
五、SA
板子:
void getsa(){
for (int i = 1;i <= n;i++) c[x[i] = s[i]]++;
for (int i = 1;i <= m;i++) c[i] += c[i-1];
for (int i = n;i >= 1;i--) sa[c[x[i]]--] = i;
for (int k = 1;k <= n;k <<= 1){
int num = 0;
for (int i = n-k+1;i <= n;i++) y[++num] = i;
for (int i = 1;i <= n;i++) if (sa[i] > k) y[++num] = sa[i]-k;
memset(c,0,sizeof(c));
for (int i = 1;i <= n;i++) c[x[i]]++;
for (int i = 1;i <= m;i++) c[i] += c[i-1];
for (int i = n;i >= 1;i--) sa[c[x[y[i]]]--] = y[i],y[i] = 0;
swap(x,y);
num = 1,x[sa[1]] = 1;
for (int i = 2;i <= n;i++){
x[sa[i]] = (y[sa[i]] == y[sa[i-1]]&&y[sa[i]+k] == y[sa[i-1]+k]) ? num : ++num;
}
if (num == n) break;
m = num;
}
}
void gethigh(){
int k = 0;
for (int i = 1;i <= n;i++){
if (rk[i] == 1) continue;
if (k) k--;
int j = sa[rk[i]-1];
while (j+k <= n&&i+k <= n&&s[i+k] == s[j+k]) k++;
height[rk[i]] = k;
}
}
应用:
1.最小循环移位
把串重复一遍拼在末尾,然后对于前n个后缀查询最小的一个
2.两子串最长公共前缀
找一段区间内height的最小值,问题转化成了区间RMQ问题
3.出现至少 k 次的子串的最大长度
可以二分目标串的长度s,每次贪心的选取最大的height都大于等于s的段,看大小是否超过k-1
4.是否有某字符串在文本串中至少不重叠地出现了两次
可以二分目标串的长度s,将height数组划分成若干个连续LCP大于等于s的段
利用RMQ对每个段求其中出现的数中最大和最小的下标,若这两个下标的距离满足条件,则一定有长度为s的字符串不重叠地出现了两次。
5.两个串的最长公共子串
从左到右扫一遍,因为排名靠近的串越相近,只查询排名相邻的后缀的height就可以了
6.LCP求和
单调栈维护贡献
ll solve(char s[]){
init();
int len = strlen(s+1);
getsa(s,len);gethigh(s,len);
ll ans = 0;
for (int i = 1;i <= len;i++){
int res = 0;
while (height[i] < st[top].val) res += st[top].num, top--;
st[++top].val = height[i], st[top].num = res + 1;
for (int k = 1; k <= top; k++) ans += st[k].val * st[k].num;
}
return ans;
}
六、kmp
kmp数组表示到第i位为止,最长的前缀等于后缀的长度
解决问题:
1.字符串匹配问题
2.最小循环节
对于以\(i\)为结尾的字符串,他的最小循环节长度为\(i-kmp[i]\),如果\(i-kmp[i]|i\),存在循环节,否则不存在
int j = 0;
for (int i = 2;i <= lb;i++){
while (j&&b[i] != b[j+1]) j = kmp[j];
if (b[i] == b[j+1]) j++;
kmp[i] = j
}
j = 0;
for (int i = 2;i <= la;i++){
while (j&&a[i] != b[j+1]) j = kmp[j];
if (a[i] == b[j+1]) j++;
if (j == la) j = kmp[j];
}
七、扩展kmp
exkmp的kmp数组求的是以i开头的后缀与整个串的最长公共前缀
他可以来优化一下暴力?或者题干中需要我们求这样的后缀等于前缀的长度
for (int i = 2,l = 1,r = 1;i <= lenb;i++){
if (i <= r&&kmpA[i-l+1] < r-i+1) kmpA[i] = kmpA[i-l+1];
else{
kmpA[i] = max(0,r-i+1);
while (i+kmpA[i] <= lenb&&b[kmpA[i]+1] == b[i+kmpA[i]]) ++kmpA[i];
}
if (i+kmpA[i]-1 > r) l = i,r = i+kmpA[i]-1;
}
for (int i = 1,l = 0,r = 0;i <= lena;i++){
if (i <= r&&kmpA[i-l+1] < r-i+1) kmpB[i] = kmpA[i-l+1];
else{
kmpB[i] = max(0,r-i+1);
while (i+kmpB[i] <= lena&&b[kmpB[i]+1] == a[i+kmpB[i]]) ++kmpB[i];
}
if (i+kmpB[i]-1 > r) l = i,r = i+kmpB[i]-1;
}