[总结] 基础算法(二)
[总结] 基础算法(二)
目录
- 分治
- 字符串
- 倍增
- 贪心
- 搜索
分治
分而治之
-
将一个难以直接解决的大问题,分割成一些规模较小的相同问题。
-
将枚举统计分成若干层,若干个部分。
-
每个部分统计一些贡献。
-
经典算法:归并排序,快速排序
分治具有的基本特征
- 该问题缩小到一定规模后可以容易地得到答案(边界)
- 该问题可以分解为若干个规模较小的相同问题,利用该问题分解出的子问题的解可以合并为该问题的解。
- 该问题所分解出的各个子问题是相互独立的,不包含公共的子问题
高级分治
- \(CDQ\) 分治
- 点、边分治
- 线段树分治
- \(FFT\)
纯分治很少用到
例题
- 给出m,把m拆成若干个数,使得每个大于1的数互不相同,且这若干个数能凑出 \([1,m]\) 所有的数。
- 最小化拆分出来的数字个数,并且输出方案
题解
- 如果可以表示出 \([1,x]\) 和 \(y\) ,那么也可以表示出 \([1+y,1+x+y]\)
- 所以如果想表示出 \([1,m]\) ,只需要表示出 \([1,(m+1)/2]\) 和 \(m/2\) ,递归下去,边界是1
//鬼谷子的钱袋
//by Liang Sheng
#include <iostream>
#include <cstdio>
#define re register
using namespace std;
const int maxn = 1e6;
int n,a[maxn],cnt;
inline void solve(int n){
while(n){
a[++cnt]=(n+1)/2;
n/=2;
}
cout<<cnt<<endl;
for(re int i=cnt;i>=1;i--)cout<<a[i]<<" ";
}
int main(){
scanf("%d",&n);
solve(n);
return 0;
}
Painting
【题目描述】
有一块有 \(n\) 段的栅栏,要求第 \(i\) 段栅栏最终被刷成颜色 \(c_i\)。每一次可以选择一段区间 $ l \cdots r$ 刷成某种颜色,后刷的颜色会覆盖之前的。一共有 \(m\) 种颜色。雇主知道只需要用 \(m\) 次就能达成目标,因此你只能刷 \(m\) 次。你希望最大化 \(m\) 次刷漆选择的区间长度 \((r-l+1)\) 总和。
【数据范围】
\(1\leq n\leq 10^5,m\leq5000\)
题解
- 由于颜色之间不存在包含的关系,所以像贪心模型
- 考虑拿到一个区间怎么处理
\(e.g.\) 对于区间 \(AEEGEABBBCDDC\)
- 假设\(a[l]==a[r]\),一定先刷这种颜色再地柜下去
- 根据贪心思想得出结论
- 里面的夹心是不可能先处理的
- 先刷两边长度较小的一个(长度相同再向内比较,目的是先把小的解放出来)
复杂度 \(O(m^2+n)\)
字符串
字符串哈希
- 选择进制:\(26,131,13331\)
- 选择模数:$unsigned long long/1e9+7/998244353 $,尽量多,尽量大
维护哈希值:
- 处理整串哈希值
- 提取子串哈希值
- 拼接串,插入串,删除串后的哈希值
哈希的用途
- 判断一个字符串之前是否出现过
- 判断字符串是否相等。取hash段比较即可, \(O(1)\)
- 找某两个位置开始的 \(LCP\)(最长公共前缀),二分位置+ \(hash\) 判
断 \(O(logn)\)
例题
兔子与兔子
基本的提取子串操作:
- 对于任意区间 \([l,r]\) 的哈希值为 \(F[r]-F[l-1]*P^{r-l+1}\),\(P\) 为进制底,\(F\) 数组表示 \(1-i\) 的哈希值。(其中 \(P^{r-l+1}\)可以预处理出 )
for(int i=1;i<=n;i++){
f[i]=f[i-1]*P+(s[i]-'a'+1);
p[i]=p[i-1]*P;
}
[NOI2016]优秀的拆分(95分)
- 把串拆成形似 \(AABB\) 称为一次优秀的拆分
- 一个串可能有多个优秀的拆分。
- 给出长度为n的字符串,求所有的子串的优秀拆分个数的和。
题解
设 \(L[i]\) 表示以 \(i\) 结尾的形如 \(AA\) 串的个数,\(R[i]\) 表示以 \(i+1\) 开头的形如 \(AA\) 串的个数。
则答案为 \(\sum L_iR_i\)。
利用哈希可以 \(O(n^2)\) 拿到 \(95pts\)
using namespace std;
const int maxn=1e5+3;
const int mod=99824353;
long long base[maxn],hash[maxn],T;
int n;
int pre[maxn],nxt[maxn];
char s[maxn];
long long gethash(int l,int r){
long long ret=hash[r]-hash[l-1]*base[r-l+1];
return (ret%mod+mod)%mod;
}
int main(){
scanf("%lld",&T);
base[0]=1;
for(int i=1;i<=30000;i++){
base[i]=base[i-1]*37;
base[i]=(base[i]%mod+mod)%mod;
}
while(T--){
memset(pre,0,sizeof pre);
memset(nxt,0,sizeof nxt);
scanf("%s",s+1);
n=strlen(s+1);
for(int i=1;i<=n;i++){
hash[i]=hash[i-1]*base[1]+s[i]-'a'+1;
hash[i]=(hash[i]%mod+mod)%mod;
}
for(int i=1;i<=n;i++){
for(int len=1;i-len*2>=0;len++){
if(gethash(i-len+1,i)==gethash(i-len*2+1,i-len)){
pre[i]++;
}
}
}
for(int i=n;i>=1;i--){
for(int len=1;i+len*2-1<=n;len++){
if(gethash(i,i+len-1)==gethash(i+len,i+len*2-1)){
nxt[i]++;
}
}
}
long long ans=0;
for(int i=1;i<=n;i++){
ans+=pre[i]*nxt[i+1];
}
printf("%lld\n",ans);
}
}
哈希冲突的解决方法
- 取大质数为模数(\(1015\)以上)
- 双哈希(模数不同,或者一个 \(int\) 一个 \(unsigned \ long \ long\))
KMP
给出两个串A,B,求B字符串在A字符串中出现的位置
(当然,我们可以用哈希求)
- 算法关键: 关键是利用匹配失败后的信息,尽量减少模式串与主串的匹配次数以达到快速匹配的目的。 \(O(n+m)\)
- \(nxt[i]\) 表示,\(b[1-i]\) 这个前缀中,最长的前缀等于后缀的长度(除了本身),或者说是以 \(i\) 为结尾的非前缀子串与 \(b\) 的前缀能够匹配的最大长度
小结论
- 哪个串小就和哪个串的前缀匹配,这个小串就是大串的子串
对 \(b\) 进行自我匹配
for(int i=2,j=0;i<=lenb;i++){
while(j>0 && b[i]!=b[j+1])j=nxt[j];
if(b[i]==b[j+1])j++;
nxt[i]=j;
}
匹配 \(a\) 和 \(b\)
for(int i=1,j=0;i<=lena;i++){
while(j>0 && (a[i]!=b[j+1]))j=nxt[j];
if(a[i]==b[j+1])j++;
if(j==lenb){
printf("%d\n",i-j+1);
j=nxt[j];
}
}
\(i\) 是从 1 开始的!
KMP用处
- 模式串在主串中出现的次数。
- 求一个串的循环节:长度为 \(n\) 的字符串的最短循环节是 \(n-nxt[n]\)
- 当 \(n\%(n-nxt[n])=0\) 时,字符串是一个循环字符串。最长循环次数为 \(n/(n-nxt[n])\)