提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档
目录
1大数模拟
2并查集
3dp
4最长上升子序列
5最长公共子序列
6基础函数
7二分
8位运算
9一维二维前缀和差分
10双指针,数据离散化
11rmq(求区间最值)
12线性筛
13快速幂
14通过预处理逆元求组合数(n为上万)
15有向图拓扑排序
16堆优化的Gijkstra(mlogn)
17最小生成树(prime)
18lca(最近公共祖先)
19二分图(匈牙利算法)
20滑动窗口(找窗口最大最小值)
21字符串哈希
22kmp字符串匹配
23线段树与树状数组
前言
大部分的算法竞赛都是可以携带纸质的资料,整理了一些常用的算法模板与好用的函数
1大数模拟
加法
string add1(string s1, string s2)
{
if (s1 == "" && s2 == "") return "0";
if (s1 == "") return s2;
if (s2 == "") return s1;
string maxx = s1, minn = s2;
if (s1.length() < s2.length()){
maxx = s2;
minn = s1;
}
int a = maxx.length() - 1, b = minn.length() - 1;
for (int i = b; i >= 0; --i){
maxx[a--] += minn[i] - '0'; // a一直在减 , 额外还要减个'0'
}
for (int i = maxx.length()-1; i > 0;--i){
if (maxx[i] > '9'){
maxx[i] -= 10;//注意这个是减10
maxx[i - 1]++;
}
}
if (maxx[0] > '9'){
maxx[0] -= 10;
maxx = '1' + maxx;
}
return maxx;
}
阶乘
#include <iostream>
#include <cstdio>
using namespace std;
typedef long long LL;
const int maxn = 100010;
int num[maxn], len;
/*
在mult函数中,形参部分:len每次调用函数都会发生改变,n表示每次要乘以的数,最终返回的是结果的长度
tip: 阶乘都是先求之前的(n-1)!来求n!
初始化Init函数很重要,不要落下
*/
void Init() {
len = 1;
num[0] = 1;
}
int mult(int num[], int len, int n) {
LL tmp = 0;
for(LL i = 0; i < len; ++i) {
tmp = tmp + num[i] * n; //从最低位开始,等号左边的tmp表示当前位,右边的tmp表示进位(之前进的位)
num[i] = tmp % 10; // 保存在对应的数组位置,即去掉进位后的一位数
tmp = tmp / 10; // 取整用于再次循环,与n和下一个位置的乘积相加
}
while(tmp) { // 之后的进位处理
num[len++] = tmp % 10;
tmp = tmp / 10;
}
return len;
}
int main() {
Init();
int n;
n = 1977; // 求的阶乘数
for(int i = 2; i <= n; ++i) {
len = mult(num, len, i);
}
for(int i = len - 1; i >= 0; --i)
printf("%d",num[i]); // 从最高位依次输出,数据比较多采用printf输出
printf("\n");
return 0;
}
相减
#include <iostream>
#include <string.h>
using namespace std;
const int N=1005;
void Sub(char *p1,char *p2,int *p3)
{
int i,j,k=0;
int bit=0,t=0; //bit:表示借位,1:表示需要借位,0:表示不需要借位。t:存储中间计算的位相减值
int len1=strlen(p1); //被减数的长度
int len2=strlen(p2); //减数的长度
for(i=len1-1,j=len2-1;i>=0 && j>=0;--i,--j) //此为循环遍历两数,分别对位进行相减操作
{
t=(p1[i]-'0')-(p2[j]-'0')-bit; //计算两个位之间的差值,同时要考虑借位
if(t<0) //如果t小于0,表示需要借位,bit赋值为1,且最终相减结果需加10作为调整并存储到p3数组中,自己用笔画一下就好理解.
{
bit=1;
//例:123-456,t相减实际值分别为-3(3-6-0(bit)),-4(2-5-1(bit)),-4(1-4-1(bit)),加10调整后为:
// 7,6,6,由于最高位相减后还bit为1即还需借位, 因此还需调整,转换为第一种情况,即用1000-667=333,且结果为负
p3[k++]=t+10;
}
else
{
bit=0; //相减为正,则无需调整,直接将t赋给p3对应位。
p3[k++]=t;
}
}
while(i>=0) //strlen(p1)>strlen(p2) ,result is greater than zero
{
t=p1[i]-'0'-bit;
if(t<0)
{
bit=1;
p3[k++]=t+10;
}
else
{
bit=0;
p3[k++]=t;
}
i--;
}
while(j>=0) //strlen(p1)<strlen(p2),result is less than zero
{
t=10-bit-(p2[j]-'0');
p3[k++]=t;
j--;
}
if(bit==1) //对仍有进位的情况考虑,主要分两种:一种是strlen(p1)<strlen(p2),另一种是p1-p2<0,这两种情况bit为1
{
p3[0]=10-p3[0];
for(i=1;i<k;++i)
{
p3[i]=10-p3[i]-bit;
}
}
if(bit==1)
cout<<"-";
for(i=k-1;i>=0;--i)
cout<<p3[i];
cout<<endl;
}
int main()
{
char c1[N],c2[N];
int a[N];
int i,j;
while(cin>>c1>>c2)
{
memset(a,0,sizeof(a));
Sub(c1,c2,a);
}
return 0;
}
2并查集
/*
|合并节点操作|
|16/11/05ztx, thanks to chaixiaojun|
*/
int father[maxn]; // 储存i的father父节点
void makeSet() {
for (int i = 0; i < maxn; i++)
father[i] = i;
}
int findRoot(int x) { // 迭代找根节点
int root = x; // 根节点
while (root != father[root]) { // 寻找根节点
root = father[root];
}
while (x != root) {
int tmp = father[x];
father[x] = root; // 根节点赋值
x = tmp;
}
return root;
}
void Union(int x, int y) { // 将x所在的集合和y所在的集合整合起来形成一个集合。
int a, b;
a = findRoot(x);
b = findRoot(y);
father[a] = b; // y连在x的根节点上 或father[b] = a为x连在y的根节点上;
}
/*
在findRoot(x)中:
路径压缩 迭代 最优版
关键在于在路径上的每个节点都可以直接连接到根上
*/
3动态规划
01背包
void bag01(int cost,int weight) {
for(i = v; i >= cost; --i)
dp[i] = max(dp[i], dp[i-cost]+weight);
}
完全背包
void complete(int cost, int weight) {
for(i = cost ; i <= v; ++i)
dp[i] = max(dp[i], dp[i - cost] + weight);
}
多重背包
void multiply(int cost, int weight, int amount) {
if(cost * amount >= v)
complete(cost, weight);
else{
k = 1;
while (k < amount){
bag01(k * cost, k * weight);
amount -= k;
k += k;
}
bag01(cost * amount, weight * amount);
}
}
// other
int dp[1000000];
int c[55], m[110];
int sum;
void CompletePack(int c) {
for (int v = c; v <= sum / 2; ++v){
dp[v] = max(dp[v], dp[v - c] + c);
}
}
void ZeroOnePack(int c) {
for (int v = sum / 2; v >= c; --v) {
dp[v] = max(dp[v], dp[v - c] + c);
}
}
void multiplePack(int c, int m) {
if (m * c > sum / 2)
CompletePack(c);
else{
int k = 1;
while (k < m){
ZeroOnePack(k * c);
m -= k;
k <<= 1;
}
if (m != 0){
ZeroOnePack(m * c);
}
}
}
4 Lis(最长上升子序列)
/*
状态转移dp[i] = max{ 1.dp[j] + 1 }; j<i; a[j]<a[i];
d[i]是以i结尾的最长上升子序列
与i之前的 每个a[j]<a[i]的 j的位置的最长上升子序列+1后的值比较
*/
void solve(){ // 参考挑战程序设计入门经典;
for(int i = 0; i < n; ++i){
dp[i] = 1;
for(int j = 0; j < i; ++j){
if(a[j] < a[i]){
dp[i] = max(dp[i], dp[j] + 1);
} } }
}
/*
优化方法:
dp[i]表示长度为i+1的上升子序列的最末尾元素
找到第一个比dp末尾大的来代替
*/
void solve() {
for (int i = 0; i < n; ++i){
dp[i] = INF;
}
for (int i = 0; i < n; ++i) {
*lower_bound(dp, dp + n, a[i]) = a[i]; // 返回一个指针
}
printf("%d\n", *lower_bound(dp, dp + n, INF) - dp;
}
/*
函数lower_bound()返回一个 iterator 它指向在[first,last)标记的有序序列中可以插入value,而不会破坏容器顺序的第一个位置,而这个位置标记了一个不小于value的值。
*/
5最长公共子序列
void solve() {
for (int i = 0; i < n; ++i) {
for (int j = 0; j < m; ++j) {
if (s1[i] == s2[j]) {
dp[i + 1][j + 1] = dp[i][j] + 1;
}else {
dp[i + 1][j + 1] = max(dp[i][j + 1], dp[i + 1][j]);
} } }
}
6 好用的函数
int num=456;
char a[5];
sprintf(a , "%d" , int num);
//将num转换成十进制然后逐位存入char型数组a中
%c 整数转成对应的 ASCII 字元
%d 整数转成十进位
%f 倍精确度数字转成浮点数
%o 整数转成八进位
%s 整数转成字符串
%x 整数转成小写十六进位
%X 整数转成大写十六进位
char to int
char a[5] = "123";int num;sscanf(a , "%d" , &num);//将char型数组转换成十进制int型num
[点击并拖拽以移动]
max_element(),min_element():
这两个函数用来快速得到数组中的最大/小值和最大/小值下标
其中num是定义的数组名,6是数组num的长度,例如:
int num[6]={1,0,5,2,5,0};
cout << *max_element(num,num+6) << endl;//则此时输出的是num数组的最大值5
int num[6]={1,0,5,2,5,0};
cout << max_element(num,num+6)-num << endl;//则此时输出的是num数组的最大值的下标2
(注意:无论是max_element()或是min_element(),若数组中最大值为多个相同值时,只按照最先出现的最大值来输出。eg:这里的最大值下标是2而不是4)
去重函数
unique(data,data+n):将date数组去重,n是数组长度
注意:unique只是能对相邻元素去重,例如:
int main()
{
int date[6] = {2,3,3,6,2,5};
int n = unique(date,date+6)-date;
/*因为unique是将重复的数字放在了最后面
所以n用来记录没有重复的数字有多少几个*/
for(int i=0;i<n;i++){
cout << date[i] << endl;
}
//结果是: 2 3 6 2 5
return 0;
}
这样就只能将相邻的3去重,而不相邻的2就不能实现去重,所以用这个函数之前还需要使用sort()进行排序才行,更重要的是所谓的去重并没有将重复的内容删去,而是把他们都排在了最后面
全排列函数int与string
//int型数组:
#include<bits/stdc++.h>
using namespace std;
int main(){
int num[3] = {5,2,6};
sort(num,num+3);
//先使用sort排个序
do{
for(int i=0;i<3;i++)
cout << num[i] << " ";
cout << endl;
}while(next_permutation(num,num+3));
}
字符串比较
string s1="asd", s2="asd";
s1.compare(s2);
/*相等返回0, 大于返回1, 小于返回-1*/
Int to string
string s = "";
int i = 15;
s += to_string(i); // C++11新增 std::to_string
lower_bound/upper_bound
这两个二分查找操作可以在set,数组,vector,map中使用;
数组 或者 vector 中的语法:
序列是升序的(从小到大)
lower_bound(begin(),end(),x) //返回序列中第一个大于等于x的元素的地址
upper_bound(begin(),end(),x) //返回序列中第一个大于x的元素的地址
序列是降序的(从大到小)
lower_bound(begin(),end(),x,greater<tpye>()) //返回序列中第一个小于等于x的元素的地址
upper_bound(begin(),end(),x,greater<type>()) //返回序列中第一个小于x的元素的地址
set 或者 map 中的语法:
和数组差不多,只不过返回的是迭代器:
s.lower_bound(x) //返回序列中第一个大于等于x的元素的地址
s.upper_bound(x) //返回序列中第一个大于x的元素的地址
重点注意:如果当前序列中找不到符合条件的元素,那么返回end(),对于数组来说,返回查询区间的首地址位置,对于set来讲,返回end()-1后面元素的迭代器,也就是begin();
Unordered
unordered_set, unordered_map, unordered_multiset, unordered_multimap, 哈希表
和上面类似,增删改查的时间复杂度是 O(1)
不支持 lower_bound()/upper_bound(), 迭代器的++,--
bitset
bitset, 圧位(存放一个十进制数的二进制,可以像数组一样来使用)
bitset<10000> s;
count() 返回有多少个1
any() 判断是否至少有一个1
none() 判断是否全为0
set() 把所有位置成1
set(k, v) 将第k位变成v
reset() 把所有位变成0
flip() 等价于~
flip(k) 把第k位取反
String
string 是一个很强大的字符串类
size()/length() 返回字符串的长度
reverse(s.begin(),s.end()); 将字符串的反转
s.append(str) 在字符串后面加上字符串str
支持对两个字符串的 ’ + ‘ 操作,实现字符串的拼接(s.append(str)比 + 要慢)
s.erase(0,s.find_first_not_of('0')); //利用string函数去除前导0
s1=s2.substr(起始下标,拷贝长度) //string的截取
pos = s.find('x') //返回string里面字符x的下标;
字符串转化为数字的库函数:
string str = "16";
int a = atoi(str.c_str());//能转换整数和小数,能报异常
int b = strtol(str.c_str(), nullptr, 10);//能指定进制
数字转化为字符串的函数:
int val=123456;
string s=to_string(val);
iterator erase(iterator p):删除字符串中p所指的字符
iterator erase(iterator first, iterator last):删除字符串中迭代器区间 [first, last) 上所有字符
string& erase(size_t pos, size_t len):删除字符串中从索引位置 pos 开始的 len 个字符
7.整数二分模板
bool check(int x) {/* ... */} // 检查x是否满足某种性质
// 区间[l, r]被划分成[l, mid]和[mid + 1, r]时使用:
int bsearch_1(int l, int r)
{
while (l < r)
{
int mid = l + r >> 1;
if (check(mid)) r = mid; // check()判断mid是否满足性质
else l = mid + 1;
}
return l;
}
// 区间[l, r]被划分成[l, mid - 1]和[mid, r]时使用:
int bsearch_2(int l, int r)
{
while (l < r)
{
int mid = l + r + 1 >> 1;
if (check(mid)) l = mid;
else r = mid - 1;
}
return l;
}
浮点数二分模板整数三分模板浮点数三分模板高精度加法高精度减法高精度乘低精度高精度除以低精度
8 位运算模板
求n的二进制的第k位数字: n >> k & 1
返回n的二进制最后一位1所代表的十进制数:lowbit(n) = n & -n
当枚举状态时假设有n个点,每个点有两种状态,那么一共就有2^n个状态,所以可以用位运算来枚举每种方案里面的状态;1~2^n-1里面的所有的数都可以作为一种方案,比如n=5,那么枚举1~31,假设枚举到12,它的二进制为
01100 ,利用位运算判断12的哪一位是1,就证明对第几个点进行了相应的操作;
9子矩阵的和
S[i, j] = 第i行j列格子左上部分所有元素的和(也就是矩阵前缀和)
矩阵前缀和的求法:S[i, j] = S[i-1, j] + s[i, j-1] -s[i-1, j-1]
以(x1, y1)为左上角,(x2, y2)为右下角的子矩阵的和为:
S[x2, y2] - S[x1 - 1, y2] - S[x2, y1 - 1] + S[x1 - 1, y1 - 1]
差分矩阵
void insert(int l,int r,int x)
{
b[l]+=x,b[r+1]-=x;
}
给以(x1, y1)为左上角,(x2, y2)为右下角的子矩阵中的所有元素加上c:
S[x1, y1] += c, S[x2 + 1, y1] -= c, S[x1, y2 + 1] -= c, S[x2 + 1, y2 + 1] += c
10双指针算法
for (int i = 0, j = 0; i < n; i ++ )
{
while (j < i && check(i, j)) j ++ ;
// 具体问题的逻辑
}
常见问题分类:
(1) 对于一个序列,用两个指针维护一段区间
(2) 对于两个序列,维护某种次序,比如归并排序中合并两个有序序列的操作
数据离散化:保序离散化
vector<int> alls; // 存储所有待离散化的值
sort(alls.begin(), alls.end()); // 将所有值排序
alls.erase(unique(alls.begin(), alls.end()), alls.end()); // 去掉重复元素
// 二分求出x对应的离散化的值
int find(int x) // 找到第一个大于等于x的位置
{
int l = 0, r = alls.size() - 1;
while (l < r)
{
int mid = l + r >> 1;
if (alls[mid] >= x) r = mid;
else l = mid + 1;
}
return r + 1; // 映射到1, 2, ...n
}
非保序离散化
unordered_map<int,int> mp;//增删改查的时间复杂度是 O(1)
int res;
int find(int x)
{
if(mp.count(x)==0) return mp[x]=++res;
return mp[x];
}
11RMQ(ST表查询区间最值)
//以查询最大值为例
状态表示: 集合:f(i,j)表示从位置i开始长度为2^j的区间的最大值;
属性:MAX
状态转移: f(i,j)=max(f(i,j-1),f(i+(1<<(j-1)),j-1));
含义:把区间[i,i+2^j],分成两半,[i,i+2^(j-1)]和[i+(1<<(j-1)),2^j],整个区间最大值就是这两段区间最大值的最大值
const int N=2e5+7,M=20;
int dp[N][M]; //存储区间最大值
int a[N];//存放每个点的值
//dp求从位置i开始长度为2^j的区间的最大值
for(int j=0;j<M;j++)
{
for(int i=1;i+(1<<j)-1<=n;i++)
{
if(!j) dp[i][j]=a[i];
else dp[i][j]=max(dp[i][j-1],dp[i+(1<<(j-1))][j-1]);
}
}
//求任意区间的最大值;(可以预处理log)
int res=log(b-a+1)/log(2);
cout <<max(dp[a][res],dp[b-(1<<res)+1][res])<<endl;
}
数学知识
12线性筛法求素数
int primes[N], cnt; // primes[]存储所有素数
bool st[N]; // st[x]存储x是否被筛掉
void get_primes(int n){
for (int i = 2; i <= n; i ++ ){
if (!st[i]) primes[cnt ++ ] = i;
for (int j = 0; primes[j] <= n / i; j ++ ){
st[primes[j] * i] = true;
if (i % primes[j] == 0) break;
}
}
}
13快速幂
求 m^k mod p,时间复杂度 O(logk)。
int qmi(int m, int k, int p){
int res = 1 % p, t = m;
while (k){
if (k&1) res = res * t % p;
t = t * t % p;
k >>= 1;
}
return res;
}
位运算处理大数相乘(1e18)
//0 < a,b,p < 1e18 ;
//求a * b % p
//原理把乘法变成加法
ll quick_add(ll a,ll b,ll p){
ll res=0;
while(b){
if(b&1) res=(res+a)%p;
a=(a+a)%p;
b>>=1;
}
return res;
}
14求组合数
通过预处理逆元的方式求组合数(数据规模上万)
首先预处理出所有阶乘取模的余数fact[N],以及所有阶乘取模的逆元infact[N]
如果取模的数是质数,可以用费马小定理求逆元
int qmi(int a, int k, int p) // 快速幂模板
{
int res = 1;
while (k)
{
if (k & 1) res = (LL)res * a % p;
a = (LL)a * a % p;
k >>= 1;
}
return res;
}
// 预处理阶乘的余数和阶乘逆元的余数
fact[0] = infact[0] = 1;
for (int i = 1; i < N; i ++ )
{
fact[i] = (LL)fact[i - 1] * i % mod;
infact[i] = (LL)infact[i - 1] * qmi(i, mod - 2, mod) % mod;
}
15图论
有向图的拓扑序
bool topsort()
{
// inv存储点的入度
for(int i=1;i<=n;i++)
{
if(inv[i]==0) q.push(i);
}
while(!q.empty())
{
int res=q.front();
q.pop();
p.push(res);
for(int i=h[res];i!=-1;i=ne[i])
{
int j=e[i];
inv[j]--;
if(inv[j]==0) q.push(j);
}
}
if(p.size()==n) return true; //如果所有点都入队,说明存在拓扑序
else return false;
}
16最短路算法模板
堆优化版Dijkstra O(mlogn)
typedef pair<int, int> PII;
int n; // 点的数量
int h[N], w[N], e[N], ne[N], idx; // 邻接表存储所有边
int dist[N]; // 存储所有点到1号点的距离
bool st[N]; // 存储每个点的最短距离是否已确定
// 求1号点到n号点的最短距离,如果不存在,则返回-1
int dijkstra()
{
memset(dist, 0x3f, sizeof dist);
dist[1] = 0;
priority_queue<PII, vector<PII>, greater<PII>> heap;
heap.push({0, 1}); // first存储距离,second存储节点编号
while (heap.size())
{
auto t = heap.top();
heap.pop();
int ver = t.second, distance = t.first;
if (st[ver]) continue;
st[ver] = true;
for (int i = h[ver]; i != -1; i = ne[i])
{
int j = e[i];
if (dist[j] > distance + w[i])
{
dist[j] = distance + w[i];
heap.push({dist[j], j});
}
}
}
if (dist[n] == 0x3f3f3f3f) return -1;
return dist[n];
}
17最小生成树
prim算法
int n; // n表示点数
int g[N][N]; // 邻接矩阵,存储所有边
int dist[N]; // 存储其他点到当前最小生成树的距离
bool st[N]; // 存储每个点是否已经在生成树中
// 如果图不连通,则返回INF(值是0x3f3f3f3f), 否则返回最小生成树的树边权重之和
int prim()
{
memset(dist, 0x3f, sizeof dist);
int res = 0;
for (int i = 0; i < n; i ++ )
{
int t = -1;
for (int j = 1; j <= n; j ++ )
if (!st[j] && (t == -1 || dist[t] > dist[j]))
t = j;
if (i && dist[t] == INF) return INF;
if (i) res += dist[t];
st[t] = true;
for (int j = 1; j <= n; j ++ ) dist[j] = min(dist[j], g[t][j]);
}
return res;
}
18最近公共祖先
倍增在线算法
void bfs(int root)//从根节点开始处理每个点的深度并且进行fa的倍增
{
memset(depth,0x3f,sizeof depth);
queue<int> q;
q.push(root);
depth[0]=0,depth[root]=1;//设置哨兵
while(q.size())
{
int t=q.front();
q.pop();
for(int i=h[t];~i;i=ne[i])
{
int j=e[i];
if(depth[j]>depth[t]+1)
{
depth[j]=depth[t]+1;
fa[j][0]=t;
q.push(j);
for(int k=1;k<16;k++)//倍增里面的常数根据题目节点个数而定
fa[j][k]=fa[fa[j][k-1]][k-1];
}
}
}
}
int lca(int a,int b)
{
if(depth[a]<depth[b]) swap(a,b);//对深度大的点进行往上跳跃
for(int k=15;k>=0;k--)
if(depth[fa[a][k]]>=depth[b])
a=fa[a][k];
if(a==b) return a;
for(int k=15;k>=0;k--)//两个点同时跳跃,直到最近公共祖先的下面的节点
{
if(fa[a][k]!=fa[b][k])
{
a=fa[a][k];
b=fa[b][k];
}
}
return fa[a][0];//返回a的父亲,也就是最近公共祖先
}
19二分图
染色法
匈牙利算法(把妹算法)
nt n1, n2; // n1表示第一个集合中的点数,n2表示第二个集合中的点数
int h[N], e[M], ne[M], idx; // 邻接表存储所有边,匈牙利算法中只会用到从第一个集合指向第二个集合的边,所以这里只用存一个方向的边
int match[N]; // 存储第二个集合中的每个点当前匹配的第一个集合中的点是哪个
bool st[N]; // 表示第二个集合中的每个点是否已经被遍历过
bool find(int x)
{
for (int i = h[x]; i != -1; i = ne[i])
{
int j = e[i];
if (!st[j])
{
st[j] = true;
if (match[j] == 0 || find(match[j]))
{
match[j] = x;
return true;
}
}
}
return false;
}
// 求最大匹配数,依次枚举第一个集合中的每个点能否匹配第二个集合中的点
int res = 0;
for (int i = 1; i <= n1; i ++ )
{
memset(st, false, sizeof st);
if (find(i)) res ++ ;
}
20滑动窗口
常见模型:找出滑动窗口中的最大值/最小值
int hh = 0, tt = -1;
for (int i = 0; i < n; i ++ ){
while (hh <= tt && check_out(q[hh])) hh ++ ; // 判断队头是否滑出窗口
while (hh <= tt && check(q[tt], i)) tt -- ;
q[ ++ tt] = i;
}
21字符串哈希
核心思想:将字符串看成P进制数,P的经验值是131或13331,取这两个值的冲突概率低
小技巧:取模的数用2^64,这样直接用unsigned long long存储,溢出的结果就是取模的结果
typedef unsigned long long ULL;
ULL h[N], p[N]; // h[k]存储字符串前k个字母的哈希值, p[k]存储 P^k mod 2^64
// 初始化
p[0] = 1;
for (int i = 1; i <= n; i ++ )
{
h[i] = h[i - 1] * P + str[i];
p[i] = p[i - 1] * P;
}
// 计算子串 str[l ~ r] 的哈希值
ULL get(int l, int r){
return h[r] - h[l - 1] * p[r - l + 1];
}
22KMP——字符串匹配
KMP最小循环节、循环周期:
定理:假设S的长度为 len,则S存在最小循环节,循环节的长度L为 len-next[len] ,子串为S[0…len-next[len]-1]。
(1)如果 len 可以被 len - next[len] 整除,则表明字符串S可以完全由循环节循环组成,循环周期 T=len/L。
(2)如果不能,说明还需要再添加几个字母才能补全。需要补的个数是循环个数 L-len%L=L-(len-L)%L=L-next[len]%L,L=len-next[len]。
(3) 循环节出现的长度就是当前位置的长度
#include <bits/stdc++.h>
using namespace std;
const int N=1e6+7;
char p[N],s[N];//p是模式串(短),s是文本串(长)
int ne[N];//next[j]就是待匹配串从t[0]开始到t[j-1]结尾的这个子串中,前缀和后缀相等时对应前缀/后缀的最大长度
int main()
{
int n;cin >>n>>p+1; //字符串都从1开始
int m;cin >>m>>s+1;
for(int i=2,j=0;i<=n;i++)//先求模式串本身的next数组
{
while(j&&p[i]!=p[j+1]) j=ne[j];
if(p[i]==p[j+1]) j++;
ne[i]=j;
}
for(int i=1,j=0;i<=m;i++)
{
while(j&&s[i]!=p[j+1]) j=ne[j];
if(s[i]==p[j+1]) j++;//当前能两个字符串能匹配的最大长度
//max_p=max(max_p,j);
if(j==n)//匹配成功
{
cout <<i-n<<" ";
j=ne[j];
}
}
}
23线段树
单点修改
int w[N];//区间里的数
int n,m;
struct node
{
int l,r; //当前结点所处区间
int sum; //当前区间的权值和
int lmax; //当前区间的最大前缀和
int rmax; //当前区间的最大后缀和
int tmax; //当前区间的最大连续子序列和
}tr[N*4];//注意对题中所给操作数量或者数据要开4倍大
void pushup(node &u,node &l,node &r)
{
u.sum=l.sum+r.sum;//父节点的和等于 左节点+右节点
u.lmax=max(l.lmax,l.sum+r.lmax); //父节点的最大前缀和等于max(左孩子最大前缀和,左孩子的和+右孩子的最大前缀和)
u.rmax=max(r.rmax,r.sum+l.rmax);//和上面同理
u.tmax=max(max(l.tmax,r.tmax),l.rmax+r.lmax);//包含三种情况,属于左孩子,属于有孩子,或者跨区间左边和右边
}
void pushup(int u)// 由子节点更新父节点
{
pushup(tr[u],tr[u<<1],tr[u<<1|1]);
}
void build(int u,int l,int r)
{
if(l==r) tr[u]={l,r,w[r],w[r],w[r],w[r]};//如果处理到叶结点了,就保存叶结点的信息
else
{ tr[u]={l,r}; //保存当前节点的区间信息
int mid=l+r>>1;
build(u<<1,l,mid); //递归左节点
build(u<<1|1,mid+1,r); // 递归右节点
pushup(u); //每次根据子节点更新父节点
}
}
void modify(int u,int x,int v)
{
if(tr[u].l==x&&tr[u].r==x) tr[u] = {x,x,v,v,v,v}; // //如果处理到叶结点了,就保存叶结点的信息
else
{
int mid=tr[u].l+tr[u].r>>1; //当前节点区间的中点
if(x<=mid) modify(u<<1,x,v); //如果要修改的地方处于中点的左端,则递归其左儿子
else modify(u<<1|1,x,v); // 如果要修改的地方处于中点的右端,则递归其右儿子
pushup(u); //修改完之后由子节点更新父节点的信息
}
}
node query(int u,int l,int r) //在区间l,r里面查询
{
if(tr[u].l>=l&&tr[u].r<=r) return tr[u]; // 如果当前区间在l~r里面,则直接返回想要的信息
else
{
int mid=tr[u].l+tr[u].r>>1; //取当前节点的区间中点
if(r<=mid) return query(u<<1,l,r); // 如果当前查询区间在当前区间的中点左端,则递归左儿子
else if(l>mid) return query(u<<1|1,l,r); //如果当前查询区间在当前节点区间的右端,则递归右儿子;
else //如果一部分在mid左边,一部分在mid右边
{
auto left=query(u<<1,l,r); //递归左儿子
auto right=query(u<<1|1,l,r); //递归右儿子
node res;
pushup(res,left,right); //由左儿子和右儿子的信息来更新当前父节点的信息
return res;
}
}
}
区间整体加和乘(区间修改)
int w[N];//区间里的数
int n,m,p;
struct node
{
ll l,r; //当前结点所处区间
ll sum; //当前区间的权值和
ll add; //当前区间所具有加权值的懒标记
ll mul; //当前区间所具有倍数权值的懒标记
}tr[N*4];//注意对题中所给操作数量或者数据要开4倍大
void eval(node &root,int add,int mul)
{
//更新公式: (root.mul * root.sum + root.add)*mul+add
root.sum=(root.sum*mul+(root.r-root.l+1)*add)%p;
root.mul=root.mul*mul%p;
root.add=(root.add*mul+add)%p;
}
void pushup(int u)// 由子节点更新父节点
{
tr[u].sum=(tr[u<<1].sum+tr[u<<1|1].sum)%p;
}
void pushdown(int u)
{
eval(tr[u<<1],tr[u].add,tr[u].mul);
eval(tr[u<<1|1],tr[u].add,tr[u].mul);
tr[u].add=0,tr[u].mul=1;//恢复懒标记
}
void build(int u,int l,int r)
{
if(l==r) tr[u]={l,r,w[r],0,1};//如果处理到叶结点了,就保存叶结点的信息
else
{ tr[u]={l,r,0,0,1}; //保存当前节点的信息
int mid=l+r>>1;
build(u<<1,l,mid); //递归左节点
build(u<<1|1,mid+1,r); // 递归右节点
pushup(u); //每次根据子节点更新父节点
}
}
Void modify(int u,int l,int r,int add,int mul)
{
if(tr[u].l>=l&&tr[u].r<=r)
{
eval(tr[u],add,mul);//当前树中区间被包含在修改区间时,直接修改即可;
}
else
{
pushdown(u);
int mid=tr[u].l+tr[u].r>>1; //当前节点区间的中点
if(l<=mid) modify(u<<1,l,r,add,mul); //如果要修改的地方处于中点的左端,则递归其左儿子
if(r>mid) modify(u<<1|1,l,r,add,mul); // 如果要修改的地方处于中点的右端,则递归其右儿子
pushup(u); //修改完之后由子节点更新父节点的信息
}
}
node query(int u,int l,int r) //在区间l,r里面查询
{
if(tr[u].l>=l&&tr[u].r<=r) return tr[u]; // 如果当前区间在l~r里面,则直接返回想要的信息
else
{
pushdown(u);
node res;
res.sum=0;
int mid=tr[u].l+tr[u].r>>1; //取当前节点的区间中点
if(l<=mid) res.sum+=query(u<<1,l,r).sum%p;//查询区间的和等于左右两个子树区间的和
if(r>mid) res.sum+=query(u<<1|1,l,r).sum%p;
pushup(u);
return res;
}
}
树状数组的三个操作
int lowbit(int x)
{
return x&-x;
}
void modify(int x,int c)//修改树状数组x位置的值
{
for(int i=x;i<=n;i+=lowbit(i)) tr[i]+=c;
}
int query(int x)//查询区间1~x的区间和;
{ int res=0;
for(int i=x;i>=1;i-=lowbit(i)) res+=tr[i];
return res; }
总结
后续在学习和比赛的过程中会继续补充