首页 > 其他分享 >「学习笔记」扩展 KMP(Z 函数)

「学习笔记」扩展 KMP(Z 函数)

时间:2023-08-22 09:02:43浏览次数:41  
标签:ch 函数 int void 笔记 while right KMP

对于个长度为 \(n\) 的字符串 \(s\)。定义 \(z[i]\) 表示 \(s\) 和 \(s[i,n-1]\)(即以 \(s[i]\) 开头的后缀)的最长公共前缀(LCP)的长度。\(z\) 被称为 \(s\) 的 Z 函数。这里注意,在 Z 函数中,\(z[0] = 0\),但是根据 LCP 的定义,\(z[0] = n\),具体应该为何值,根据题目意思来判断。本文更偏向根据 LCP 的定义来确定 \(z[0]\) 的值

演示

对于字符串 \(\texttt{aaaaaaaba}\),它的 Z 函数是这样的。

\[z(\texttt{aaaaaaaba}) = \left [9, 6, 5, 4, 3, 2, 1, 0, 1 \right ] \]

过程

我们设现在 \(i + z[i] - 1\) 的最大值为 \(r\),得到这个最大值的 \(i\) 为 \(l\)。

image

假设我们现在要求 \(z[x]\),\(z[0, x - 1]\) 已经求出来了,现在,让我们分类讨论各种情况。

  • 当 \(x \le r\) 时

如图所示,

image

因为 \(s[l, r]\) 等于 \(s[0, r - l]\),所以 \(s[l, x] = s[0, x - l]\),对应到下图,就是绿色区域和黄色区域相同。

image

因此,\(z[x]\) 的取值可以参考 \(z[l - x]\)。

\(z[x]\) 可以直接等于 \(z[l - x]\) 吗?

显然是不行的,像下面的情况,灰色区域为 \(z[l - x]\) 的长度,但是,对于 \(x\),有一小段的灰色区域超出了红色区域,因此不保证这段灰色区域与前面灰色区域的对应位置相等,所以,我们正确的写法应该是 \(z[x] = \min \{z[l - x], r - x + 1 \}\),随后再暴力拓展。

image

  • 当 \(x > r\) 时

没有“前车之鉴”,我们直接进行暴力拓展即可。


代码中的 \(i\) 就是 \(x\)。

if (i <= r) {
    z[i] = min(z[i - l], 1ll * r - i + 1);
}

暴力拓展 + 修改 \(l, r\)

注意要判断边界,同时判断 \(x + z[x] - 1\) 与 \(r\) 的大小更新 \(l, r\),相信你可以看懂这段代码。

while (i + z[i] < len and s[z[i]] == s[i + z[i]]) {
    ++ z[i];
}
if (i + z[i] - 1 > r) {
    l = i;
    r = i + z[i] - 1;
}

拼凑一下,就是 Z 函数(或者是扩展 KMP)的代码了。

void Z(char* s, ll* z) {
    int len = strlen(s), l = 0, r = 0;
    rep (i, 1, len - 1, 1) {
        if (i <= r) {
            z[i] = min(z[i - l], 1ll * r - i + 1);
        }
        while (i + z[i] < len and s[z[i]] == s[i + z[i]]) {
            ++ z[i];
        }
        if (i + z[i] - 1 > r) {
            l = i;
            r = i + z[i] - 1;
        }
    }
}

匹配所有子串

为了避免混淆,我们将 \(t\) 称作 文本,将 \(p\) 称作 模式。所给出的问题是:寻找在文本 \(t\) 中模式 \(p\) 的所有出现。

为了解决该问题,我们构造一个新的字符串 \(s = p + \diamond + t\),也即我们将 \(p\) 和 \(t\) 连接在一起,但是在中间放置了一个分割字符 \(\diamond\)(我们将如此选取 \(\diamond\) 使得其必定不出现在 \(p\) 和 \(t\) 中)。

首先计算 \(s\) 的 Z 函数。接下来,对于在区间 \([0,\left |t \right | - 1]\) 中的任意 \(i\),我们考虑以 \(t[i]\) 为开头的后缀在 \(s\) 中的 Z 函数值 \(k = z[i + \left |p \right | + 1]\)。如果 \(k = \left |p \right |\),那么我们知道有一个 \(p\) 的出现位于 \(t\) 的第 \(i\) 个位置,否则没有 \(p\) 的出现位于 \(t\) 的第 \(i\) 个位置。

其时间复杂度(同时也是其空间复杂度)为 \(O(\left |t \right | + \left |p \right |)\)。

// 匹配 A 在 B 中的所有出现
void Z(char* s, ll* z) {
    int len = strlen(s), l = 0, r = 0;
    rep (i, 1, len - 1, 1) {
        if (i <= r) {
            z[i] = min(z[i - l], 1ll * r - i + 1);
        }
        while (i + z[i] < len and s[z[i]] == s[i + z[i]]) {
            ++ z[i];
        }
        if (i + z[i] - 1 > r) {
            l = i;
            r = i + z[i] - 1;
        }
    }
}

void get_ext() {
    strcpy(p, b);
    strcat(p, "#");
    strcat(p, a);
    Z(p, z);
}

P5410 【模板】扩展 KMP(Z 函数) - 洛谷 | 计算机科学教育新生态 (luogu.com.cn)

//The code was written by yifan, and yifan is neutral!!!

#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
#define bug puts("NOIP rp ++!");
#define rep(i, a, b, c) for (int i = (a); i <= (b); i += (c))
#define per(i, a, b, c) for (int i = (a); i >= (b); i -= (c))

template<typename T>
inline T read() {
    T x = 0;
    bool fg = 0;
    char ch = getchar();
    while (ch < '0' || ch > '9') {
        fg |= (ch == '-');
        ch = getchar();
    }
    while (ch >= '0' && ch <= '9') {
        x = (x << 3) + (x << 1) + (ch ^ 48);
        ch = getchar();
    }
    return fg ? ~x + 1 : x;
}

const int N = 2e7 + 5;

ll z[N << 1];
char a[N], b[N], p[N << 1];

void input() {
    scanf("%s", a);
    scanf("%s", b);
}

void Z(char* s, ll* z) {
    int len = strlen(s), l = 0, r = 0;
    rep (i, 1, len - 1, 1) {
        if (i <= r) {
            z[i] = min(z[i - l], 1ll * r - i + 1);
        }
        while (i + z[i] < len and s[z[i]] == s[i + z[i]]) {
            ++ z[i];
        }
        if (i + z[i] - 1 > r) {
            l = i;
            r = i + z[i] - 1;
        }
    }
}

void get_ext() {
    strcpy(p, b);
    strcat(p, "#");
    strcat(p, a);
    Z(p, z);
}

void solve() {
    int lenz = strlen(b);
    int lenext = strlen(p);
    ll ans = 0;
    z[0] = lenz;
    rep (i, 0, lenz - 1, 1) {
        ans = ans ^ ((i + 1) * (z[i] + 1));
    }
    cout << ans;
    putchar('\n');
    ans = 0;
    rep (i, lenz + 1, lenext - 1, 1) {
        ans = ans ^ ((i - lenz) * (z[i] + 1));
    }
    cout << ans;
    putchar('\n');
}

int main() {
    input();
    get_ext();
    solve();
    return 0;
}

字符串整周期

给定一个长度为 \(n\) 的字符串 \(s\),找到其最短的整周期,即寻找一个最短的字符串 \(t\),使得 \(s\) 可以被若干个 \(t\) 拼接而成的字符串表示。

考虑计算 \(s\) 的 Z 函数,则其整周期的长度为最小的 \(n\) 的因数 \(i\),满足 \(i+z[i]=n\)。

题目

P7114 [NOIP2020] 字符串匹配 - 洛谷 | 计算机科学教育新生态 (luogu.com.cn)

求出每个位置的 Z 函数,通过判断 \((AB)\) 个数的奇偶来计算出现奇数次字符的个数,用树状数组维护。

//The code was written by yifan, and yifan is neutral!!!

#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
#define bug puts("NOIP rp ++!");
#define rep(i, a, b, c) for (int i = (a); i <= (b); i += (c))
#define per(i, a, b, c) for (int i = (a); i >= (b); i -= (c))
#define lowbit(x) (x & (-x))

template<typename T>
inline T read() {
    T x = 0;
    bool fg = 0;
    char ch = getchar();
    while (ch < '0' || ch > '9') {
        fg |= (ch == '-');
        ch = getchar();
    }
    while (ch >= '0' && ch <= '9') {
        x = (x << 3) + (x << 1) + (ch ^ 48);
        ch = getchar();
    }
    return fg ? ~x + 1 : x;
}

const int N = 2e6 + 5;

int T, n, all, prefix, suffix;
int pre[30], nxt[30], Z[N], t[30];
char s[N];

void input() {
    scanf("%s", s);
}

void exkmp() {
    int l = 0, r = 0;
    rep (i, 1, n - 1, 1) {
        if (i <= r) {
            Z[i] = min(Z[i - l], r - i + 1);
        }
        while (s[i + Z[i]] == s[Z[i]] and i + Z[i] < n) {
            ++ Z[i];
        }
        if (i + Z[i] - 1 > r) {
            r = i + Z[i] - 1;
            l = i;
        }
    }
    Z[0] = n;
}

void modify(int x) {
    while (x <= 27) {
        ++ t[x];
        x += lowbit(x);
    }
}

int query(int x) {
    int ans = 0;
    while (x) {
        ans += t[x];
        x -= lowbit(x);
    }
    return ans;
}

void deal() {
    n = strlen(s);
    memset(pre, 0, sizeof pre);
    memset(nxt, 0, sizeof nxt);
    memset(Z, 0, sizeof Z);
    memset(t, 0, sizeof t);
    all = prefix = suffix = 0;
    exkmp();
    rep (i, 0, n - 1, 1) {
        if (i + Z[i] == n) {
            -- Z[i];
        }
    }
    rep (i, 0, n - 1, 1) {
        ++ nxt[s[i] - 'a'];
    }
    rep (i, 0, 25, 1) {
        if (nxt[i] & 1) {
            ++ all;
        }
    }
    suffix = all;
    ll ans = 0;
    rep (i, 0, n - 1, 1) {
        if (nxt[s[i] - 'a'] & 1) {
            -- suffix;
        } else {
            ++ suffix;
        }
        -- nxt[s[i] - 'a'];
        if (pre[s[i] - 'a'] & 1) {
            -- prefix;
        } else {
            ++ prefix;
        }
        ++ pre[s[i] - 'a'];
        if (i != 0 && i != n - 1) {
            int t = Z[i + 1] / (i + 1) + 1;
            ans += 1ll * (t / 2) * query(all + 1) + 1ll * (t - t / 2) * query(suffix + 1);
        }
        modify(prefix + 1);
    }
    cout << ans << '\n';
}

void solve() {
    T = read<int>();
    while (T --) {
        input();
        deal();
    }
}

int main() {
    solve();
    return 0;
}

标签:ch,函数,int,void,笔记,while,right,KMP
From: https://www.cnblogs.com/yifan0305/p/17647143.html

相关文章

  • 【Boost】boost.log 要点笔记
    常用简写:namespacelogging=boost::log;namespacesrc=boost::log::sources;namespaceexpr=boost::log::expressions;namespacesinks=boost::log::sinks;namespaceattrs=boost::log::attributes;namespacekeywords=boost::log::keywords;要点:结构图要牢......
  • [Trick] [算法学习笔记] 线段树
    事先声明:本文并非线段树教学。只是一些理解Trick。若您需从0学起线段树建议您移步其他博文呢qwq感谢Idea提供尺子姐姐的博客!,尺子好闪,拜谢尺子!我们在学习线段树的时候,对于乘法“lazytag先乘再加”是不是难以理解?这里介绍一种线段树思考方法。我们可以将序列中的每个元素......
  • Programming abstractions in C阅读笔记:p123-p126
    《ProgrammingAbstractionsInC》学习第50天,p123-p126,总结如下:一、技术总结1.notaion这也是一个在计算机相关书籍中出现的词,但有时却不是那么好理解,因为它可以指代很多对象,这里做一个记录。示例:p124。InC,youcanuseanycharacterarraytoholdstringdata.charstr[6]={......
  • 《深入理解Java虚拟机》读书笔记: 虚拟机类加载的时机和过程
    虚拟机类加载的时机和过程一、类加载的时机类从被加载到虚拟机内存中开始,到卸载出内存为止,它的整个生命周期包括:加载(Loading)、验证(Verification)、准备(Preparation)、解析(Resolution)、初始化(Initialization)、使用(Using)和卸载(Unloading)7个阶段。其中验证、准备、解析3个部分统称......
  • 操作系统笔记04
    进程管理一、进程的基本概念1、进程与程序程序是存储在磁盘上的可执行文件,程序被加载到内存中开始运行称为进程。一个程序可以同时加载多个进程,进程就是处于活动状态下的程序2、进程的分类进程根据功能不同分为三种类型:交互进程、批处理进程、守护进程交互进程:由一个shell......
  • 【学习笔记】优化建图相关(线段树优化,倍增优化)
    优化建图发现并没有人写得很详细的样子,那我也摆烂好惹点击查看目录目录前言线段树优化建图单点连区间区间连区间例题解题:倍增优化建图例题解题:前言众所周知,连边的时间复杂度一般是\(O(1)\),但,当连边的对象是一个连续的树上区间的时候,我们或许有更优的连边方式:优化建图。......
  • 箭头函数与普通函数
    箭头函数与普通函数箭头函数和普通函数在JavaScript中有一些区别,涉及到语法、作用域、this绑定等方面。1.语法:箭头函数使用=>符号来定义,通常可以更简洁地表示函数。普通函数使用function关键字来定义。示例比较://箭头函数constarrowFunction=()=>{//函数体};......
  • keytool : 无法将“keytool”项识别为 cmdlet、函数、脚本文件或可运行程序的名称。请
    如果在运行keytool命令时出现"keytool"项无法识别的错误,可能是因为你没有正确设置Java开发环境或未将keytool添加到系统路径中。你可以按照以下步骤解决此问题:一、确保已正确安装JDK(JavaDevelopmentKit)并配置了Java环境变量。你可以通过在命令提示符或终端中运行java......
  • openGauss学习笔记-46 openGauss 高级数据管理-子查询
    openGauss学习笔记-46openGauss高级数据管理-子查询子查询或称为内部查询,嵌套查询,指的是在数据库查询的WHERE子句中嵌入查询语句,相当于临时表。一个SELECT语句的查询结果能够作为另一个语句的输入值。子查询可以与SELECT,INSERT,UPDATE和DELETE语句一起使用。以下是子查询必须遵......
  • 理解组合函数
    理解组合函数组合(Compose)函数是在JavaScript开发过程中一种对函数的使用技巧、模式:口比如我们现在需要对某一个数据进行函数的调用,执行两个函数fn1和fn2,这两个函数是依次执行的;口那么如果每次我们都需要进行两个函数的调用,操作上就会显得重复;口那么是否可以将这两个函数组合......