习题课4-3
字符串匹配
- 一个大串A和一个模式串(小串)B,求B在A的哪些位置出现
kmp算法
- 1.构造next数组,使得你在任意时刻匹配失败的时候都可以立即找到新的模式串中的匹配位置
- 2.匹配,可以暴力的从左往右匹配大串,使用next数组实现快速跳转。
什么叫next数组
- 从第一位开始匹配,直到不相同,记录了最长长度的匹配,下次匹配,直接从这个位置开始匹配
- 然后反向从上次记录的位置往左比较,比较出大串中记录位置为终止点的子串的最长的后缀跟模式串匹配的长度
- 举例 a b c a b a 模式串 a b x
- next 0 0 0 1 2
- 整个模式串的匹配看成是一张图,能够匹配就继续往后走,不能就回到起点,回到起点的位置是根据next数组的值来确定的,next数组实际上存的是所有回去的边
- 大串ababcabcaba 模式串abcaba
- fori 从start开始,a匹配上,移动到a,b匹配,移动b,c不匹配,沿着next指针,回到start,回到start,重新匹配,不匹配的话一直往回走,直到回到起点,退无可退的时候,在此处的情况是a,又匹配上,第二位是b,也匹配上,前两位匹配上,一直比较到最后一位,不匹配,沿着next指针回到模式串的第二位b,然后第三位刚好和大串匹配上了,然后又开始往后匹配,最后正好匹配上了,一直到达终点end,也就找到了一个匹配。
构建next数组
-
模式串abcaba
-
0 1 2 3 4 5 str a b c a b a next -1 -1 -1 0 1 0 -
求next数组,就是模式串自我匹配的过程
-
表格中a匹配错了只能回到-1,也就是要从0重新开始比较,
-
第3位a为什么next是1,因为前3位能够和前0位进行一个后缀的匹配
-
当i=4时,3处是next(i-1),如果匹配上,就是next(i-1)+1,所以b位置的next就是1,第5位不匹配,只能回到-1,然后和0位的a比较,能够匹配,所以是0
代码解析
匹配过程
-
while (j>=0 && A.charAt(i)!=B.charAt(j+1)) { j = Next[j]; }
-
为什么j>=0是因为防止退无可退
-
if (j == m-1) { ans.add(i-m+1); }
-
完全匹配了整个模式串,匹配的起点,就等于大串当前位置减去模式串长度
next数组
-
int j = Next[0] = -1;
-
记录start位置,是模式串的前一位,假设出来的界桩位置
-
其余代码与匹配过程类似,最后需要记录当前比较位置的next指针
复杂度分析
- 为什么是O(n),for里面有个while
- while执行的前提是++j,所以while执行的次数是由++j决定的,而++j执行的次数是小于等于n的,所以while部分执行的均摊复杂度是O(1),整体是O(n)
标程
static class Task {
/* 请在这里定义你需要的全局变量 */
// 这是匹配函数,将所有匹配位置求出并返回
// n:串 A 的长度
// A:题目描述中的串 A
// m:串 B 的长度
// B:题目描述中的串 B
// 返回值:一个 List<Integer>,从小到大依次存放各匹配位置
List<Integer> match(int n, String A, int m, String B) {
// 初始化
int[] Next = new int[m];
// j为匹配失败时跳转到的位置
int j = Next[0] = -1;
for (int i = 1; i < m; i++) {
// 如果下一位无法匹配,则利用next数组跳转
while (j>=0 && B.charAt(i)!=B.charAt(j+1)) {
j = Next[j];
}
if (B.charAt(i) == B.charAt(j+1)) {
// 当前位置匹配
++j;
}
// 记录当前位的next数组
// next数组记录的是当前位置匹配失败后跳转的位置
Next[i] = j;
}
j = -1;
List<Integer> ans = new ArrayList<>();
for (int i = 0; i < n; i++) {
// 如果下一位无法匹配,则利用next数组跳转
while (j>=0 && A.charAt(i)!=B.charAt(j+1)) {
j = Next[j];
}
// 为什么要再判断一次?
// 因为是退无可退,并且第一位匹配上
if (A.charAt(i) == B.charAt(j+1)) {
// 当前位置匹配,++j
// j是匹配的指针
++j;
}
// 如果整个模式串匹配,则找到一个答案
// i和m-1对应,和0对应的就是i-(m+1)
if (j == m-1) {
ans.add(i-m+1);
}
}
return ans;
}
void solve(InputReader in, PrintWriter out) {
int n = in.nextInt();
String A = in.next();
int m = in.nextInt();
String B = in.next();
List<Integer> ans = match(n, A, m, B);
for (Integer i : ans)
out.println(i);
}
}
最大间隙
-
后面两道题都是非常大数据量的题
-
数轴上有n个点乱序排序,找出相差最大的两个点
-
n很大,无法排序,时间复杂度必须控制在O(n)
hash
- 把值域平均划分成若干段
- 每一段设置一个桶
- 根据编号把每一个数丢到对应的桶(整除运算)
- 最大间隙要么在桶内部,要么在相邻的两个非空桶之间
- 每个桶都需要维护一个最大值和最小值,就是用右边的最小值减去左边相邻非空桶的最大值
- 难点在于桶内
- 一个解决问题的方法就是让问题消失
- 鸽巢原理(抽屉原理)–全局的最大间隙一定不会小于s/n
- 如果ans>=100,如果每一段只有80长度,则答案一定不在桶内
- 所以直接把每一段的大小设置成s/n
- 最后只需要扫描一遍所有的桶,就可以找到答案
代码解析
-
值域6710884 (2^26)
-
桶的个数26
-
_k辅助用位运算来代替除法
-
final int _k = Math.max(k-26,0); int bl = a[i] >>> _k;
-
找出在哪个桶
-
if ( l[i]-last > ans) { ans = l[i] - last ; }
-
当前的桶的最小值和上一个桶的最大值做比较,由于是哈希运算,上一个桶的最大值也是可能大于当前桶的最小值的,所以需要比较更新下
-
因为此题是随机数,所以数据量大能够满足每一段的长度是大于64的,对于非随机数,此时需要考虑长度小于64的情况
标程
#include <bits/stdc++.h>
using namespace std;
typedef unsigned int u32;
// 以下代码不需要解释,你只需要知道这是用于生成数据的就行了
u32 nextInt(u32 x) {
x ^= x << 13;
x ^= x >> 17;
x ^= x << 5;
return x;
}
void initData(u32* a, int n, int k, u32 seed) {
for (int i = 0; i < n; ++i) {
seed = nextInt(seed);
a[i] = seed >> (32 - k);
}
}
// 以上代码不需要解释,你只需要知道这是用于生成数据的就行了
const int N = 67108864;
u32 a[N+1];
u32 l[N+1],r[N+1];
// 这是求解答案的函数,你需要对全局变量中的 a 数组求解 maxGap 问题
// n, k:意义与题目描述相符
// 返回值:即为答案(maxGap)
u32 maxGap(int n, int k) {
// 初始化
// 确定桶的个数
// 值域是2^32
// 因为N是2^26,所以桶的数量就用26表示
// 桶的个数
// 所以每一段的长度是2^6
const int m =1 << 26;
// 将l中的所有位置赋值为-1,l是最小值
memset(l,-1,sizeof(int)*m);
// 将r中的所有位置赋值为-1,r是最大值
memset(r,-1,sizeof(int)*m);
// 这是一个参数,辅助后续用位运算代替除法
// 保证它非负的情况下减去26
const int _k = max(k-26,0);
for(int i =0; i<n;++i){
// 这个式子等价于a[i]除以2的_k次幂,求出a[i]所在的桶
u32 bl = a[i] >> _k;
// 更新对应桶的l,r
if(l[bl] == -1)
l[bl] = r[bl] = a[i];
else if(a[i]<l[bl])
l[bl] = a[i];
else if(a[i]>r[bl])
r[bl] = a[i];
}
// 统计答案
u32 last = a[0];
u32 ans =0;
for(int i = 0;i<m;++i)
// 判断桶是否非空
if(l[i] != -1){
// 修正last
// last是前面的最大值
if(last>l[i])
last = l[i];
// 更新最大间隙
if(l[i] - last >ans)
ans = l[i] - last;
last = r[i];
}
return ans;
}
int main() {
int n, k;
u32 seed;
scanf("%d%d%u", &n, &k, &seed);
initData(a, n, k, seed);
u32 ans = maxGap(n, k);
printf("%u\n", ans);
return 0;
}
基数排序
- 值域的范围是[0,2^32)
- 先考虑[0,2^16)问题,考虑桶排序方法
- 依次将数丢到相应的桶类,然后按桶的顺序枚举桶里面的数
桶排序
- 丢进来一个数x,统计该桶的cnt[]
- 丢完所有的数后,对所有的桶的数组求一遍前缀和,桶数组cnt[],等于该桶当前最后一名的名次,或者小于等于这个值的个数
- cnt[x]表示x出现的次数,对cnt数组求一个前缀和,表示的就是小于等于x出现的次数
- 可以保留很多信息,对于多关键字排序也是适用的
- 常数操作的复杂度也是有区别的,数组某个位置赋值,和vector.push_back的复杂度就是不一样的
- 并列的处理,强行对每个元素的初始位置来避免并列,原来更靠前的就更靠前
- 把2^32切割成两部分,先把16位按第一关键字排序,再把后16位按第二关键字排序,此处是双关键字排序,也就可以类比推广到多关键字排序
代码解析
-
实际分了2^16个桶,0,1,2,3…一直到2的16次幂,正常情况下一个桶只有1个元素,当排序数组有重复元素时,桶内元素才可能超过1个
-
for (int i = 0; i < n; i++) { ++sum[a[i]&b]; }
-
sum数组就是cnt
-
先取出后16位(第二关键字)比较,排完序后再比较前十六位(第一关键字)
-
_a[--sum[a[i]&b]] = a[i];
-
从后往前依次取出桶中的元素,sum中此时存的是该元素的名次,比如9,4,3,10,999,此时999的名次是5,而在数组元素是从0开始的,所以要–
标程
#include <bits/stdc++.h>
using namespace std;
typedef unsigned int u32;
// 以下代码不需要解释,你只需要知道这是用于生成数据的就行了
u32 nextInt(u32 x) {
x ^= x << 13;
x ^= x >> 17;
x ^= x << 5;
return x;
}
void initData(u32* a, int n, int k, u32 seed) {
for (int i = 0; i < n; ++i) {
seed = nextInt(seed);
a[i] = seed >> (32 - k);
}
}
u32 hashArr(u32* a, int n) {
u32 x = 998244353, ret = 0;
for (int i = 0; i < n; ++i) {
ret ^= (a[i] + x);
x = nextInt(x);
}
return ret;
}
// 以上代码不需要解释,你只需要知道这是用于生成数据的就行了
const int N = 100000000;
u32 a[N + 1];// 待排序数组
u32 _a[N + 1];// 辅助基数排序的临时数组
const int m = 16;// 基数排序中的参数,表示每次排序的二进制位数
const int B = 1 << m;// 2的m次幂,表示一次排序中的值域
const int b = B - 1;// 这是一个用于辅助计算的常值,它在二进制下的最低m位均为1
// sum:在基数排序中记录各值出现的次数
int sum[B + 1];
// 这是你的排序函数,你需要将全局变量中的 a 数组进行排序
// n, k:意义与题目描述相符
// 返回值:本函数需不要返回值(你只需要确保 a 被排序即可)
void sorting(int n, int k) {
// 第一轮对后16位进行排序
// 针对a的后16位进行基数排序
// 将sum数组清零,sum数组就是桶数组
memset(sum, 0, sizeof(sum));
// 统计待排序二进制位各值的出现次数
for(int i = 0; i < n; ++i)
// a[i] & b等价于取出a[i]的对应二进制位
++sum[a[i] & b];
// 接下来需要对sum数组进行一些操作
// 计算前缀和
for(int i = 1; i < B; ++i)
sum[i] += sum[i - 1];
// 求出每个位置每个值的名次
// 取出cnt[a[i]],需要cnt[a[i]] -= 1,取出后,桶中数目要减一
for(int i = n - 1; i >= 0; --i)
_a[sum[a[i] & b]--] = a[i];
// 第二轮对前16位排序
// 再重复一遍上面的基数排序,只不过这次是对_a的前16位排序,并存入a中
memset(sum, 0, sizeof(sum));
for(int i = 0; i < n; ++i)
++sum[(_a[i] >> m) & b];
for(int i =1; i < B; ++i)
sum[i] += sum[i - 1];
for(int i = n - 1; i >= 0; --i)
a[sum[(_a[i] >> m) & b]--] = _a[i];
}
int main() {
int n, k;
u32 seed;
scanf("%d%d%u", &n, &k, &seed);
initData(a, n, k, seed);
sorting(n, k);
u32 ans = hashArr(a, n);
printf("%u\n", ans);
return 0;
}