[总结] 基础算法(二)

[总结] 基础算法(二)

目录

  • 分治
  • 字符串
  • 倍增
  • 贪心
  • 搜索

分治

分而治之

  • 将一个难以直接解决的大问题,分割成一些规模较小的相同问题。

  • 将枚举统计分成若干层,若干个部分。

  • 每个部分统计一些贡献。

  • 经典算法:归并排序,快速排序

分治具有的基本特征

  1. 该问题缩小到一定规模后可以容易地得到答案(边界
  2. 该问题可以分解为若干个规模较小的相同问题,利用该问题分解出的子问题的解可以合并为该问题的解
  3. 该问题所分解出的各个子问题是相互独立的,不包含公共的子问题

高级分治

  • \(CDQ\) 分治
  • 点、边分治
  • 线段树分治
  • \(FFT\)

纯分治很少用到


例题

P2320 [HNOI2006]鬼谷子的钱袋

  • 给出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 $,尽量多,尽量大

维护哈希值:

  • 处理整串哈希值
  • 提取子串哈希值
  • 拼接串,插入串,删除串后的哈希值

哈希的用途

  1. 判断一个字符串之前是否出现过
  2. 判断字符串是否相等。取hash段比较即可, \(O(1)\)
  3. 找某两个位置开始的 \(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用处

  1. 模式串在主串中出现的次数。
  2. 求一个串的循环节:长度为 \(n\) 的字符串的最短循环节是 \(n-nxt[n]\)
  3. \(n\%(n-nxt[n])=0\) 时,字符串是一个循环字符串。最长循环次数为 \(n/(n-nxt[n])\)

例题

posted @ 2021-08-12 17:01  ¶凉笙  阅读(14)  评论(0编辑  收藏  举报