从 C S P \mathcal{CSP} CSP 爆炸 到现在,已经有 3 3 3个月了。这三个月间,我——这个小蒟蒻又接触了许多听不懂的东西
-
N o . 1 \mathcal{No.}1 No.1 字符串 h a s h \mathcal{hash} hash:
-
定义:
- 是把任意长度的输入通过散列算法变换成固定长度的输出,该输出就是散列值。这种转换是一种压缩映射,也就是,散列值的空间通常远小于输入的空间,不同的输入可能会散列成相同的输出,所以不可能从散列值来确定唯一的输入值。简单的说就是一种将任意长度的消息压缩到某一固定长度的消息摘要的函数。
e m m m emmm emmm,还是不懂,跳过
- 是把任意长度的输入通过散列算法变换成固定长度的输出,该输出就是散列值。这种转换是一种压缩映射,也就是,散列值的空间通常远小于输入的空间,不同的输入可能会散列成相同的输出,所以不可能从散列值来确定唯一的输入值。简单的说就是一种将任意长度的消息压缩到某一固定长度的消息摘要的函数。
-
特点:
- 时间复杂度较低
- 运用了前缀和思想
- 对相同数据运算,得到的结果是一样的
- 没法逆运算
-
实现:
- 注意:字符串 h a s h hash hash通常使用一个数组来存储 h a s h hash hash值, 并需要在计算时乘以 m o d mod mod( m o d mod mod通常取一个较大质数, l i k e like like 131 131 131, 13331 13331 13331), 数组通常用 l o n g l o n g long\ long long long
- 步骤:
-
首先进行初始化
const int MAXN = 1000010; const long long Base = 131; //初始化MOD(我这用的是Base) char Str[MAXN]; //需处理的字符串 long long Hash[MAXN]; //定义hash数组 void Init(const int &Len) { for (int i = 1; i <= Len; i ++ ) { Hash[i] = Hash[i - 1] * Base + (Str[i] - 'a' + 1); } return ; }
怎么样,简不简单?
简单个锤子!!! -
随后,我们有时需要查询 L e f t \mathcal{Left} Left ~ R i g h t \mathcal{Right} Right 的 h a s h hash hash 值,但 L e f t Left Left 不一定等于 1 1 1, 怎么办?
-
这时, 我们就需要运用前缀和思想,既然要求 L e f t \mathcal{Left} Left ~ R i g h t \mathcal{Right} Right的 h a s h hash hash值,我们是不是可以求出 1 1 1 ~ ( L e f t − 1 ) (\mathcal{Left - }1) (Left−1)的 h a s h hash hash值与 1 1 1 ~ R i g h t \mathcal{Right} Right 的 h a s h hash hash值,再进行操作,就完美解决了!!!
-
上代码
long long Get(const int &Left, const int &Right) { return Hash[Right] - Hash[Left - 1] * pow(Base, Right - Left + 1); }
-
当然,我们会发现, G e t \mathcal{Get} Get函数中调用的的 p o w pow pow过于耗时,所以我们在初始化时可以定义一个 P o w \mathcal{Pow} Pow数组来存储, 实现代码如下:
const int MAXN = 1000010; const long long Base = 131; //初始化MOD(我这用的是Base) char Str[MAXN]; //需处理的字符串 long long Hash[MAXN], Pow[MAXN]; //定义hash数组 void Init(const int &Len) { Pow[0] = 1; //长度为零时初始化为一 for (int i = 1; i <= Len; i ++ ) { Pow[i] = Pow[i - 1] * Base; Hash[i] = Hash[i - 1] * Base + (Str[i] - 'a' + 1); } return ; } long long Get(const int &Left, const int &Right) { return Hash[Right] - Hash[Left - 1] * Pow[Base, Right - Left + 1]; }
-
完美解决!!!
-
-
例题:
-
兔子与兔子 O J \mathcal{OJ} OJ a c w i n g acwing acwing
- 解析:这道题十分简单,直接套 h a s h hash hash模板
- 代码
#include <cstdio> #include <cstring> const int MAXN = 1000010; const long long Base = 131; char Str[MAXN]; long long Hash[MAXN], Pow[MAXN]; void Init(const int &Len) { Pow[0] = 1; for (int i = 1; i <= Len; i ++ ) { Pow[i] = Pow[i - 1] * Base; Hash[i] = Hash[i - 1] * Base + (Str[i] - 'a' + 1); } return ; } long long Get(const int &Left, const int &Right) { return Hash[Right] - Hash[Left - 1] * Pow[Base, Right - Left + 1]; } int main () { scanf ("%s", Str + 1); Len = strlen(Str + 1); Init(Len); scanf ("%d", &m); while (m --) { scanf ("%d %d %d %d", &L1, &R1, &L2, &R2); if (Get(L1, R1) == Get(L2, R2)) { puts("Yes"); } else { puts("No"); } } return 0; }
-
回文子串的最大长度 O J \mathcal{OJ} OJ a c w i n g acwing acwing
- 解法:前缀和 + 后缀和 + 二分 + h a s h hash hash 时间复杂度 O ( n ∗ l o g ( n ) ) \mathcal{O}(n * log(n)) O(n∗log(n))
- 我们发现这道题目数据范围恐怖, 那么只有几个办法可以让我们求解这道题目,那就是 h a s h hash hash, 或者是 O ( n ) \mathcal{O(n)} O(n)复杂度的 M a n a c h e r \mathcal{Manacher} Manacher算法
小蒟蒻不会 - M a n a c h e r \mathcal{Manacher} Manacher由此进入
- 通过兔子与兔子,我们知道判断两个字符串是否相等,可以使用字符串 h a s h hash hash也就是将字符串算成 P \mathcal{P} P进制数值,然后区间和判断即可,那么这道题目我们需要一个正的字符串,还需要一个反的字符串,然后如果正字符串等于反的字符串,那么奇数回文串就 2 + 1 2\ +\ 1 2 + 1,偶数回文串就直接 2 2 2即可.之所以要这么做,因为我们是要回文对不对,我们需要将回文拆解成为一个正字符串和一个反字符串,这样才好处理这道题目.
- 既然如此,我们可以算出一个前缀和,再算出一个后缀和,然后就可以知道,正字符串和一个反字符串.字符串的 h a s h hash hash值就是这个区间的 h a s h hash hash值和.
- 算完之后,我们当前就只需要枚举一个 m i d mid mid中间点,因为所有回文串都是有一个中间点(奇),或者中间区间(偶),然后二分分别寻找这个字符串长度即可,记住不是回文串,回文串的长度,是字符串长度 ∗ 2 + 1 *\ 2\ +\ 1 ∗ 2 + 1(奇) 或者是字符串长度 ∗ 2 \ *\ 2 ∗ 2(偶数).
- 切记如果说这个最大回文串为 1 1 1(也就是所有字符都不一样,比如说 a b c d e f g abcdefg abcdefg),那么输出是 1 1 1,不是 3 3 3,奇数回文串=奇数字符串 ∗ 2 + 1 *\ 2\ +\ 1 ∗ 2 + 1,你们要小心特判这种情况,或者处理二分边界.
- 代码
偷懒不写注释(逃):#include <cstdio> #include <cstring> #include <algorithm> using namespace std; const int MAXN = 100010, Base = 131; int Len, Total, MaxLen, n; char Str[MAXN], s[MAXN]; long long Hash_Front[MAXN], Hash_Back[MAXN], Power[MAXN] = { 1}; long long Get1(int l, int r) { return Hash_Front[r] - Hash_Front[l - 1] * Power[r - l + 1]; } long long Get2(int l, int r) { return Hash_Back[l] - Hash_Back[r + 1] * Power[r - l + 1]; } bool Check1(int L, int x) { return Get1(x - L, x - 1) == Get2(x + 1, x + L); } bool Check2(int L, int x) { return Get1(x - L, x - 1) == Get2(x, x + L - 1); } void Binary_Search(int x) { int Left_ = 0, Right_ = min(x - 1, Len - x), Left__ = 0, Right__ = min(x - 1, Len - x + 1); while (Left_ < Right_) { int Mid = (Left_ + Right_ + 1) >> 1; if (Check1(Mid, x) == true) { Left_ = Mid; } else { Right_ = Mid - 1; } } MaxLen = max(MaxLen, Left_ * 2 + 1); while (Left__ < Right__) { int Mid = (Left__ + Right__ + 1) >> 1; if (Check2(Mid, x) == true) { Left__ = Mid; } else { Right__ = Mid - 1; } } MaxLen = max(MaxLen, Left__ * 2); return ; } int main () { for (int i = 1; i < MAXN; i ++ ) { Power[i] = Power[i - 1] * Base; } while (scanf ("%s", Str + 1) != EOF and Str[1] != 'E') { Len = strlen(Str + 1); MaxLen = 1; Hash_Back[Len + 1] = 0; for (int i = 1; i <= Len; i ++ ) { Hash_Front[i] = Hash_Front[i - 1] * Base + Str[i] - 'a' + 1; } for (int i = Len; i >= 1; i -- ) { Hash_Back[i] = Hash_Back[i + 1] * Base + Str[i] - 'a' + 1; } for (int i = 1; i <= Len; i ++ ) { Binary_Search(i); } printf ("Case %d: %d\n", ++ Total, MaxLen); getchar(); } return 0; }
-
- h a s h hash hash完结!!!
-
-
N o . 2 T r i e \mathcal{No.}2\ \mathcal{Trie} No.2 Trie树(主要字符串)
-
实质(我看来):高效的存储和查找字符串集合的数据结构
-
思想:从根节点开始,判断有没有该类字符,有就向下,没有就添加叶节点,依次存储,把所有结尾点标记一下,然后用 T r i e \mathcal{Trie} Trie高速查找某一个字符出现的次数
-
模板(字符串):
void Insert(char *Str) { //简单的初始化插入 int Root = 0, Len = strlen(Str + 1); for (int i = 1; i <= Len; i ++ ) { int ID = Str[i] - 'a'; if (Trie[Root][ID] == 0) { //如果当前字符未出现过,开一个新空间存储 Trie[Root][ID] = ++ Total; } Root = Trie[Root][ID]; //进入下一个字符 } return ; } bool Find(char *Str) { //查找是否有此单词 int Root = 0, Len = strlen(Str + 1); for (int i = 1; i <= Len; i ++ ) { int ID = Str[i] - 'a'; if (Trie[Root][ID] == 0) { //如果当前字符未匹配成功,返回找不到 return false; } Root = Trie[Root][ID]; //进入下一个字符 } return true; }
-
例题:
-
前缀统计 O J \mathcal{OJ} OJ a c w i n g acwing acwing
- 解析:一想到字符串的前缀,我们就应该想到 T i r e \mathcal{Tire} Tire树.这道题目是字典树模板的略微改动,我们发现这道题目和一般 T i r e \mathcal{Tire} Tire树的查询不一样, T i r e \mathcal{Tire} Tire树一般查询是看这个字符串是否出现,而这道题目这是统计这个字符串出现的次数.
- 代码:
#include <cstdio> #include <cstring> #include <algorithm> using namespace std; const int MAXN = 1e6 + 5; int n, m, Trie[MAXN][26], Total, End[MAXN]; char Str[MAXN]; void Insert(char *Str) { //简单的初始化插入加处理 int Root = 0, Len = strlen(Str + 1); for (int i = 1; i <= Len; i ++ ) { int ID = Str[i] - 'a'; if (Trie[Root][ID] == 0) { //如果当前字符未出现过,开一个新空间存储 Trie[Root][ID] = ++ Total; } Root = Trie[Root][ID]; //进入下一个字符 } End[Root] ++; //统计个数 return ; } bool Find(char *Str) { //查找是否有此单词 int Root = 0, Len = strlen(Str + 1); for (int i = 1; i <= Len; i ++ ) { int ID = Str[i] - 'a'; if (Trie[Root][ID] == 0) { //如果当前字符未匹配成功,返回找不到 return false; } Root = Trie[Root][ID]; //进入下一个字符 } return true; } int Search(char *Str) { int Root = 0, Sum = 0, Len = strlen(Str + 1); for (int i = 1; i <= Len; i ++ ) { int ID = Str[i] - 'a'; Root = Trie[Root][ID]; if (Root == 0) { //如果到了头,就跳出循环 break; } Sum += End[Root]; //统计答案 } return Sum; //返回答案 } int main () { scanf ("%d %d", &n, &m); for (int i = 1; i <= n; i ++ ) { scanf ("%s", Str + 1); Insert(Str); //初始化 } while (m --) { scanf ("%s", Str + 1); printf ("%d\n", Search(Str)); } return 0; }
-
-
T r i e \mathcal{Trie} Trie树完结!!!
-
-
N o . 3 \mathcal{No.}3 No.3 单调栈/单调队列:
- 一种神奇的优化,元素在队列中是单调有序的,由于小蒟蒻理解不够透彻,接不再过多阐述
有兴趣的同学可由此进一步了解 -
例题:
-
直方图中最大的矩形 (单调栈) O J \mathcal{OJ} OJ a c w i n g acwing acwing
- 解析:这道题目是一道单调栈的好题目,首先我们发现,如果说我们确定了这个矩阵的高度的话,那么其实这个矩阵已经确定了,它的 [ l , r ] [l,r] [l,r]其实我们已经求出来了,因为矩阵要最大,所以贪心求出 l , r l,r l,r即可,那么我们就可以以这个矩阵的高度为单调性,然后每一次弹出的时候,就计算出矩阵的面积,然后开一个 A n s \mathcal{Ans} Ans记录最大值就好了.
- 代码:
#include <stack> #include <cstdio> using namespace std; long long Max, Hight, n; struct node { //定义结构体 long long h, len; }t; int main () { while (scanf ("%lld", &n) != EOF && n != 0) { //无限输入 stack <node> S; //维护一个单调不下降的栈 Max = -1; for (int i = 1; i <= n; i ++) { scanf ("%lld", &Hight); long long Len = 0; while (S.empty () == false and S.top().h > Hight) { //如果单调性被破坏 t = S.top(); Len += t.len; // 统计长度 Max = Max < t.h * Len ? t.h * Len : Max; S.pop(); //弹出栈顶元素 } S.push(node{ Hight, Len + 1}); //更新答案 } long long Len_ = 0; while (S.empty() == false) { //弹出栈内元素 t = S.top(); Len_ += t.len; S.pop(); Max = Max < t.h * Len_ ? t.h * Len_ : Max; //更新答案 } printf ("%lld\n", Max); //输出 } return 0; }
-
最大连续和 (单调队列) O J \mathcal{OJ} OJ
- 解析:由题意设 d p [ i ] dp[i] dp[i]表示到第i个数,不超过 m m m的最大连续子段和, s u m [ i ] sum[i] sum[i]表示 1 1 1 ~ i i i的前缀和.则容易得到 d p [ i ] = m a x ( s u m [ i ] − s u m [ k ] ) ( i − m < = k < = i ) dp[i] = max(sum[i] - sum[k]) (i - m <= k <= i) dp[i]=max(sum[i]−sum[k])(i−m<=k<=i),形如此类的递推式可由单调队列维护
- 代码:
//我程序有个漏洞,如果输入全为负数时,输出为零 //所以才会有特判 #include <queue> #include <cstdio> #include <algorithm> using namespace std; const int MAXN = 200010; int n, k, f; deque <long long> Q; //单调队列一般使用STL中的双端队列 long long a[MAXN], Ans = -0x3f3f3f3f; int main(
-
- 一种神奇的优化,元素在队列中是单调有序的,由于小蒟蒻理解不够透彻,接不再过多阐述